ERC-8415: Asynchronous Register Projection for NFTs

I’ve been working on a draft called Asynchronous Register Projection for NFTs and wanted to put it up for discussion. No ERC number assigned yet — I’d like to get the scope and semantics right before pushing a PR. Would appreciate any feedback, especially on the historical-query behavior and how (or whether) this overlaps with existing standards.

Problem

An ERC-721 can trade faster than an external register updates. ownerOf(tokenId) tells you who holds the token now; it doesn’t tell you who the register had confirmed at a particular past instant.

The extension keeps those two views separate. Normal transfers proceed while a register update is still pending. The projection is just a time-indexed history of confirmed holders.

Proposed model

Each entry records a holder address, effective time, record commitment, previous commitment, registry reference and version. Versions are consecutive, effective times strictly increase, and a commitment can’t be reused within a token. An entry’s interval runs up to (but not including) the next entry’s effective time.

For a token with at least one entry, any instant at or after the first entry resolves to one recorded holder. Query before the first entry and it reverts. The result may still be provisional.

The core interface, IRegisterProjection, exposes:

  • currentEntry, entryAt, entryCount for walking the history
  • entryAsOf, holderAsOf for point-in-time lookups
  • isFinalAsOf — whether a later admission could still change that answer
  • registerId to identify the projected register

ERC-165 ID: 0x6309e170.

A concrete example

Say the first entry confirms Alice from time 100. Bob buys the token at 120. The register records Bob effective at 130, but the entry doesn’t land on chain until 150.

Before admission, ownerOf may already return Bob while holderAsOf(140) returns Alice provisionally. After admission, holderAsOf(140) returns Bob. Everything from 100 up to (but excluding) 130 is now final; 130 onward stays provisional until a later entry closes the interval.

A later entry can confirm the same holder. This matters: it lets historical instants become final even when the holder didn’t change.

Optional settlement interface

IProjectionSettlement (ERC-165 ID 0xf4a7d71b) adds a discoverable authority, a bounded settlement deadline, at most one open projection gap per token, and proof-verified admission.

Opening a gap does not freeze ERC-721 transfers. Settlement authority is separate from token ownership. Anyone can relay a proof; submitting one grants no special rights.

An admission checks that the remote state is finalized and the proof verifies under the declared profile. It binds the local chain and contract, token and settlement IDs, holder, snapshot, prior and next commitments, version and effective time. Proof consumption, remote-height advancement, entry admission and gap closure are atomic.

Three questions stay distinct:

  1. Who was recorded at instant t? → holderAsOf
  2. Can a later admission change that? → isFinalAsOf
  3. Is a change in progress for that instant? → openGapOf, openedAt

Cancellation closes a gap but doesn’t make provisional history final.

Scope and trust assumptions

The proposal standardizes the projection and its query semantics. It doesn’t move tokens across chains, decide the legal effect of registration, or prescribe an app’s entitlement rules.

A proof establishes inclusion in accepted remote state, not the truth of the underlying register. You’re still relying on the registrar, the consensus or attestation model, the verification profile, and the authority’s policy. If the register stops issuing entries, recent instants can stay non-final indefinitely. Repeated supersession is a liveness concern too.

The full register stays off-chain, but holder addresses, commitments, references, times and proof material are public — commitments alone don’t give you privacy.

Related work and questions

ERC-5805/6372 are relevant to historical checkpoint queries; the difference here is a source whose effective times can precede on-chain admission, so some historical answers are provisional. ERC-7965/7786 cover proof delivery and verification. Happy to be corrected, and I’d like examples of where this should compose with existing interfaces.

A few things I’m unsure about:

  1. Is the distinction between historical resolution, finality and an open gap actually useful and clear to integrators?
  2. Is a strictly increasing effective-time model practical for registers that may report corrections or multiple changes in one second? Should a profile reject those, or aggregate them?
  3. Should projection-only conformance specify its admission guarantees explicitly, independently of the optional settlement interface?
  4. Which limits on supersession and future effective times should be mandatory rather than profile recommendations?
  5. Are confirming entries an adequate liveness mechanism for apps waiting on historical finality?
  6. Does an existing standard already cover this combination well enough that an extension would be preferable?

A Solidity reference implementation and a local EVM invariant suite are ready. This is an early technical discussion, not an assigned ERC or adopted standard. I’ll add the public spec/PR link here when it’s available.

2 Likes

If I bought one of these NFTs, I’d want the wallet to clearly explain what “registration pending” means for me. Do I simply wait, or is there something I need to do? Showing who to contact if the update takes too long would also be helpful.

1 Like

Thanks Anzus — you’ve put your finger on exactly how RWA works in practice, and it clarifies the wallet UX considerably. Three points:

“Registration pending” means: the previous transfer hasn’t yet been recorded by the registry. You simply wait. It’s not a failure — it’s the normal chain of registration catching up.

Why waiting is unavoidable: every RWA token represents a real-world right (a house, for example). On-chain, the token can change hands every few minutes — a → b → c → … → n — but the registry must record each hop one at a time: first a→b, then b→c, then c→d… Some take a day, some take several. This serial, recursive registration is physical — no standard can eliminate it. Blocking is inevitable, and that’s by design.

What if it takes too long? If the registry exceeds the agreed commitment time, that’s a signal the previous transfer may itself have an issue — and you may be entitled to cancel the trade under the trade terms. This is not the standard’s concern; it belongs in the transaction agreement.

ERC-8415’s role is strictly bounded: it does not restrict on-chain trading, and it does not absorb recursive registration risk. Whether a buyer is willing to accept that risk is their own decision, reflected in the trade terms — same as any other settlement risk.

So the wallet’s job is just to surface the real status clearly — “previous transfer still registering; you may need to wait, or check your trade terms if the commitment window passes” — and never block the token.

Happy to add a non-normative wallet note to that effect. Thanks again.

Michael

To add on my earlier reply: one more thing worth being explicit about —

The reason 8415 keeps on-chain transferability fully decoupled from off-chain registration isn’t an oversight; it’s the core design. The token must remain freely tradable even when the registry is still processing a prior hop. Blocking the token would mean the protocol is implicitly “waiting on the registry” — which is exactly the coupling we want to avoid.

So for wallet UX, the job is really just to surface the distinction clearly:

  • ownerOf → tradable position (always live)

  • projectionStatus → registry confirmation (may lag)

“Registration pending” is therefore not an error state — it’s the normal asynchronous gap. The wallet simply shows it as such, and never blocks the user.

Does that match how you’d expect GemWallet to handle it? Curious whether a subscribe-to-ProjectionUpdated model would fit your adapter better than polling.

Michael

Thanks, that helps. Showing “registration pending” separately from a failed transaction makes sense to me, with guidance on when to check the trade terms if it takes longer than expected.

I’m coming from the user and product side, so I can’t speak to Gem Wallet’s implementation or the best way to fetch those updates. My feedback here is about what would be clear to users.

Thanks for bringing this up and opening up this discussion! It’s great to see people digging into how this actually works at the application level.

To clarify the protocol boundaries here: ERC-8415 operates strictly as a registration and projection standard—it deliberately leaves trading and escrow execution mechanics out of the core protocol spec. The settlement logic you’re highlighting naturally lives at the wallet and application layer, serving as a client-side state machine that mirrors ERC-8415’s projection states.

Here is how client interfaces and wallet abstractions can enforce this asynchronous window cleanly:

  • Decoupling Transfer from Economic Settlement: During the pending window—even after the asset token moves out of the seller’s wallet—all contingent proceeds (trade consideration like ETH/USDC, accrued dividends, yield) remain quarantined in a client-side escrow vault. Economic finality stays tied to off-chain registry confirmation.

  • Alignment with Real-World Legal Mechanics: This matches standard legal title perfection. In traditional real estate or equity transfers, beneficial title and income rights remain with the record holder until official registry update. The wallet layer isn’t introducing novel logic; it’s enforcing existing legal precedence on-chain.

  • Deterministic Unwinds on Rejection: Quarantining both sides of the trade during the pending state guarantees zero double-spend risk if registration fails. A failed off-chain entry simply triggers a deterministic revert: consideration flows back to the buyer, and asset title returns to the seller.

State Machine Alignment

The wallet interface acts as a direct client-side mirror of the core ERC-8415 projection state machine:

  • Pending → Locked: Asset token transfers on-chain, but trade consideration and economic proceeds are quarantined in client escrow while awaiting off-chain processing.

  • Confirmed → Released: The off-chain registry confirms state; consideration releases to the seller, and beneficial title fully settles to the buyer.

  • Rejected → Refunded: The off-chain registry denies entry; escrow executes a reverse unwind (asset token reverts to seller, ETH/USDC returns to buyer).

TL;DR: App and wallet builders don’t need to invent custom clearing logic for off-chain asset registration. By treating pending ERC-8415 states as unsettled, client interfaces can quarantine trade proceeds and yield until off-chain finality triggers a deterministic release or clean refund.

1 Like

Thanks for pushing on this — worth pinning down before it drifts further into the thread’s own shorthand.

Terminology clarification

A few replies above (mine included) used “Pending / Confirmed / Rejected” as shorthand. To be precise: these are not states defined by the protocol. ERC-8415 doesn’t have a three-state lifecycle or a “Rejected” event. The actual vocabulary is:

  • open gap / closed gap — whether a settlement gap is currently outstanding (openGapOf, openedAt)

  • provisional / final — whether a historical answer at a given instant could still change (isFinalAsOf)

  • admitted — an entry has been accepted via a verified proof (entryAt, currentEntry)

There is no on-chain “veto” or “rejection” event, because a proof that fails verification is never submitted successfully — it simply never becomes an admitted entry. Cancellation closes an open gap, but as noted upthread, closing a gap does not make provisional history final. So “Rejected” as used in casual conversation here maps loosely to “gap cancelled, prior history stays provisional” — not to a discrete on-chain outcome the protocol emits.

This matters for @babyblueviper1’s question about a late veto hitting after downstream state has built on the optimistic transfer: the protocol has no mechanism for a registrar to retroactively invalidate an already-admitted entry. Admission is final once it happens. What can happen is a gap staying open indefinitely (liveness issue, not a veto) or being cancelled before admission (in which case nothing was ever finalized for downstream state to have safely relied on). If an application let downstream state depend on a still-open, non-final projection, that’s the application choosing to build on provisional data — which is exactly the risk the next section is meant to make it easy to avoid.

Application-layer reference pattern (non-normative)

Since a few people have asked how a wallet or trading app should actually handle the async window, here’s a reference pattern — not part of the protocol, just a suggested composition:

  • Quarantine consideration during an open gap. While openGapOf(tokenId) is true, hold trade proceeds (ETH/USDC, accrued yield, etc.) in a client-side or application-level escrow rather than releasing them to the seller. The token itself keeps trading freely — only the economic settlement is held back.

  • Release on admission. Once the corresponding entry is admitted (gap closes with a matching entry), release consideration to the seller and treat beneficial title as settled.

  • Refund on gap cancellation without admission. If a gap is cancelled and no entry lands, reverse the escrow: consideration back to the buyer.

This mirrors how legal title perfection already works off-chain (beneficial rights typically stay with the record holder until registry update) — the wallet layer isn’t inventing new legal logic, just mirroring it client-side.

To be explicit about scope: this pattern is optional. Applications that instead allow secondary composition on open-gap (non-final) state — lending against it, re-trading it — are accepting that risk themselves. ERC-8415 guarantees the projection is auditable and queryable; it does not guarantee, and does not attempt to guarantee, that every possible downstream use of a non-final state can be safely unwound. That boundary is intentional, for the same reason the spec supports both the HKEX-style netting model and the sequential chain-of-title model — a single mandated unwind rule would break one of the two.

Happy to fold the vocabulary table above into a non-normative section of the spec itself if that’s useful, rather than leaving it scattered across replies.

1 Like

Q5 is worth pressure-testing against a structurally similar problem: verdict finality in a signed-attestation system. A confirming entry existing isn’t, on its own, proof of liveness – a registrar that’s silently stopped emitting entries looks identical, from the consumer’s vantage point, to one that’s still working through a normal backlog. What we run instead is a strongest-surviving-anchor hierarchy (Bitcoin PoW anchor, trust-maximal > independent relay copy, tightest time > the registrar’s own committedAt field > a documented survivor floor when nothing else is available), because a single confirming-entry timestamp actually conflates two different questions: precedence (did this exist no later than time T – frozen once anchored, never re-checked) and freshness (is this specific claim still good right now – re-derivable on demand, not just asserted).

Applied to 8415: an open gap plausibly needs both – a precedence proof that the registration request itself existed by some time (so a later competing claim can’t quietly rewrite history), and a freshness/liveness check that’s independently re-derivable rather than trusting “the registrar hasn’t emitted anything new” as evidence of anything in particular. Might be relevant to #3 too (projection-only conformance’s own admission guarantees) if it’s useful to compare notes on how the hierarchy composes.

1 Like

Hey. How would the deterministic unwind work if the NFT has already been transferred again?

Suppose user1 sells to user2 and user2 sells to user3 before the registry confirms the first transfer. The registry then rejects user1 → user 2.

Refunding user 2 from escrow seems possible. But returning the NFT to user 1 would require some control over user 3’s token. What mechanism would make that possible while keeping transfers unrestricted?

Would the application need an explicit clawback mechanism or restrictions on onward transfers? Or should the unwind guarantee be limited to applications that impose those additional rules?

On Commercial Contract Chains and Why the Solution Belongs in the Wallet, Not a Complex Spec:

You’ve hit on the exact boundary between commercial contract law and protocol design.

When we look at onward transfers (User 1 → User 2 → User 3), the relationship isn’t a technical paradox—it directly mirrors off-chain derivative title transfers:

1. Commercial Dependency naturally handles the sequence User 2 → User 3 is functionally a conditional continuation of User 1 → User 2.

  • If the registry fails User 1 → User 2, then User 2 never received perfection of title.

  • As a result, User 2 → User 3 fails downstream as a matter of legal consequence.

  • The unwinding of economic consideration naturally cascades backward step-by-step (User 3 recovers funds from User 2; User 2 recovers funds from User 1).

2. ERC-8415 remains minimal: It projects state, it doesn’t enforce law Attempting to solve this by making ERC-8415 more complex—such as adding recursive contract-level clawbacks or restrictions on transfer()—is an anti-pattern. It would destroy token composability and force rigid legal assumptions into an EVM standard.

ERC-8415 simply acts as an auditable state engine. It deterministically projects whether a historical hop is Provisional or Final.

3. The real integration target: Adapting Wallet UX The correct path to support this asynchronous reality is adapting the wallet/client layer, not over-engineering the protocol spec:

  • Expose clear state separation: Wallets must natively render the difference between ownerOf (live secondary position) and projectionStatus (registry finality).

  • Informed User Context: When User 3 attempts to acquire a token with an open settlement gap (isFinalAsOf == false), the wallet surfaces this transparently to the user (e.g., “Registry pending from prior transfer; economic consideration will be held in escrow”).

In short: Commercial logic unrolls sequentially off-chain, while wallets provide transparency on-chain. ERC-8415 stays minimal, clean, and non-blocking.

1 Like

That’s a clean resolution — deliberately choosing safety over liveness at the protocol layer, then pushing freshness verification to the application/watchtower layer via a plain SLA-window check, avoids exactly the trap of importing a heavy cross-chain anchor into a lightweight token interface. Precedence via EVM state + block ordering is the right call here since you don’t need cross-domain agreement on when something happened, only that this chain’s own history can’t be rewritten — that’s a strictly easier problem than what we’re solving (our anchor hierarchy exists because a second system, the registrar’s own timestamp, needs an independent check against manipulation, which EVM-native precedence doesn’t need to worry about).

On the #3/projection-only-conformance connection from the other thread — same shape, confirmed: your “admission is final, no on-chain veto, a gap either closes before admission or stays open” is functionally the same posture as our own “precedence is frozen once anchored, never re-checked” — both refuse to let a later event retroactively rewrite an already-settled fact, and both explicitly leave freshness/liveness monitoring to a layer built for it rather than the base layer.

Concrete question rather than a vague offer: you mentioned a non-normative client-side escrow/watchtower reference pattern in the cross-post on the other thread. Would a worked example of that watchtower emitting an independently re-checkable “still fresh as of block N” attestation (rather than just a local flag a dApp trusts silently) be a useful artifact to sketch, or is that already further along than it sounds? Asking because it’s the exact freshness-recomputability boundary our own layer sits on for a different domain, and I’d genuinely like to see whether the same shape holds up here or breaks on something 8415-specific.

Thank you for the thoughtful synthesis, and for grounding your question in the framework of signed attestation systems—it clarifies the core design assumptions.

To answer your specific question directly: yes, a worked example of an independently re-checkable “still fresh as of block N” attestation would be an excellent and highly useful artifact. A few reasons why.

  1. Layered Trust Composition

This fits cleanly into the layered architecture we have been discussing. ERC-8415’s base layer provides deterministic auditability of every state transition across the async boundary (Invariant 3). Crucially, it deliberately does not establish trust in the registrar’s claims—it records what the registrar claimed, and when.

Your watchtower / signed-attestation proposal fills exactly the gap that is left open: it supplies a verifiable freshness proof, so that an application can distinguish between “the registrar is actively operating” and “it has gone silently dead.”

A note on terminology: freshness, in this sense, is a safety property—a stale attestation must not be mistaken for a current one. It is not liveness. Liveness, here, is the separate claim that “the registrar eventually confirms or rejects” (termination / progression). Keeping these two separate matters, and I will be careful about that distinction in the annex.

  1. Non-Normative Reference Pattern

We referenced a client-side escrow / watchtower reference pattern in the cross-post on the other thread. A concrete worked example is the natural expansion of that thought: it shows how dApps or Escrow contracts can compose two orthogonal proof concerns—base-layer finality versus attestation-layer freshness—according to the asset’s risk profile.

It belongs in an informational annex or a Security Considerations section, non-normative to the core state machine. It is not part of the mandatory projection interface, and it does not alter the Base 8415 state machine. That separation is intentional.

  1. Suggested Scope (this is the part I would most like your sketch to anchor)

To keep the artifact small and usable, I would suggest it cover exactly three cases, all expressed against the existing Base 8415 interface (holderAsOf / isFinalAsOf / openGapOf):

(a) Key rotation at block N. The registrar may rotate signing keys. A verifier checking “fresh as of block N” must validate against the key that was authoritative at block N, not the registrar’s current key. A confirmation signed under an old-but-then-valid key remains valid for states at the corresponding height. We never rewrite history; we only advance the active key.

(b) Re-verification of the signed sequence. The attestation should be independently re-checkable: recover the signature against the correct historical key, and require the sequence number to be monotonic (no gaps, no rewinds). This is what makes the freshness claim externally auditable rather than a claim taken on trust.

(c) The stale-attestation failure mode. When signedAtBlock is too far behind the current block (the exact threshold being an asset-specific risk parameter—small for a money-market collateral, days for a land-registry token), the attestation is stale. The correct response is to quarantine economic value, not to release it. This is precisely the “compose by risk profile” idea: the escrow contract decides its tolerance, and the proof layer supplies the fact on which that decision is made.

Three outcomes should be cleanly separated, and this is where I think the annex is most valuable:

  • Fresh AND Final (isFinalAsOf = true) → release consideration to seller, title to buyer.
  • Fresh but Pending (isFinalAsOf = false) → quarantine and wait for the registrar.
  • Stale (whatever isFinalAsOf says) → quarantine on the stale-attestation path, distinct from “still pending.”

That last distinction matters. Many broken designs conflate “stale” with “pending,” which causes a silently dead registrar to look like ordinary delay. Separating them is, I think, the whole point of adding this layer.

What the sketch should deliberately NOT claim

Two boundaries are worth stating up front, because they protect the proposal from overreach:

  • It does not prove that the registrar cannot sign a false confirmation. That requires a separate trust assumption (registrar key authority, a dispute or appeal process, etc.). Base 8415 plus this attestation layer give auditable state plus freshness—not omniscience.
  • It does not define how a downstream DEX or marketplace unwinds on Rejected. That remains application-level, per the Base 8415 scope boundary. The annex supplies the proof; the application decides the consequence.

Closing

So the division of labour is: Base 8415 owns the state machine and the audit trail; the attestation layer owns “is the registrar still alive and current.” Two orthogonal proof concerns, composable by risk profile.

I would genuinely love to see how this composition holds up for RWAs, and how that boundary is established between Base 8415 and a verifiable attestation layer.

If the rough scope above is directionally right, feel free to take the pseudocode and run with it—a minimal version built against holderAsOf / isFinalAsOf would be enough to start validating the boundary. And if it helps, I am happy to open a separate thread for the reference implementation so this one stays focused on the design question.

Looking forward to your sketch!

This is a genuinely well-scoped ask — appreciate the three-case split, the fresh/pending/stale trichotomy is exactly the right shape (and you’re right that conflating stale with pending is the whole failure mode worth naming). Want to actually sketch the pseudocode against holderAsOf/isFinalAsOf properly rather than rush a half-formed version — will follow up with a real worked example.

Great, thanks for taking this on! Really looking forward to seeing your worked example and how the pseudocode shapes up against ⁠holderAsOf⁠ / ⁠isFinalAsOf⁠. Take your time—no rush at all!

Here’s the sketch, built and self-checked against real secp256k1 signatures (not hand-waved pseudocode):slight_smile:

Structured exactly against the three cases:

(a) Key rotation. KeyManifest holds block-ranged key windows (mirrors this account’s own live verifier-key-manifest pattern – key_id/valid_from/valid_to – expressed in blocks instead of timestamps, since Base 8415 already resolved precedence via EVM state + block ordering rather than a second clock). key_active_at(block) looks up whichever key was authoritative AT the attestation’s signed_at_block, never the registrar’s current key. Rotation and revocation are kept as separate fields deliberately – a rotated-away key still validates a signature made while it was active; a revoked key never does, retroactively.

(b) Monotonic sequence. verify_sequence_monotonic enforces exactly what you specified: no gaps, no rewinds – next must equal last_seen + 1, not merely > last_seen.

(c) Stale vs pending vs final. classify_freshness takes the staleness threshold as an asset-specific input (never hardcodes one), and returns one of three verdicts that are never conflated – notably, a STALE attestation is STALE even when isFinalAsOf=true, since staleness is about whether the claim itself is still trustworthy as current, not about what it asserts.

The self-check (self_check(), runs standalone) proves each boundary with real crypto rather than asserting it in prose: a pre-rotation signature verifies correctly at its own block; a forged key-id claim at that block is rejected; a forged block-height claim on a genuine signature is rejected (since signed_at_block is inside the signed bytes, not a side-channel field); a sequence rewind and a sequence gap are both rejected; and the full composed path correctly dispatches FRESH_FINAL to release and STALE to quarantine-on-the-stale-path, distinct from FRESH_PENDING.

Kept the two boundaries you named explicit in the file’s own docstring rather than letting them get implied away: this does not prove the registrar can’t sign a false confirmation (separate trust assumption), and it does not decide how an escrow unwinds on Rejected (application-level, per Base 8415’s own scope). escrow_action_for() is a thin dispatch sketch for the informational annex, not a real contract.

Happy to adjust field names/shapes to whatever the annex’s actual interface conventions end up being – this is meant as a starting point to argue with, not a final form.

Thanks for putting this together! The Python sketch is super clear, especially with the standalone ⁠self_check()⁠ proving the sequence monotonically and handling stale vs. pending states properly.

Spot on regarding the boundaries as well—Base 8415 should remain agnostic to escrow unwinding and registrar trust.

1 Like

Thanks again for the Python sketch. I’ve ported erc8415_watchtower_freshness_sketch.py into a runnable EVM setup (watchtower/) in the repo as a discussion draft. It sits completely separate from Base 8415 and does not alter the core standard.

You can check out the code at github.com/GiraffeTechnology/ERC-8415/tree/main/watchtower.

Here is how your sketch maps to the Solidity setup:

  • Key Rotation: Preserved your rotation vs. revocation distinction. Rotated keys validate historical signatures, while revoked keys fail retroactively.
  • Monotonic Sequence: Enforced strict seq == head + 1 order and added block height checks to prevent rewinds.
  • Freshness Classification: Extracted FreshnessLib.sol so apps can check staleness independently. Even a final claim is marked STALE if it exceeds the freshness threshold.

All boundary tests from your self_check() (in-flight signatures, key-id forgery, block-height forgery, and sequence gaps) have been ported to both Hardhat and Foundry fuzzing suites.

One detail to consider: revokeKey currently causes all subsequent submissions to fail until a new key is rotated in, which halts the feed. Two softer options I’m considering: (1) let revoked keys’ historical windows expire naturally, or (2) gate revocation behind a timelock. Happy to hear which (if either) fits your model.

Escrow logic and registrar trust remain strictly at the application layer, keeping the protocol boundary clean.

Would love your thoughts on three quick points:

  1. Does finalityDepth fit your mental model for anchored finality?
  2. Is initialSequence in registerAsset(...) the right hatch for feed migration?
  3. Should we define a standard minimal EIP-712 type string for cross-watchtower interop?

Cheers,
Michael

This is a real, careful port – read through WatchtowerFreshnessLayer.sol + FreshnessLib.sol before answering, not just the description. Taking your three questions in order, plus the revokeKey tradeoff.

1. finalityDepth – this is a genuine, worthwhile semantic shift from the sketch, not just a straight port, and worth naming explicitly. My sketch took is_final_as_of as an EXTERNAL boolean, meant to be Base 8415’s own registrar-confirmed finality passed in from the base state machine – the watchtower layer never computed finality itself, only freshness. FreshnessLib.classify() instead derives FRESH_FINAL purely from age >= finalityDepth – a self-contained, on-chain-computable reorg-safety signal, decoupled from whether the REGISTRAR has actually confirmed anything.

Those are two different claims: “enough blocks have passed that this head won’t reorg away” vs. “the registrar has confirmed this state is final.” Collapsing them into one field is defensible for an informational annex that deliberately doesn’t touch Base 8415’s own state machine (per the annex’s own scope boundary) – but a consuming escrow that reads FRESH_FINAL and assumes registrar-confirmed finality would be reading more into it than the contract actually claims. Two options rather than one: keep finalityDepth exactly as the reorg-safety signal it is now (my preference, since it needs no cross-contract call and stays a clean annex), but make the naming carry that scope – something like Freshness.REORG_SAFE instead of FRESH_FINAL would stop a downstream integrator from silently assuming registrar finality that was never asserted.

2. initialSequence in registerAsset – yes, this is the right hatch, and it matches the sketch’s own principle exactly (“we never rewrite history, we only advance the active key/sequence going forward” – post #14a). Making it a constructor-time-only parameter rather than a callable admin function is the correct shape: migration becomes a one-time, explicit act at registration, not an ongoing capability that could rewind a LIVE feed. One thing worth being explicit about in the docs: the contract has no native link between an old assetId and its migrated successor – a consuming app has to track that continuity itself. Not a flaw, just worth stating so nobody assumes the contract does it for them.

3. Standard minimal EIP-712 type string for cross-watchtower interop – agreed, and I’d go one step further than defining the string: expose the raw TYPE STRING itself via a view function (not just its hash via ATTESTATION_TYPEHASH), the way eip712Domain() already does for the domain. The interop problem isn’t just agreeing on one canonical string – it’s letting a cross-watchtower VERIFIER reconstruct the exact struct hash independently, without hardcoding Solidity source per deployment or trusting the deployer’s own claim of which version they implement. Same principle this project’s own proof schema uses (a policy_commitment hash a caller can recompute, never just asserted) – the type string should be a thing a third party pulls and verifies, not something they take on faith from a README.

revokeKey tradeoff – neither of the two softenings you’re considering is safe if “revoked” means what the sketch meant by it (key COMPROMISE, not planned decommission):

  • Natural expiry re-opens exactly the hole revocation exists to close – a compromised key’s PAST attestations would stay valid until their own staleness threshold passes, meaning a forged attestation signed during the compromise window keeps working for a while after you’ve discovered the compromise.
  • A timelock is the wrong tool here too – it’s right for slowing down a malicious STEWARD, but revocation is an incident-response action responding to a malicious KEY. Delaying it only extends the attacker’s window.

What’s actually causing the “halts the feed” pain, I think, isn’t revocation’s retroactive strength – it’s that revokeKey and rotateKey are two separate transactions, so there’s a real gap where zero keys are active. A rotateAndRevoke(assetId, newKey, newValidFrom, newValidTo, keyToRevoke) convenience function that does both atomically would close that operational gap without softening the security property at all. Worth checking first, though: does “revoked” in your model actually mean compromise, or planned decommission? If it’s the latter, natural expiry is probably fine and this whole concern doesn’t apply – the answer changes depending on which threat the field is meant to cover.

Thanks for going deep into the Solidity implementation and logic checks. This gets to the exact level of precision we need before settling the annex interface. Let me respond directly to your three points plus the revokeKey tradeoff, and confirm the actions on my side.

One — finalityDepth vs. registrar finality → agreed, rename to REORG_SAFE.

You hit the key distinction cleanly. In the base state machine, “final” carries registrar confirmation; in the watchtower layer, age versus finalityDepth is purely an on-chain reorg-safety signal — that the block depth is sufficient so this head will not flip. Collapsing them into a single FRESH_FINAL tag would let downstream escrows read registrar title finality into a check that only guarantees EVM chain immutability.

Action: I will rename FRESH_FINAL to Freshness.REORG_SAFE, or explicitly expose IS_REORG_SAFE and IS_REGISTRAR_FINAL as distinct bitmasks. This keeps the watchtower layer self-contained without making unasserted claims about registrar status. This also aligns with the terminology note in #18 — finality-depth measures projection immutability, not legal title.

Two — initialSequence migration safety → agreed, constructor-time only.

Making initialSequence a constructor-time parameter during registerAsset, and not a callable admin function after deployment, is critical for feed immutability. It ensures migration is an explicit, one-time setup action rather than a perpetual admin door to rewind live feeds.

Documentation note: I will add to the annex specification that the contract does not maintain a native link between a predecessor assetId and its migrated successor. Downstream consuming applications and indexers are responsible for tracking continuity across migrations. This is a clean separation: the protocol records each feed faithfully; continuity is the application’s concern.

Three — EIP-712 interop → agreed, expose raw TYPE_STRING via view function.

Exposing only ATTESTATION_TYPEHASH requires off-chain verifiers to either trust a README or hardcode Solidity source per deployment. Exposing the raw TYPE_STRING through a dedicated view function, similar to eip712Domain(), allows any cross-watchtower verifier to independently reconstruct and verify the struct hash, on-chain or off-chain, without blind trust.

Action: I will add a pure view returning the type string, placed alongside ATTESTATION_TYPEHASH and the domain verifiers. This is a small addition with a large payoff for interop.

Four — revokeKey tradeoff and atomic rotateAndRevoke → agreed, full alignment.

Your diagnosis of the threat model is spot on:

First, revocation means key compromise, not planned decommission. Therefore natural expiry is unsafe — it leaves the vulnerability window open for stale forged attestations — and a timelock is the wrong primitive for incident response.

Second, the actual operational pain point is the zero-active-key gap between calling revokeKey and rotateKey in separate transactions.

Third, action: I will introduce a single atomic method that takes the asset, the new key and its validity window, and the key to revoke. It closes the operational gap atomically without diluting the security properties of revocation.

Great alignment across the board. Once these four are in the Gist and PR draft, I believe the annex interface reaches the precision needed. I will update the implementation accordingly and reflect REORG_SAFE, the raw type-string getter, and the atomic rotateAndRevoke pattern.

Finally, this code-level agreement connects directly to the protocol boundary discussion in the next two posts. The four points above map cleanly onto the four layers I propose there: key continuity and feed immutability sit in layer two (faithful record); freshness and finality-depth sit in layer three (diagnosis); the zero-active-key gap is a layer-two operational concern, not a remedy question. With that bridge in mind, please see belows.

Following up on the freshness-layer work and the code review above — as I read through the contract semantics, I kept running into the same question: where exactly does the protocol end and the application begin? I’d like to propose a split, and get it corrected early if it’s wrong.

Under the hood, ERC-8415 is tracking two sequences that describe the same asset: the on-chain ownership sequence recorded by ERC-721, and the confirmed-holder sequence reported by the external register. At rest they agree; in flight they may diverge. The whole design rests on keeping those two views separate — that’s the point of the projection (OP #1).

The part I want to make explicit is this: the standard should record both faithfully, expose whether they align at any instant, and stop there. Everything else — whether a trade gets cancelled, when, and who unwinds what — sits outside that recording layer.

I find it useful to think in four layers:

Layer one — identity. At a final state, the on-chain owner and the register-confirmed holder are assumed to refer to the same underlying right. The protocol does not, and cannot, verify this equivalence. It is an operational premise the projection assumes, not a property the contract enforces. Stating this explicitly prevents a downstream integrator from reading agreement between records as verified legal identity.

Layer two — faithful record. The standard records both sequences: the holder entries from the register, indexed by effective time, with consecutive versions; and the on-chain ownership changes it sits alongside. It records what happened, not whether what happened was legally valid. This layer also covers the implementation-level immutability concerns surfaced in the review above: constructor-only initialSequence, and atomic key rotation and revocation to prevent operational gaps.

A technical note on what “faithful record” entails. Each holder entry carries: the holder address, effective time, record commitment, previous commitment, registry reference, and a strictly increasing version number. Effective times strictly increase; admission is atomic — proof consumption, remote-height advancement, entry admission, and gap closure happen together; and the core guarantee is non-blocking — opening a gap does not freeze ERC-721 transfers, anyone may relay a proof, and submitting one grants no special rights. These are the constraints that make the record trustworthy without the protocol ever deciding legal validity. The exact scoping of commitment and registry-reference uniqueness is defined below in #21 (per-token enforcement only; cross-token handling is out of scope).

Layer three — diagnosis and freshness. Given those two records, the standard exposes their alignment and freshness at any historical instant — holderAsOf, isFinalAsOf, openGapOf. Crucially, as noted in the code review, signals derived from finalityDepth measure on-chain reorg safety of the projection, not registrar-asserted finality. Misalignment or staleness is a signal that the registrar has not yet recorded, or has stopped recording. The protocol reports the signal. It does not label it success or failure.

Layer four — remedy. Whether to cancel, when to cancel, escrow, timeouts, refunds, downstream unwinding — these belong to the transaction terms between the parties, not the protocol. This is consistent with what I wrote in #3: downstream unwinding is out of scope, and the escrow pattern is a non-normative application-layer reference.

So the responsibility line I’d propose is: the protocol is a faithful record and audit trail — a mirror, not a tribunal. It tells you what each side recorded, and whether those records agree at a given instant. It does not decide outcomes.

This also reframes a few things I’d like to double-check:

First, the load-leveling post spoke of “guaranteeing eventual consistency across both domains.” Read strictly, that sounds like a protocol guarantee. But OP #1 says recent instants can stay non-final indefinitely if the registrar stops issuing. So I read it instead as: the standard records both sequences and exposes their alignment — convergence is an operational property of the registrar, not a guarantee the protocol makes.

Second, the “late veto” framing from earlier replies used Pending / Confirmed / Rejected as shorthand. As #18 clarified, those are not protocol-defined states — the real vocabulary is open/closed gap, provisional/final, and admitted. I’d like the Scope language to use only the latter, so it cannot be read as introducing a rejection event.

Third, holderAsOf(t) is best read as the holder the registrar had confirmed at effective time t, as recorded on chain — with no opinion on the legal validity of that record. That keeps “mirror, not tribunal” airtight.

If this four-layer split holds, #22 below turns it into a proposed Scope section. But I’d genuinely like to be corrected on the boundary before it goes into the PR — especially whether Layer one should be stated as strongly as “assumed identity at final state,” or kept implicit.