diff --git a/apps/ensindexer/ponder/src/register-handlers.ts b/apps/ensindexer/ponder/src/register-handlers.ts index 499b9177de..5edfbe13a2 100644 --- a/apps/ensindexer/ponder/src/register-handlers.ts +++ b/apps/ensindexer/ponder/src/register-handlers.ts @@ -47,18 +47,26 @@ if (config.plugins.includes(PluginName.TokenScope)) { attach_TokenscopeHandlers(); } -// REQUIRED ORDER: NodeMigration → Unigraph → ProtocolAcceleration +// IMPORTANT: the order of these attach_*() calls does NOT control the order Ponder dispatches +// handlers. Ponder orders events by checkpoint (chainId, blockNumber, transactionIndex, logIndex). +// Two handlers registered against the SAME log (e.g. Unigraph and ProtocolAcceleration both on +// `ENSv1Registry:NewResolver`) receive IDENTICAL checkpoints, and Ponder's tie-break is not +// deterministic — so NO ordering between same-log handlers can be relied upon. Handlers must +// therefore be independent of each other's same-event writes (see `handleBridgedResolverChange`, +// which reads the Domain's own `subregistryId` rather than ProtocolAcceleration's Domain-Resolver +// Relation for exactly this reason). // -// 1. NodeMigration runs first so that `nodeIsMigrated` is populated before either plugin's -// Old-registry guards consult it. -// 2. Unigraph runs before ProtocolAcceleration so its `handleBridgedResolverChange` can read the -// PREVIOUS Domain-Resolver Relation from the index — ProtocolAcceleration's NewResolver / -// ResolverUpdated handlers overwrite that row, so reading MUST happen first. -// 3. ProtocolAcceleration's resolver handlers then write the new DRR. +// Cross-log ordering (different contracts/logs) IS deterministic by checkpoint. NodeMigration +// relies only on that: it writes `nodeIsMigrated` on `ENSv1Registry:NewOwner` (the new Registry), +// and the Old-registry guards read it on `ENSv1RegistryOld:*` events — different logs, so a node's +// migration is always processed before any stale Old-registry event that consults it. // // Note: NodeMigration is gated on ProtocolAcceleration but the Unigraph plugin has // ProtocolAcceleration as a hard requirement, so checking ProtocolAcceleration is sufficient // to cover both plugins' needs. +// +// In the future, we may abstract the NodeMigration logic further, or unify the ProtocolAcceleration +// and Unigraph plugins to avoid this ordering concern. if (config.plugins.includes(PluginName.ProtocolAcceleration)) { attach_NodeMigrationHandlers(); diff --git a/apps/ensindexer/src/lib/ensv2/canonicality-db-helpers.ts b/apps/ensindexer/src/lib/ensv2/canonicality-db-helpers.ts index e1249fd805..d84481ca0d 100644 --- a/apps/ensindexer/src/lib/ensv2/canonicality-db-helpers.ts +++ b/apps/ensindexer/src/lib/ensv2/canonicality-db-helpers.ts @@ -12,7 +12,7 @@ import type { } from "enssdk"; import { isRootRegistryId } from "@ensnode/ensnode-sdk"; -import { isBridgedResolver } from "@ensnode/ensnode-sdk/internal"; +import { isBridgedResolver, isBridgedTargetRegistry } from "@ensnode/ensnode-sdk/internal"; import { namehashLabelHashPath } from "@/lib/ensv2/namehash-label-hash-path"; import { ensIndexerSchema, type IndexingEngineContext } from "@/lib/indexing-engines/ponder"; @@ -236,9 +236,10 @@ export async function handleSubregistryUpdated( * Reconciles the canonical edge for a Domain whose Resolver just changed. Detaches any prior * bridged target and attaches the new one (when the new resolver is a known Bridged Resolver). * - * Reads the previous resolver from the Domain-Resolver Relation. This requires that this helper - * runs BEFORE Protocol Acceleration's NewResolver/ResolverUpdated handlers, which overwrite the - * DRR row — see `apps/ensindexer/ponder/src/register-handlers.ts` for the ordering. + * Derives the previous bridged target from the Domain's own `subregistryId` (the field this helper + * owns) rather than the Domain-Resolver Relation. Protocol Acceleration's NewResolver/ResolverUpdated + * handlers overwrite the DRR row for the SAME event, so reading the DRR here would depend on + * cross-plugin handler ordering. Reading `subregistryId` keeps this helper order-independent. * * This helper manages only the originating Domain's `subregistryId` (pointing it at the Bridged * Resolver's target Registry, or clearing it). The target Registry's `canonicalDomainId` is owned @@ -250,24 +251,18 @@ export async function handleBridgedResolverChange( domainId: DomainId, nextResolver: NormalizedAddress | null, ): Promise { - const prev = await context.ensDb.find(ensIndexerSchema.domainResolverRelation, { - chainId: registry.chainId, - address: registry.address, - domainId, - }); - - const prevResolver = prev?.resolver; - - const prevBridged = prevResolver - ? isBridgedResolver(config.namespace, { chainId: registry.chainId, address: prevResolver }) - : null; - const nextBridged = nextResolver ? isBridgedResolver(config.namespace, { chainId: registry.chainId, address: nextResolver }) : null; - // the previous and the next are identical, no-op - // NOTE: this also covers the "neither are bridged resolvers" case (null === null) + // the Domain's current bridged target, derived from its own `subregistryId` rather than the DRR + const domain = await context.ensDb.find(ensIndexerSchema.domain, { id: domainId }); + const prevBridged = domain?.subregistryId + ? isBridgedTargetRegistry(config.namespace, domain.subregistryId) + : null; + + // the previous and the next bridged targets are identical, no-op + // NOTE: this also covers the "neither is a bridged resolver" case (null === null) if (prevBridged?.targetRegistryId === nextBridged?.targetRegistryId) return; // handle the domain's implicit SubregistryUpdated event diff --git a/apps/ensindexer/src/plugins/protocol-acceleration/handlers/node-migration.ts b/apps/ensindexer/src/plugins/protocol-acceleration/handlers/node-migration.ts index 6728ddc3a9..bbe56f6a34 100644 --- a/apps/ensindexer/src/plugins/protocol-acceleration/handlers/node-migration.ts +++ b/apps/ensindexer/src/plugins/protocol-acceleration/handlers/node-migration.ts @@ -15,9 +15,13 @@ const ensRootChainId = getENSRootChainId(config.namespace); /** * Node migration handler — tracks ENSv1RegistryOld → ENSv1Registry migration on the ENS Root Chain. * - * Extracted from the 'protocol-acceleration' plugin so it can be registered before both the 'unigraph' and - * 'protocol-acceleration' plugins. This guarantees `nodeIsMigrated` reads from a populated table when - * those plugins' Old-registry guards run. + * Extracted from the 'protocol-acceleration' plugin so its migration tracking is callable when + * either plugin is active, independent of plugin selection. + * + * Correctness does NOT depend on handler registration order: this writes `nodeIsMigrated` on the + * new Registry's `ENSv1Registry:NewOwner`, while the Old-registry guards read it on + * `ENSv1RegistryOld:*` events. Those are different logs, so Ponder's checkpoint ordering guarantees + * a node's migration is processed before any later Old-registry event that consults it. */ export default function () { addOnchainEventListener(