Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 15 additions & 7 deletions apps/ensindexer/ponder/src/register-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
31 changes: 13 additions & 18 deletions apps/ensindexer/src/lib/ensv2/canonicality-db-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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
Expand All @@ -250,24 +251,18 @@ export async function handleBridgedResolverChange(
domainId: DomainId,
nextResolver: NormalizedAddress | null,
): Promise<void> {
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;
Comment thread
shrugs marked this conversation as resolved.

// 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Loading