ERC-8107: Using ENS as a Trust Layer for Autonomous Agents

Thank you. That settles the 17.

I agree with your reading of all of them. The twelve capped lines and the two simulated gaps are not differences: the library only claims to be applied together withverifyPath, and it fails closed when the input is incomplete. 142 agree once those are counted that way.

The three liveness lines (20–22) are the third of my options: the text should say it more clearly, in two places.

  1. The library header. It says the library checks registration, not control, and that it is applied in addition to verifyPath. It does not say that the coordinator must also apply the liveness check for a wrapped trustor, which the library does not perform. Read on its own, it suggests that verifyPath plus the library is the whole check. It is not, and your lines 20–22 show exactly what is missing.

  2. The scope of the liveness check. Under Wrapped Name Lifecycle, the continuity bullet says it applies to every attestation, whenever it was issued. The liveness bullet has no equivalent sentence, so read literally, it covers only endorsements issued after the trustor’s registrar expiry. My earlier reply relied on it for the case with no renewal, and you applied it the same way. The text should say so.

#2096 adds that sentence to the liveness bullet and the missing clause to the library header. The library still does not read control, and its verdicts are unchanged.

After #2096, verifyPath with enforceExpiry set, the liveness check, and the library together should give your 157 verdicts: 155 on complete input, plus 2 that are Unknown where you simulate the gap.

Thank @kbryan for #2096 and for reading the replay so closely. I replayed our 157 lines against master at 3c0f7076, with #2096 merged, at the same block (26,192,453) and the same ENS v1 setup.

With verifyPath and enforceExpiry, the liveness check applied to every attestation of a wrapped trustor, and the library together, I get 155 agreements on complete input, 2 Unknown, and 0 disagreements. The library alone still gives 140 agreements and 17 disagreements, before and after #2096, since #2096 only changes a comment in the library. With the liveness check switched off, the three liveness lines come back as disagreements.

Two limits: this is one block on ENS v1, and agreement shows the combination reproduces our rule, not that our rule is right. The two Unknown depend on how the two input gaps are represented.

Thanks for the rerun and for the care you’ve taken with every round of testing. It’s greatly appreciated.

Your caveats are fair. Agreement at a single block, against ENSv1, shows that the combined checks reproduce your rule, not that either rule is necessarily correct. The two Unknown results also depend on how each simulated gap is presented. I’d rather state those limits clearly than overclaim.

Your three liveness cases prompted one further clarification. A wrapped trustor can lose its controller between the coordinator’s check and execution, for example, when an emancipated subname reaches its NameWrapper expiry during that window.

The on-chain re-evaluation only checks control for the terminal node, so it would miss that change. Unlike continuity, liveness depends on current state, so the coordinator can repeat the liveness check within the executing transaction.

#2098 documents this with one informative paragraph. No normative requirements have changed.

ERC-8107 is now proposed for Last Call in #2099, with a deadline of 5 November 2026. The text under review is master at commit 6b7a48ff.

If you find anything else before then, whether from another block or a different test setup, please share feedback here or on Ethereum Magicians.

You asked here for feedback “whether from another block or a different test setup”. I ran independent tests of two cases at 6b7a48ff.

Aliases: with agent.eth and backup.eth endorsed by G and resolving to the same address X, denying agent.eth at universal scope rejects X through [G, agent.eth] and still admits it through [G, backup.eth]. Denying both names refuses X, and clearing the denial of agent.eth admits X again through agent.eth. The first result is what the Address Aliases section on master describes.

Wrapped trustor: a wrapped trustor in the middle of the path that loses control between the decision and the execution is rejected by an execution-time liveness check, and accepted when I switch that check off. This is the case of #2098. I did not test a wrapped second-level trustor that loses its holder during the window.

Limits: one block (mainnet fork, 26,192,453), ENSv1 only, and one reading of the text that is my own. I replayed 157 lines against 6b7a48ff and get 155 agreements on complete input, 2 Unknown where my simulated input is incomplete, 0 disagreements. These are counts for a combined rule (verifyPath with enforceExpiry, the liveness check, and the reference continuity check); the reference continuity check alone gives 140 agreements and 17 differences. The combined rule was assembled to cover those 17 differences, so the counts show that it reproduces my reading, not that the reading or the text is correct. The three reference files these 157 lines run on (TrustRegistry.sol, ITrustRegistry.sol and RegistrationBoundary.sol) are byte for byte the same on master as of 31 October, so #2100 and #2101 change none of the 157 lines.

One open question: #2099 changes only the header of 6b7a48ff, but master now also has #2100 and #2101. Is the text under Last Call review 6b7a48ff as announced, or master with those two changes?

I posted a version of this on ethereum-magicians on 1 November; it is waiting for moderator approval.

The tests are here (CC0, with the commands to replay them at 6b7a48ff; they need a mainnet archive node that reads block 26192453): https://github.com/RealSpap/authority-risk-oracle-showcase/tree/main/research/erc-8107-coordinator-tests

Thank you for running both cases independently and for publishing the replay.

Text under Last Call review

The review targets master. PR #2099 changes only the preamble, so once merged, the Last Call text will be master plus that header.

The current master is 831b1c15, which includes 6b7a48ff plus #2100 and #2101. The original #2099 description referenced 6b7a48ff because those two PRs merged after #2099 was opened. I’ve since rebased #2099 onto master and updated its description.

Neither PR changes normative text or the reference contracts:

  • #2100 adds the informative Address Aliases subsection, a cross-reference under Key Compromise, and one test.

  • #2101 adds tests/continuity/ReferenceCoordinator.sol, its fork test, and the listing entries.

  • Neither introduces a MUST, SHOULD, or MAY.

TrustRegistry.sol, ITrustRegistry.sol, and RegistrationBoundary.sol are identical between 6b7a48ff and master, as you found. Your 157 test cases therefore apply unchanged.

Address aliases

Your result matches the Address Aliases section. Denial applies per node. An address is excluded only when every name resolving to it is denied. Clearing the denial of one name readmits the address through that name.

Wrapped trustor liveness

Your result matches #2098.

The remaining case is a wrapped second-level trustor that loses its holder during the execution window. This is covered by test_Wrapped2LDTrustorPastWrapperExpiry in ReferenceCoordinator.fork.t.sol (#2101).

The test runs at block 26,194,700. The trustor is evaluated during its grace period and passes its NameWrapper expiry before execution. No re-registration occurs, so the pinned expiry condition still holds. Only the liveness check at execution rejects it.

If your harness reaches the same verdict at block 26,192,453, that would provide an additional independent check.

Scope and limitations

I agree with your caveats:

  • Testing covers one block.

  • ENSv1 only; ENSv2 is explicitly out of scope for this version.

  • Agreement establishes that your independent implementation reproduces the interpretation, not that the specification itself is necessarily correct.

Last Call deadline

Since #2099 has not yet merged, I’ve moved the proposed Last Call deadline from 5 November to 17 November 2026.

Thanks for the pointer to master at 831b1c15. Yes, my harness reaches the same verdict at block 26,192,453. A wrapped .eth second-level trustor, registered for one year, sits in the middle of a three-node path. Decided at 30 days after its expiry E, in its grace period, the decision accepts. At E plus 90 days and one second, the NameWrapper ownerOf returns zero, the registrar’s nameExpires still returns E, and the registry’s evaluation still accepts the address. The execution is refused, and only by the liveness check at execution: with that one check switched off it accepts wrongly, while switching off the path verification (verifyPath), the continuity check, the liveness check at decision or the expiry check changes nothing.

I also ran the fork tests of #2101 at that block, changing only the pinned block from 26,194,700 to 26,192,453: all 6 tests pass, including test_Wrapped2LDTrustorPastWrapperExpiry. My scenario gave the same lines on two public nodes, Tenderly and drpc, with the storage cache switched off. The #2101 tests ran with a local cache of that block’s state, so I do not count that run as read on two nodes. Limits: one block, ENS v1 only. The coordinator in my harness is my own, written from the text at 6b7a48ff, so the agreement of the two coordinators does not show that either is right for every path. My scenario was written from the description of the case, not copied from your test, and at execution it checks validateParticipantAddress, not validateParticipantWithPath. I have not published my scenario; I can add it to my own test repository, not to ethereum/ERCs, if it is useful.

Thank you, that closes the case.

Your harness and the reference coordinator in #2101 reach the same verdict at block 26,192,453:

  • The decision accepts during the grace period.

  • The registry and pinned expiry checks still pass at E + 90 days + 1 second.

  • Only the execution-time liveness check rejects the path.

It’s useful that your harness uses validateParticipantAddress while the reference test uses validateParticipantWithPath. The result holds under both coordinator re-evaluation methods.

The ablation confirms the expected behaviour: disabling only the execution-time liveness check causes the case to be accepted.

I agree with the limitations. Agreement between two coordinators does not establish correctness for every path, and the replay covers only one block and ENSv1.

Please add the scenario to your repository. The ERC already includes its reference test, so I’d prefer to link to your implementation from the discussion as an independent replay rather than duplicate it in ethereum/ERCs.

@kbryan, thank you, truly. It was a pleasure to work on this with you, and I am glad the replay closes the case.

The scenario is now in my repository, as you asked. test/Wrapped2LDGrace.t.sol holds it as S7, with S7b, which records what validateParticipantAddress and validateParticipantWithPath each return once the wrapper has expired (both accept), and S7c, which decides at the last second of the grace period and executes one second later (the execution refuses). bash jouer-s7.sh replays it, and the raw outputs of two nodes at block 26,192,453 are in the sorties folder; the README gives the block hash.

The README says what it shows: the same verdict as the reference coordinator of PR #2101 on this case, and only the liveness check at execution refuses.

Thanks again. I’m very grateful for your help with this. I truly couldn’t have gotten this far without your thorough reviews, technical rigour, and knowledge.