ERC-8419: Know-Your-Agent (KYA) Framework

Assigned ERC-8419 (2026-09-20). Latest: revision 4 — see the replies below; current Sepolia addresses are in the repo README.

PR: ethereum/ERCs#2012 · Repo / reference implementation: garyyang-finchip/kya-standard · Status: Draft, ERC-8419

What this proposes

ERC-8004 gives agents portable identity and a place to accumulate raw trust signals (Reputation, Validation). It deliberately stops short of trust conclusions — a shared way to say “this agent was checked under principle X, by issuer Y, reached level L, valid until T, not revoked.”

This ERC standardises the container for such conclusions, not the conclusions themselves:

  • Scheme Registry — registers a KYA Scheme: an addressable, versioned, freezable pointer to a descriptor that says what is checked, how the result is expressed as a uint8 level, which evidence kinds are admissible, and how assertions are admitted (ATTESTED by an issuer, or PROVED by a verifier contract).
  • KYA Registry — records Assertions (subjectKey, schemeId, issuer, level, claimDigest, issuedAt, expiresAt, evidenceHash, status) with per-issuer supersession and revocation, and resolves them: resolve(subject, schemeId, issuers[]) / check(subject, schemeId, minLevel, issuers[]). issuers must be non-empty — same Sybil rationale as 8004’s getSummary requiring clientAddresses.
  • ZK-KYA profile — not a second registry. attestWithProof(subject, schemeId, publicInputs, proof) calls the scheme’s IKYAVerifier, which returns (ok, subjectKey, nullifier, level, claimDigest, expiresAt). The registry enforces subject binding and nullifier consumption and records the assertion with issuer = verifier. Any proving system enters via an adapter; the spec fixes only the public-input layout (kya-public-v1). Proofs can also be presented ephemerally in the handshake without ever touching the chain.
  • Handshake — EIP-712 KYAChallenge / KYAPresentation, /.well-known/kya.json discovery; mutual KYA is two exchanges.
  • Policy (minimal)registerPolicy(uri, hash) gives relying-party requirements an id that can be named in a challenge; on-chain evaluation is optional.
  • ERC-8004 bindingerc8004 is the MUST-support subject type (chainId, identityRegistry, agentId). Optional supportedTrust: ["kya","zk-kya"], a KYA services entry, a "kya" metadata key, and a KYA Bridge that acts as an 8004 validator and mirrors levels to 0–100 under tag = "kya:<8 hex of schemeId>", so 8004-only clients see KYA outcomes with zero changes to 8004.

The framework never defines a KYA algorithm or a credit rule. That is the point: like 8004 with reputation math, fixing rules on-chain would be obsolete before Final; a container for rules is not.

What is in the repo

Full ERC text; reference contracts (KYASchemeRegistry, KYARegistry, KYAPolicyRegistry, KYABridge8004, a Groth16→IKYAVerifier adapter); JSON Schemas for scheme / policy / discovery documents; deterministic vectors (ids, EIP-712 digests, ERC-165 ids); a 17-case end-to-end suite on an in-process EVM covering attested and proved flows, revocation/expiry, policy evaluation and the full 8004 bridge round-trip.

Where I would most like feedback

  1. Subject abstraction — is (subjectType, subjectData) with erc8004 as MUST and account / erc721 / did as optional the right cut, or should v1 be 8004-only?
  2. Verifier-as-issuer in proved mode — relying parties trust a verifier address the same way they trust an attester. Is that vocabulary acceptable, or should proved assertions carry a separate verifier field?
  3. Bridge design — a curated validator with operator-configured issuers and level→0–100 map, deterministic requestHash bound to (agentId, schemeId). Is mirroring into Validation (not Reputation) the right choice?
  4. Nullifier scopingscheme vs scheme-epoch; should domain separation (chainId + registry) be MUST rather than SHOULD?
  5. Relationship to ERC-8143 and to generic attestation services — this draft positions itself as the agent-KYA semantic layer that can sit on top of either. Push back welcome.

Prior work from the same author: ERC-8338 (Token-Bound Executable Skills), ERC-8414 (Token-Bound Task Tenders). Those appear here only as informative use cases; nothing in this ERC depends on them.

Update: revision 2 of the draft after an external design review, plus a live Sepolia reference deployment

Since the OP we had the whole design reviewed with an adversarial eye. Several points were right and are now in the text and the reference implementation (PR updated in place; the kya-standard repo is in sync). The short version of what changed, and why:

What the framework is (and is not). KYA is a registry for trust conclusions about agents — not an identity system, not a reputation score, not an anonymity layer. Each conclusion is addressed by a scheme whose semantics are now immutable per schemeId (only the descriptor URI can be re-pointed; any semantic change is a new scheme with predecessor). Every scheme declares what it binds to — the identity record, the current controller, or a specific instance — because an ERC-8004 agent is a transferable ERC-721 and “this agent is accountable” does not automatically survive a sale. Relying parties get a rule they can implement (check for transfers since issuedAt); the registry stays ignorant of ERC-721 mechanics.

level is still a uint8, but ordering is now a profile. resolve’s “highest wins”, check’s minLevel and the bridge’s responseMap all assume a total order. That assumption is isolated as the Ordered-Level Profile (result.kind = "ordered-level"); pass/fail and categorical results use level as a code or leave it 0 with the result committed in claimDigest.

ZK-KYA, stated precisely. The profile hides the fact issuer and the underlying facts. It does not hide the subject — subjectKey is public — and we no longer describe it as anonymous. Two roles are now explicit: the fact issuer (attestor, never named on-chain) and the admitting verifier (recorded as issuer); the assertion carries an anchor (for the reference circuit, the issuer-set root) so relying parties pin (verifier, anchor) rather than an address alone. Three concrete fixes came out of the review:

  • the attestor’s credential now includes schemeId, and the circuit constrains it to the adapter’s argument — otherwise a credential issued under a lenient scheme was presentable under a strict one that shares attestors (cross-scheme replay);
  • the epoch is enforced on-chain from block.timestamp (current − grace ≤ epoch ≤ current), not supplied by the prover — otherwise “re-prove per epoch” was “mint a fresh nullifier at will”;
  • the 256-bit → (hi, lo) split uses an alias-checked decomposition, so a nullifier has exactly one encoding.

Bridge. Explicitly an optional, lossy snapshot of the KYA Registry: requestHash now includes the bridge address, a level outside the response map makes sync revert instead of guessing upward, and the text says plainly that a transfer after sync leaves a stale mirror until someone re-syncs.

Handshake. Acceptance rules are normative now (single-use challenge, presented assertions must belong to the challenge’s schemeIds, policy decides partial satisfaction, empty presentation only satisfies an empty challenge). The policy interface is split into a required base and an optional on-chain evaluator, discoverable via ERC-165.

Sepolia (revision 2), against the official ERC-8004 IdentityRegistry 0x8004A818…BD9e, agentId 10387

contract address
KYASchemeRegistry 0xAc7B642a5760467178F20c984f02cc72CCCa8E3f
KYARegistry 0xeBf357c87639aCcC0DaA505fAAf3F6E9b079AB9f
KYAPolicyRegistry 0x9B53BAD4346AacB34Bac328F1d856dfa2F246cfF
ValidationRegistry8004 (spec-conforming stand-in; canonical one not yet deployed) 0x4aD87d7D55C753c53A801ca9a23495fb4f9bAd53
KYABridge8004 0xCcd683927d1fC03A0163B3e9FbE7fC44afFbA5e4
Groth16Verifier (circuit rev 2, test ceremony) 0xBaE26c2a9b23c37Da76c4905F9F09004dFDBc7a9
Groth16KYAVerifierAdapter (30-day epochs, grace 1, pinned issuer-set root) 0xbC2E1A0FF358B64288a7D4157B996C72b6Ad44cD

Worked examples, every one read back from chain: setMetadata(10387, "kya", …) on the official registry (tx) · attested scheme, attest(level 2)check(≥2) true (tx) · attestWithProof with a real Groth16 proof (scheme-bound credential, epoch 690 enforced by the adapter, recorded anchor = the pinned issuer-set root; which of the three demo attestors signed is not recoverable on-chain) → check(≥4, [adapter]) true (tx) · policy allOf[attested ≥ 2, proved ≥ 4], evaluate() true (tx) · ERC-8004 bridge: agent owner files validationRequest with the deterministic requestHash, anyone calls sync, Validation Registry reports 60/100 under tag kya:3673f050, getSummary(10387, [bridge], tag) → count 1 / average 60 — request tx · sync tx

Everything — tx hashes, the proof, public signals, descriptors, and the superseded revision-1 deployment for the record — is in deployments/ of garyyang-finchip/kya-standard. npm test runs 18 end-to-end cases on an in-process EVM; npm run test:zk runs 14 more with the real circuit, including the adversarial ones (prover-chosen future/stale epoch, cross-scheme replay, tampered signals, aliased encodings, attestor outside the pinned set). The Groth16 keys are a single-party test ceremony and say so everywhere.

Two things we would still like input on: whether binding should be a per-scheme declaration (current) or a per-assertion field, and whether the epoch window rule belongs in the ERC text as MUST for every epoch-scoped scheme (current) or should be left to the verifier’s descriptor.

Rev2 is close. I think the remaining work reduces to one invariant:

Every value or transformation capable of changing a KYA decision must either be committed by the identity of the object being evaluated, or be explicitly declared live/external and excluded from claims of complete policy satisfaction.

Applying that gives fairly direct answers to the two open questions and exposes a few remaining closure points.

1. Binding stays per scheme

I would not make the binding kind per assertion.

If scheme S means controller-bound, assertions under S should not independently decide whether they are identity-, controller-, or instance-bound. Otherwise schemeId no longer identifies one semantic rule.

The useful split is:

  • binding kind = scheme-level semantics
  • binding witness = assertion-level committed state
  • binding validity = resolution-time predicate

For example, a controller-bound scheme can require the assertion to commit the controller at issuance. An instance-bound scheme can commit the relevant code/model/configuration instance.

Conceptually:

valid(A, now) = active + unexpired + binding predicate satisfied

The reason this distinction matters is already present in rev2.

The text says a controller-bound assertion MUST be treated as invalid if the ERC-8004 token transfers after issuance. But KYARegistry.check() and the optional KYAPolicyRegistry.evaluate() currently do not evaluate that transfer boundary.

Concrete case:

  • t0: Alice owns agent 10387.
  • A controller-bound assertion gives the agent level 4.
  • evaluate(policy) returns true.
  • t1: Alice transfers agent 10387 to Bob.
  • t2: the assertion remains ACTIVE and unexpired.

A relying party implementing the binding rule returns false.

The current on-chain evaluator can still return true.

So two consumers can derive different answers to “does this subject satisfy policy P?” from the same KYA registry state.

I think v1 should resolve this explicitly in one of two ways:

Complete evaluation: an assertion carries the concrete binding witness, and a subject-profile binding resolver participates before a result can mean “policy satisfied.”

Registry-local evaluation: check() / evaluate() only establish registry-local assertion conditions and MUST NOT claim complete policy satisfaction when the declared binding predicate has not been evaluated.

I prefer the first semantic model while keeping subject-specific resolvers modular.

2. Keep the epoch MUST, but put the MUST on authority

The invariant I would preserve is:

The prover MUST NOT control the epoch that determines replay eligibility.

For the current scheme-epoch profile, the existing rule should remain normative:

current = floor(block.timestamp / epochSeconds)

and the accepted epoch remains within:

current - epochGrace <= epoch <= current

What can be generalized later is the mechanism by which a profile derives its epoch, not who gets authority to choose it.

So:

  • epoch authority = MUST be system/verifier determined
  • epoch mechanism = profile-defined

A later profile could use block ranges, finalized periods, or another deterministic source.

What should not become descriptor-optional is the property that the prover cannot manufacture a fresh replay domain by choosing another epoch.

3. Policy identity should commit the policy that actually executes

This looks like the largest remaining closure point.

Today registerPolicyWithRules(policyURI, policyHash, allOf) derives policyId from the owner, policyHash, and nonce, while the executable allOf rules are supplied separately.

Nothing currently proves that the executable rules correspond to the document committed by policyHash.

For example, the committed policy document could require:

  • scheme X >= 4
  • issuer A

while the executable rules supplied on-chain require:

  • scheme X >= 1
  • issuer A

An off-chain evaluator verifying policyHash gets one policy.

The on-chain evaluator executes another.

Both still refer to the same policyId.

That means the identifier does not uniquely identify the decision rule.

I think the executable projection needs a commitment of its own, e.g. a canonical rulesHash, and policyId should bind both the policy document and the exact executable projection.

Conceptually:

rulesHash = H(canonical executable rules)

policyId = H(owner, policyHash, rulesHash, nonce)

If the policy descriptor advertises an on-chain projection, it should carry the same rulesHash.

There is a second related boundary.

The general policy language supports semantics such as anchor restrictions and categorical equality, while the current on-chain Rule is effectively only schemeId + minLevel + issuers, with evaluation through level >= minLevel.

I would not make the optional evaluator much heavier.

Instead, define its supported projection precisely — for example:

  • top-level allOf
  • ordered-level schemes
  • issuer constraints
  • no unsupported categorical/anchor semantics

and reject anything outside that subset.

The important invariant is:

One policyId must identify one policy meaning and one exact advertised executable projection.

4. The ERC-8004 bridge needs a configuration identity

Adding the bridge address to requestHash fixed ambiguity between different bridges.

There is still ambiguity between different configurations of the same bridge.

The operator can change trustedIssuers and responseMap, while the current response commitment binds the assertion, level, and issuers but not the response mapping.

Consider an assertion A that resolves to level 2.

Configuration C0:

  • issuers = [I]
  • responseMap = [0, 50, 100]
  • sync produces response 100

Configuration C1:

  • issuers = [I]
  • responseMap = [0, 20, 70]
  • sync produces response 70

The request can remain the same.

The assertion remains the same.

The existing response commitment can remain the same.

But the projected ERC-8004 conclusion changes.

Repeated ERC-8004 validation responses are not the problem. The issue is that the interpretation producing the result changed without changing its committed identity.

I would give the bridge configuration its own collision-resistant commitment, something equivalent to:

configHash = H(schemeId, trustedIssuers, responseMap)

Then bind that configuration identity into the request and response provenance.

Changing issuer authority or level-to-ERC-8004 interpretation then creates a new interpretation identity rather than silently changing what an existing request means.

5. The shortened ERC-8004 tag should not become a semantic collision point

The current bridge uses "kya:" + first 8 hex characters of schemeId.

That leaves only a 32-bit discriminator.

Two distinct schemes can intentionally share that prefix and therefore share the same ERC-8004 tag.

If a consumer aggregates by bridge + tag, semantically different KYA schemes can become indistinguishable.

I would use the complete schemeId, or another collision-resistant identifier derived from the complete scheme/configuration identity.

6. Semantic immutability also constrains verifier behavior

Rev2 establishes the right principle: one schemeId should denote one meaning, and semantic changes require a successor scheme.

But a PROVED scheme currently commits a verifier address.

If that address is an upgradeable proxy, the address can remain constant while verify() changes behavior.

Then the same schemeId has acquired new proof-admission semantics without becoming a new scheme.

I would make the invariant normative:

Every verifier code path, verification key, issuer set, configuration, or other dependency capable of changing proof acceptance for an existing schemeId MUST remain semantically immutable for that scheme. Any semantic change MUST use a successor scheme.

Upgradeable infrastructure can still exist. It simply cannot retroactively change the meaning of an already-registered scheme.

The current immutable reference adapter already has the right shape.

7. Chain and registry domain separation should be mandatory for canonical ZK admission

Nullifiers are consumed within one registry.

If a proof does not bind its chain and KYA registry, a second registry has an independent nullifier set and can admit the same proof.

For the canonical ZK-KYA registry-admission profile, I would therefore make chain + registry domain separation a MUST.

If proof portability across registries is desirable later, that should be an explicit portability profile rather than accidental replay caused by an omitted domain.

8. Per-action judgment should stay separate from durable KYA state

On the adjacent per-action-verdict question, I think the boundary is clean.

A per-action judgment asks:

Was action E correct under procedure P?

A KYA assertion asks:

What trust conclusion currently holds about subject A under scheme S?

Those should compose, not collapse.

A per-action judgment can be evidence consumed by a KYA scheme.

A KYA policy can also constrain who is eligible to issue or adjudicate such a judgment.

But the event verdict itself should not become durable subject-trust state merely by encoding its result as a level.

Regression cases

Before considering the semantic boundary closed, I would add six regression cases:

  1. Policy projection mismatch — a strict committed policy and weaker executable rules cannot share one policyId.

  2. Controller transfer — a controller-bound assertion cannot produce a complete policy-satisfied result after transfer unless binding validity was actually evaluated.

  3. Bridge configuration mutation — changing trusted issuers or responseMap changes the committed interpretation identity.

  4. Verifier semantic mutation — an existing schemeId cannot acquire different proof-admission behavior through verifier mutation.

  5. Tag collision — distinct scheme IDs sharing the same shortened prefix remain distinguishable in the ERC-8004 projection.

  6. Cross-registry replay — a canonical ZK-KYA proof bound to registry A cannot be admitted into registry B.

If those invariants hold, I think the model closes cleanly:

  • schemeId → one semantic rule
  • assertion → one subject + one concrete binding state
  • policyId → one policy + one exact executable projection
  • proof → one authorized replay domain
  • bridge request → one exact projection configuration
  • response → one traceable consequence of those commitments

That keeps the design where I think it is strongest: KYA remains a container for trust semantics rather than trying to standardize trust itself, while preventing an executable path from exercising authority that its committed identity did not already name.

1 Like

Clarification: the ZK profile covers two usage patterns, and the text now says so

Re-reading the draft, §5 described ZK-KYA as if every proved scheme were a credential: a hidden fact issuer signs a verdict in advance, and the proof transports it privately. That is what the reference circuit does (hence issuerSetRoot, anchor, issuerPolicy: verifier-only), but it is one usage, not the definition. Three things have been clarified in the PR, interfaces unchanged:

  1. Predicate pattern added (§5.1, informative). The scheme can be the decision rule: a relying party (or a community agreeing an underwriting standard) publishes its rule as a circuit or zkVM program and deploys the verifier; the counterparty evaluates the rule over its own authenticated private data inside the proof and presents only the verdict. No third party issues a credit verdict beforehand, and the relying party is never sent the raw data. The two patterns combine — one circuit can check source-signed data and evaluate the rule over it. The section is explicit about what this does not give you: the protocol does not destroy data or verify that anyone deletes proofs; inputs must be authenticated (storage/receipt proofs against a block hash, zkTLS-style notarised transcripts, or source-signed data, in decreasing order of trustlessness, with anchor committing to the source); and authenticity is not completeness, so a verdict is “satisfies rule P over the named sources, window and coverage”, never an unqualified “is creditworthy”.

  2. Scope of the scheme-binding MUST narrowed. The draft required that “whatever the fact issuer signs” include the schemeId. That is right for a signed object that carries a pre-made verdict — it is what stops a level issued under a lenient scheme being presented under a strict one — but wrong for signed inputs (a bank statement, an exchange export) that carry no verdict and legitimately predate any scheme. The rule now reads: the proof’s statement MUST be bound to the verifier’s schemeId (adapters feed it as a public input); additionally, verdict-carrying credentials MUST embed it. Signed inputs are exempt from the second half; their binding to the rule is the computation itself. This is a semantic clarification with no ABI change, but it does change the applicability of a MUST, so flagging it rather than calling it editorial.

  3. issuer in PROVED mode restated. It identifies the admitting verifier — which verification logic accepted the proof — not the source of the facts and not a guarantor. What stands behind the verifier differs by pattern and is what anchor commits to (attestor set in the credential pattern; block hash / notary set / data-signer set in the predicate pattern). Relying parties pin (verifier, anchor).

Also added to Security Considerations: query composition. A zero-knowledge proof bounds what one answer reveals, not what a sequence reveals (“above 100? above 50? above 75?” recovers a balance from perfectly ZK answers). The prover’s disclosure decision is the scheme’s outputs, parameters and query shape, and the set of schemes it will answer for one counterparty — not the proof system — and an audit of an immutable scheme is a judgement at a point in time, not a permanent one.

What has not changed and is not claimed: the reference circuit still demonstrates only the credential pattern; there is no predicate-pattern circuit yet; and there is no mechanism for a relying party to ship an ad-hoc rule inside a challenge. That last one is noted as a future extension — it needs a binding among request, program, parameters and data scope (a program hash alone is insufficient), and adding a member to KYAChallenge changes its EIP-712 type hash, so it would be a new message type or versioned extension rather than a compatible edit. Views on whether that belongs in this draft or a follow-up are welcome.

@chugarchugarr Thanks, this is a useful and concrete review. Your emphasis on keeping committed semantics aligned with executable behavior fits the intended scope of KYA: a framework for expressing and verifying trust conclusions, rather than a standard that dictates trust itself.

The split between scheme-level binding kind, assertion-level binding witness, and resolution-time binding validity makes sense. I agree that a registry-local check must not be presented as complete policy satisfaction when required binding predicates or other policy conditions have not been evaluated.

The policy-projection and bridge-configuration cases also identify important consistency requirements. For policies, committing the executable rules is only part of the solution: the supported projection and its correspondence to the declared policy must also be defined. Two committed hashes alone do not establish semantic equivalence. Unsupported conditions must not silently disappear from an evaluation that claims to satisfy the full policy.

One point I would refine is the distinction between immutable semantics and explicitly authorized live state. A scheme may intentionally depend on changing revocation status or authenticated chain state under fixed, declared rules. Such state updates should not automatically require a successor scheme. Changing the meaning, authority, or interpretation of those rules is different. Complete satisfaction should require evaluation of all necessary conditions, including declared live dependencies; otherwise the result should be explicitly scoped as partial or registry-local.

For epochs, I agree that replay eligibility must be verifier-validated under the profile’s defined authority, rather than created by an unconstrained prover-selected value. I also agree with explicit chain and registry domain separation for the canonical registry-admission profile. That should remain distinct from ephemeral peer-to-peer predicate presentations, whose request and session context needs its own explicit binding.

The separation between per-action verdicts and subject-trust assertions is consistent with the framework’s scope: they should compose through declared scheme semantics, not become interchangeable merely by encoding a verdict as a level.

The implementation-specific cases need to be checked against the current PR revision before being marked resolved. Your proposed regression cases provide a concrete checklist for that review. The follow-up should distinguish clarified requirements from changes actually implemented and tested.

Checked rev3 against the regression cases. Most of what I raised looks implemented now rather than just clarified. I think there are two boundaries left that are worth checking.

The first is the registry replay boundary. The new schemeId binds the chain and SchemeRegistry, so I think the current test correctly closes replay between two independently deployed SchemeRegistries. What I am having trouble reconciling is that the state which actually consumes the nullifier lives one level lower, inside KYARegistry.

Suppose one SchemeRegistry S has two KYARegistry instances A and B pointing to it. The scheme is the same in both, so the verifier receives the same schemeId. The proof and nullifier can also be identical. But A and B each maintain their own (schemeId, nullifier) consumption map. A proof consumed in A therefore does not appear consumed in B.

That seems to mean the current regression proves SchemeRegistry-domain separation, but not necessarily KYARegistry admission-domain separation. If the intended claim is that one proof can only be admitted once per KYA registry domain, I think the KYARegistry that actually consumes the nullifier has to enter that domain somewhere, unless the architecture makes one KYARegistry per SchemeRegistry an invariant.

The other boundary is the bridge configuration. I agree with committing configHash; I’m less sure it belongs in requestHash.

If C0 produces response 100 and then a legitimate reconfiguration to C1 produces response 0, putting configHash in the request gives us two different ERC-8004 requests, R0 and R1, under the same bridge and kya:S tag. getSummary(agent, [bridge], “kya:S”) then sees both records. In the reference aggregation that can produce count 2 / average 50 even though the current KYA projection is 0.

That seems different from the semantic-versioning case. The KYA scheme did not become a different object. The bridge’s authorized live interpretation changed.

So I think the cleaner boundary may be to keep the request stable as the continuing mirror of (bridge, agent, scheme), and move the configuration commitment into the response provenance:

requestHash = H(REQUEST_TYPE, chainId, identityRegistry, bridge, agentId, schemeId)

responseHash = H(assertionId, level, configHash)

Then validationResponse() can update the same ERC-8004 record as the live interpretation changes, while each response still proves exactly which bridge configuration produced it. That also seems consistent with the distinction you made above: immutable semantics do not require freezing authorized live state, but the consequence of that live state should identify what produced it. Those are the only two cases I still see as open.

Revision 3 is in the PR — what changed for each point of your first review

@chugarchugarr thanks for checking rev3 against the regression cases directly. You have already verified most of this yourself, so this post is mainly the point-by-point record for other readers and for the editors. Your two follow-up boundaries (the registry replay domain, and whether configHash belongs in requestHash) touch points 7 and 4 below. I am not answering them in this post; I want to work both through properly and will come back on them separately.

The invariant you stated — every value or transformation capable of changing a KYA decision must either be committed by the identity of the object being evaluated, or be explicitly declared live/external and excluded from claims of complete policy satisfaction — is now the organising rule for §3, §4, §7 and §8. Here is what changed per point, what was clarified in text only, and what I deliberately did not do. Everything below is implemented and tested (test/kya.test.js 24 cases including the six regression cases R1–R6; test/zk.test.js 15) and redeployed on Sepolia (addresses in the repo README).

1. Binding — kind per scheme, witness per assertion, predicate at resolution. Adopted as you framed it, with the complete evaluation model. binding is now an on-chain field of Scheme (registerScheme takes it; immutable). Each assertion stores a bindingWitness captured at issuance: for controller-bound schemes on erc8004 subjects the registry itself reads ownerOf(agentId) from the identity registry named in subjectData and stores keccak256(abi.encode(owner)); instance uses claimDigest; identity stores nothing. resolve/check (and therefore evaluate) now evaluate the predicate and exclude VIOLATED or UNEVALUABLE assertions, so a non-zero result means status, expiry and binding were evaluated. A new resolveLocal is the registry-local view and is specified as MUST NOT be presented as complete satisfaction; bindingStatus(assertionId) exposes the predicate. Where the registry cannot evaluate a controller binding (foreign-chain subject, other subject types without a resolver) it refuses to record the assertion rather than record one it can never check. R2 reproduces your t0/t1/t2 case: after the transfer, check and evaluate are false while resolveLocal still returns the assertion; a re-attestation to the new controller carries the new witness and satisfies again. Subject-type resolvers beyond erc8004 are left modular for a follow-up.

2. Epoch — MUST on authority, mechanism profile-defined. Text change only: “the prover MUST NOT control the epoch that determines replay eligibility; the verifier MUST derive it from system state” is the invariant; floor(block.timestamp / epochSeconds) with [current − grace, current] remains normative for the scheme-epoch profile; future profiles may derive it differently but the authority rule is not descriptor-optional.

3. Policy identity commits the executable projection. policyId = keccak256(abi.encode(owner, policyHash, rulesHash, nonce)) with rulesHash = keccak256(abi.encode(Rule[] allOf)) (0x0 when no projection). The supported projection is defined exactly — top-level allOf over ordered-level schemes with minLevel and non-empty issuers — and registerPolicyWithRules rejects empty rule sets and empty issuer lists; nothing else is expressible through the interface. The descriptor schema gained onchain: {evaluator, rulesHash, sufficient} so a document that also contains anyOf, categorical equality or anchors must declare sufficient: false. One refinement to your framing that I kept explicit in the text: two committed hashes do not by themselves establish semantic equivalence — a strict document can still be paired with a lenient projection under a fresh id — so the spec states that the correspondence is the descriptor author’s claim, verifiable by comparing the document’s top-level allOf with getRules, and that unsupported conditions must not silently disappear from an evaluation claiming full satisfaction. R1 covers the id separation.

4. Bridge configuration identity. configHash = keccak256(abi.encode(schemeId, trustedIssuers, responseMap)); it enters requestHash (… bridge, configHash, agentId, schemeId) and responseHash = keccak256(abi.encode(assertionId, level, configHash)). Reconfiguring yields a new configHash, hence a new requestHash; a request filed under the old configuration can no longer be synced and its recorded response stays exactly as committed. sync uses complete resolution, so a binding-violated assertion drives the mirror to responseMap[0] on the next sync. R3 covers both the map change and the issuer-set change. This describes rev3 as it stands; your follow-up on requestHash is open (see top).

5. Tag. Full schemeId ("kya:" + 64 hex, 68 bytes). R5 constructs two ids sharing the old 32-bit prefix and checks the tags differ.

6. Verifier immutability — with one refinement. Normative now: every code path, verification key, pinned issuer set or other dependency capable of changing proof admission MUST be semantically immutable for the life of a schemeId; upgradeable proxies and verifiers reading admission parameters from undeclared mutable storage MUST NOT be registered. Mechanically, the registry pins the verifier’s EXTCODEHASH at registration and refuses admission when the code at the address differs; the text is honest that this catches redeployment behind an address but not delegation, which is why the normative rule exists in addition. The refinement: I did not write “all state capable of changing acceptance must be frozen”. A scheme may intentionally depend on state that changes under rules it declares — a revocation source, an issuer set with a declared rotation mechanism, authenticated chain state — and such updates are part of the scheme’s meaning, not a change of it. What must not change under one schemeId is the meaning, the authority or the interpretation of those rules. Complete satisfaction then requires evaluating those declared live dependencies; otherwise the result is scoped as partial. R4 checks that a changed code hash is refused and documents the proxy limit.

7. Chain + registry domain separation — MUST for registry admission, by construction. schemeId = keccak256(abi.encode(chainId, registry, controller, schemeHash, nonce)). Since adapters already feed schemeId into the circuit as a public input, a proof for one registry’s scheme is not valid for another’s even with identical controller, descriptor and verifier, and nullifier scopes are disjoint by design rather than by accident — no circuit change was needed. The text scopes this to registry admission; ephemeral peer-to-peer predicate presentations are bound to a request, not a registry, and portability across registries, if ever wanted, is an explicit profile. R6 (in-process) and a real-circuit case in the ZK suite cover it. This describes rev3 as it stands; your follow-up on the SchemeRegistry / KYARegistry boundary is open (see top).

8. Per-action judgment. Agreed; unchanged: verdicts compose as evidence for schemes, they do not become durable subject-trust state by being encoded as a level.

Interface ids changed accordingly (IKYASchemeRegistry 0x50cb46d8, IKYARegistry 0x9cdd4647, IKYAPolicyEvaluator 0x173ff2b6; IKYAPolicyRegistry and IKYAVerifier unchanged). What remains open: the two boundaries from your follow-up, which get their own reply; and from your original list, resolvers for non-erc8004 subject types, and whether complete evaluation should be extensible to declared live dependencies beyond binding (revocation sources) through the same predicate mechanism. The latter two are follow-ups rather than v1 blockers, but I would take a view on the second.

1 Like

@chugarchugarr both boundaries are closed in revision 4 (PR updated in place, repo in sync, Sepolia redeployed — addresses in the repo README). Thank you for running the cases rather than reading the summary; both held exactly as you described them. The second one is adopted as you wrote it. For the first one I took a different shape than “fold the admitting registry into schemeId”, and I want to say why, because it is a design choice rather than the only fix.

Registry replay domain. You were right: rev3 bound the proof to the SchemeRegistry namespace while the nullifier is consumed one level lower, and nothing made one KYARegistry per SchemeRegistry an invariant. Putting the admitting KYARegistry into schemeId would close that with no circuit change, but it turns a scheme into “a rule for one registry”: two markets that want the same underwriting rule must register two schemes, and — since a credential in the credential pattern signs the schemeId — attestors must issue one credential per registry. That gives up the property the framework exists for. So rev4 keeps schemeId as the identity of the rule and gives the proof an explicit second context:

admissionDomain = keccak256(abi.encode(keccak256("erc-kya-registry-admission-v1"), chainId, schemeRegistry, kyaRegistry))

The admitting KYARegistry computes this from its own state (there is no calldata parameter for it) and passes it to IKYAVerifier.verify(schemeId, admissionDomain, publicInputs, proof). The reference adapter feeds it as public signals (13 → 15) and the reference circuit derives the nullifier from it: Poseidon(secret, schemeId, admissionDomain, epoch). The credential the attestor signs is unchanged and names no registry.

Two consequences, both intended and both stated in the text: a proof made for A does not verify at B — whether B shares A’s SchemeRegistry or not — and B cannot make it verify; and the same credential can be re-proved for B with a B-scoped nullifier. This is per-proof domain separation, not “one credential, one use anywhere”; a use case needing global single-use needs shared consumption state and its own profile, and the spec says so rather than letting it be inferred. Ephemeral peer-to-peer presentations use a request-bound domain instead of a registry’s.

What the regression covers, with the real Groth16 circuit end to end rather than a mock: one SchemeRegistry with two KYARegistries A and B; A’s admitted proof refused at B; a fresh proof made for B refused at A before anything has been consumed anywhere (so the refusal is the domain, not a nullifier); repeat at B refused by the nullifier rule, with a re-randomised proof; the adapter accepting the same proof under B’s domain and rejecting it under A’s; and the same credential re-proved for A admitted with a different nullifier. The rev3 two-SchemeRegistry case is kept alongside. The cost is honest: this is a public-input change, so circuit, verification key, verifier contract and vectors moved to circuit revision 3 rather than being patched, and IKYAVerifier / IKYARegistry have new interface ids (IKYASchemeRegistry is back to its rev3 id, since the rev4 draft’s registry parameter is gone).

Bridge configuration. Adopted as you wrote it:

requestHash = H(REQUEST_TYPE, chainId, identityRegistry, bridge, agentId, schemeId)
responseHash = H(assertionId, level, configHash)

The request is the continuing mirror of (bridge, agent, scheme) and its meaning is now written down as that — “keep this mirror current, naming the configuration in each response” — not a one-time judgment under a frozen configuration; the committed payload is the canonical ABI encoding, with the JSON at requestURI as a readable copy. A reconfiguration updates the same ERC-8004 record on the next sync; each response still proves which configuration produced it; configuration events, queries and the Synced event keep the commitment. The regression asserts the actual summary, not just the hashes: your 100 → 0 case reads count 1 / average 0 under getSummary(agent, [bridge], "kya:S"), requestHash unchanged, responseHash changed.

Two boundaries this leaves, stated in §8 rather than hidden: between a reconfiguration and the next sync the mirror shows the previous configuration’s result (the test checks that too); and the one-record guarantee is per bridge — a superseded bridge’s records stay in the Validation Registry under the same tag and are excluded by filtering on the current bridge, which is the query path the spec prescribes, and included only if a client lists both. The Sepolia demo is a fresh deployment for exactly that reason; the earlier ones are marked superseded in the repo.

If you see anything else open after this, I would rather hear it before a number is assigned.

1 Like

The draft has been assigned ERC-8419 (thank you @abcoathup). The PR now carries ERCS/erc-8419.md and assets/erc-8419/; the content is revision 4, unchanged apart from the number and the links that contain it.

One note for anyone verifying the Sepolia examples: the demo scheme descriptors were registered before the number was assigned, so their type URI still reads eip-9999 and their on-chain hashes are unchanged. The repo README says so.