Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ensindexer): introduce label healing from reverse registry #362

Open
wants to merge 19 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
78c07fe
feat(ensindexer): introduce label healing from reverse registry
tk-o Mar 10, 2025
02bd5bf
Merge remote-tracking branch 'origin/main' into feat/204-reverse-regi…
tk-o Mar 10, 2025
7ac7db3
fix(ensindexer): update eth plugin reverse root config
tk-o Mar 10, 2025
49d5aaf
Merge remote-tracking branch 'origin/main' into feat/204-reverse-regi…
tk-o Mar 11, 2025
bcf50d4
refactor(ensindexer): allow plugins to control feature flag
tk-o Mar 11, 2025
b1846c9
Merge remote-tracking branch 'origin/main' into feat/204-reverse-regi…
tk-o Mar 11, 2025
42af477
refactor(ensindexer): reverse addr label healing
tk-o Mar 12, 2025
41d6813
Merge remote-tracking branch 'origin/main' into feat/204-reverse-regi…
tk-o Mar 12, 2025
b4066b4
feat(ensindexer): treat unhealed label as a soft fail
tk-o Mar 12, 2025
110343c
Merge remote-tracking branch 'origin/main' into feat/204-reverse-regi…
tk-o Mar 12, 2025
f562448
feat(ponder.config): update build ID based on config dependecies change
tk-o Mar 12, 2025
10c7918
chore: drop unused type
tk-o Mar 12, 2025
b837e64
Merge remote-tracking branch 'origin/main' into feat/204-reverse-regi…
tk-o Mar 13, 2025
a36bd5b
fix(ensindexer): apply PR feedback for code comments
tk-o Mar 13, 2025
a4b4d7b
Merge branch 'feat/204-reverse-registry-subnames-auto-healing' of git…
tk-o Mar 13, 2025
e77e099
fix(ensnode): stop reverse address label healing
tk-o Mar 13, 2025
3d41bf4
fix(ensnode): simplify `labelByReverseAddress`
tk-o Mar 13, 2025
91de731
refactor(ensindexer): simplify reverse addresses healing integration
tk-o Mar 13, 2025
3b1f3f8
Merge remote-tracking branch 'origin/main' into feat/204-reverse-regi…
tk-o Mar 17, 2025
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
8 changes: 8 additions & 0 deletions apps/ensindexer/.env.local.example
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,11 @@ ENSRAINBOW_URL=https://api.ensrainbow.io
# The ENSIndexer public service URL
# It is used to generate self-referential URLs in the API responses
ENSNODE_PUBLIC_URL=http://localhost:42069

# A feature flag to enable or disable healing of addr.reverse subnames
# If this is set to true, ENSIndexer will attempt to heal subnames of addr.reverse
# If this is not set, the default value is set to `DEFAULT_HEAL_REVERSE_ADDRESSES`.
#
# WARNING: Setting this to `true` results in indexed data no longer being backwards compatible with the ENS Subgraph. For full
# data-level backwards compatibility with the ENS Subgraph, set this to `false`.
HEAL_REVERSE_ADDRESSES=true
21 changes: 19 additions & 2 deletions apps/ensindexer/ponder.config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { SELECTED_DEPLOYMENT_CONFIG } from "./src/lib/globals";
import { type MergedTypes, getActivePlugins } from "./src/lib/plugin-helpers";
import { deepMergeRecursive } from "./src/lib/ponder-helpers";
import { deepMergeRecursive, healReverseAddresses } from "./src/lib/ponder-helpers";
import type { PluginName } from "./src/lib/types";

import * as baseEthPlugin from "./src/plugins/base/ponder.plugin";
Expand All @@ -14,7 +14,19 @@ import * as lineaEthPlugin from "./src/plugins/linea/ponder.plugin";

const ALL_PLUGINS = [ethPlugin, baseEthPlugin, lineaEthPlugin] as const;

type AllPluginConfigs = MergedTypes<(typeof ALL_PLUGINS)[number]["config"]>;
type AllPluginConfigs = MergedTypes<(typeof ALL_PLUGINS)[number]["config"]> & {
/**
* The environment variables that change the behavior of the indexer.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tk-o Love this! Nice work!

* It's important to include all environment variables that change the behavior
* of the indexer to ensure Ponder app build ID is updated when any of them change.
**/
indexingBehaviorDependencies: {
/**
* If set to `false` then indexed data will no longer be backwards compatible with the ENS Subgraph.
*/
HEAL_REVERSE_ADDRESSES: boolean;
};
};

////////
// Next, filter ALL_PLUGINS by those that are available and that the user has activated.
Expand All @@ -35,6 +47,11 @@ const activePluginsMergedConfig = activePlugins
.map((plugin) => plugin.config)
.reduce((acc, val) => deepMergeRecursive(acc, val), {}) as AllPluginConfigs;

// set the indexing behavior dependencies
activePluginsMergedConfig.indexingBehaviorDependencies = {
HEAL_REVERSE_ADDRESSES: healReverseAddresses(),
};

// load indexing handlers from the active plugins into the runtime
activePlugins.forEach((plugin) => plugin.activate());

Expand Down
13 changes: 10 additions & 3 deletions apps/ensindexer/src/handlers/Registrar.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import { type Context } from "ponder:registry";
import schema from "ponder:schema";
import { isLabelIndexable, makeSubnodeNamehash } from "@ensnode/utils/subname-helpers";
import {
isLabelIndexable,
labelByReverseAddress,
makeSubnodeNamehash,
} from "@ensnode/utils/subname-helpers";
import type { Labelhash } from "@ensnode/utils/types";
import { type Hex, labelhash as _labelhash, namehash } from "viem";
import { createSharedEventValues, upsertAccount, upsertRegistration } from "../lib/db-helpers";
import { labelByHash } from "../lib/graphnode-helpers";
import { makeRegistrationId } from "../lib/ids";
import { EventWithArgs } from "../lib/ponder-helpers";
import type { PonderENSPluginHandlerArgs } from "../lib/plugin-helpers";
import type { EventWithArgs } from "../lib/ponder-helpers";
import type { OwnedName } from "../lib/types";

const GRACE_PERIOD_SECONDS = 7776000n; // 90 days in seconds
Expand All @@ -16,7 +21,9 @@ const GRACE_PERIOD_SECONDS = 7776000n; // 90 days in seconds
*
* @param ownedName the name that the Registrar contract manages subnames of
*/
export const makeRegistrarHandlers = (ownedName: OwnedName) => {
export const makeRegistrarHandlers = <OWNED_NAME extends OwnedName>({
ownedName,
}: PonderENSPluginHandlerArgs<OWNED_NAME>) => {
const ownedNameNode = namehash(ownedName);
const sharedEventValues = createSharedEventValues(ownedName);

Expand Down
38 changes: 31 additions & 7 deletions apps/ensindexer/src/handlers/Registry.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
import { Context } from "ponder:registry";
import schema from "ponder:schema";
import { encodeLabelhash } from "@ensdomains/ensjs/utils";
import { ROOT_NODE, isLabelIndexable, makeSubnodeNamehash } from "@ensnode/utils/subname-helpers";
import {
ROOT_NODE,
isLabelIndexable,
labelByReverseAddress,
makeSubnodeNamehash,
} from "@ensnode/utils/subname-helpers";
import type { Labelhash, Node } from "@ensnode/utils/types";
import { type Hex, zeroAddress } from "viem";
import { createSharedEventValues, upsertAccount, upsertResolver } from "../lib/db-helpers";
import { labelByHash } from "../lib/graphnode-helpers";
import { makeResolverId } from "../lib/ids";
import { EventWithArgs } from "../lib/ponder-helpers";
import { OwnedName } from "../lib/types";
import type { PonderENSPluginHandlerArgs } from "../lib/plugin-helpers";
import type { EventWithArgs } from "../lib/ponder-helpers";
import type { OwnedName } from "../lib/types";

/**
* Initializes the ENS root node with the zeroAddress as the owner.
Expand Down Expand Up @@ -73,7 +79,10 @@ async function recursivelyRemoveEmptyDomainFromParentSubdomainCount(context: Con
*
* @param ownedName the name that the Registry contract manages subnames of
*/
export const makeRegistryHandlers = (ownedName: OwnedName) => {
export const makeRegistryHandlers = <OWNED_NAME extends OwnedName>({
canHealReverseAddressFromParentNode,
ownedName,
}: PonderENSPluginHandlerArgs<OWNED_NAME>) => {
const sharedEventValues = createSharedEventValues(ownedName);

return {
Expand Down Expand Up @@ -122,9 +131,24 @@ export const makeRegistryHandlers = (ownedName: OwnedName) => {
if (!domain.name) {
const parent = await context.db.find(schema.domain, { id: node });

// attempt to heal the label associated with labelhash via ENSRainbow
// https://github.com/ensdomains/ens-subgraph/blob/c68a889/src/ensRegistry.ts#L112-L116
const healedLabel = await labelByHash(labelhash);
let healedLabel = null;

// if healing label from reverse addresses is possible, give it a go
if (canHealReverseAddressFromParentNode(node)) {
// TODO: if healing failed, log the event args for analysis and debugging
healedLabel = labelByReverseAddress({
maybeReverseAddress: owner,
labelhash,
});
}

// if label hasn't been healed yet
if (!healedLabel) {
// attempt to heal the label associated with labelhash via ENSRainbow
// https://github.com/ensdomains/ens-subgraph/blob/c68a889/src/ethRegistrar.ts#L56-L61
healedLabel = await labelByHash(labelhash);
}

const validLabel = isLabelIndexable(healedLabel) ? healedLabel : undefined;

// to construct `Domain.name` use the parent's name and the label value (encoded if not indexable)
Expand Down
10 changes: 10 additions & 0 deletions apps/ensindexer/src/lib/plugin-helpers.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { SubregistryContractConfig } from "@ensnode/ens-deployments";
import type { Node } from "@ensnode/utils/types";
import type { NetworkConfig } from "ponder";
import { http, Chain } from "viem";
import { END_BLOCK, START_BLOCK } from "./globals";
Expand Down Expand Up @@ -172,7 +173,16 @@ export interface PonderENSPlugin<PLUGIN_NAME extends PluginName, CONFIG> {
*/
export type PonderENSPluginHandlerArgs<OWNED_NAME extends OwnedName> = {
ownedName: OwnedName;

namespace: ReturnType<typeof createPluginNamespace<OWNED_NAME>>;

/**
* Determines whether a reverse address can be healed for given parent node.
* @param {Node} parentNode a node that might be a reverse root node
*
* @returns true if the reverse address can be healed from the parent node
*/
canHealReverseAddressFromParentNode(parentNode: Node): boolean;
};

/**
Expand Down
48 changes: 47 additions & 1 deletion apps/ensindexer/src/lib/ponder-helpers.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Event } from "ponder:registry";
import DeploymentConfigs, { ENSDeploymentChain } from "@ensnode/ens-deployments";
import DeploymentConfigs, { type ENSDeploymentChain } from "@ensnode/ens-deployments";
import { DEFAULT_ENSRAINBOW_URL } from "@ensnode/ensrainbow-sdk";
import type { BlockInfo } from "@ensnode/ponder-metadata";
import { merge as tsDeepMerge } from "ts-deepmerge";
Expand Down Expand Up @@ -248,6 +248,52 @@ export const parseRequestedPluginNames = (rawValue?: string): Array<string> => {
return rawValue.split(",");
};

/**
* Feature flag that determines whether the indexer should attempt healing
* reverse addresses.
*
* @returns decision whether to heal reverse addresses
*/
export const healReverseAddresses = (): boolean => {
const envVarName = "HEAL_REVERSE_ADDRESSES";
const envVarValue = process.env[envVarName];

let parsedEnvVarValue: boolean;

try {
parsedEnvVarValue = parseHealReverseAddresses(envVarValue);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "Unknown error";

throw new Error(`Error parsing environment variable '${envVarName}': ${errorMessage}.`);
}

return parsedEnvVarValue;
};

export const DEFAULT_HEAL_REVERSE_ADDRESSES = true;

/**
* Parse input value and apply `HEAL_REVERSE_ADDRESSES_DEFAULT` value
* if not provided.
*
* @param rawValue value to be parsed
* @returns {boolean} parsed input value
*/
export const parseHealReverseAddresses = (rawValue?: string): boolean => {
if (!rawValue) {
return DEFAULT_HEAL_REVERSE_ADDRESSES;
}

const isValueValid = (v: string): boolean => v === "true" || v === "false";

if (!isValueValid(rawValue)) {
throw new Error(`'${rawValue}' is not a valid value. Expected 'true' or 'false'`);
}

return rawValue === "true";
};

/** Get the Ponder application port */
export const ponderPort = (): number => {
const envVarName = "PORT";
Expand Down
5 changes: 3 additions & 2 deletions apps/ensindexer/src/plugins/base/handlers/Registrar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,16 @@ import { PonderENSPluginHandlerArgs } from "../../../lib/plugin-helpers";
*/
const tokenIdToLabelhash = (tokenId: bigint): Labelhash => uint256ToHex32(tokenId);

export default function ({ ownedName, namespace }: PonderENSPluginHandlerArgs<"base.eth">) {
export default function (args: PonderENSPluginHandlerArgs<"base.eth">) {
const { namespace } = args;
const {
handleNameRegistered,
handleNameRegisteredByController,
handleNameRenewedByController,
handleNameRenewed,
handleNameTransferred,
ownedSubnameNode,
} = makeRegistrarHandlers(ownedName);
} = makeRegistrarHandlers(args);

// support NameRegisteredWithRecord for BaseRegistrar as it used by Base's RegistrarControllers
ponder.on(namespace("BaseRegistrar:NameRegisteredWithRecord"), async ({ context, event }) => {
Expand Down
11 changes: 4 additions & 7 deletions apps/ensindexer/src/plugins/base/handlers/Registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,10 @@ import { ponder } from "ponder:registry";
import { makeRegistryHandlers, setupRootNode } from "../../../handlers/Registry";
import { PonderENSPluginHandlerArgs } from "../../../lib/plugin-helpers";

export default function ({ ownedName, namespace }: PonderENSPluginHandlerArgs<"base.eth">) {
const {
handleNewOwner, //
handleNewResolver,
handleNewTTL,
handleTransfer,
} = makeRegistryHandlers(ownedName);
export default function (args: PonderENSPluginHandlerArgs<"base.eth">) {
const { namespace } = args;
const { handleNewOwner, handleNewResolver, handleNewTTL, handleTransfer } =
makeRegistryHandlers(args);

ponder.on(namespace("Registry:setup"), setupRootNode);
ponder.on(namespace("Registry:NewOwner"), handleNewOwner(true));
Expand Down
5 changes: 5 additions & 0 deletions apps/ensindexer/src/plugins/base/ponder.plugin.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { Node } from "@ensnode/utils/types";
import { createConfig } from "ponder";
import { DEPLOYMENT_CONFIG } from "../../lib/globals";
import {
Expand All @@ -16,6 +17,9 @@ const ownedName = "base.eth" as const;
const { chain, contracts } = DEPLOYMENT_CONFIG[pluginName];
const namespace = createPluginNamespace(ownedName);

// Support for healing addr.reverse subnames on Base will be added later
const canHealReverseAddressFromParentNode = () => false;

export const config = createConfig({
networks: networksConfigForChain(chain),
contracts: {
Expand Down Expand Up @@ -45,6 +49,7 @@ export const config = createConfig({
});

export const activate = activateHandlers({
canHealReverseAddressFromParentNode,
ownedName,
namespace,
handlers: [
Expand Down
5 changes: 3 additions & 2 deletions apps/ensindexer/src/plugins/eth/handlers/EthRegistrar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,15 @@ import { PonderENSPluginHandlerArgs } from "../../../lib/plugin-helpers";
*/
const tokenIdToLabelhash = (tokenId: bigint): Labelhash => uint256ToHex32(tokenId);

export default function ({ ownedName, namespace }: PonderENSPluginHandlerArgs<"eth">) {
export default function (args: PonderENSPluginHandlerArgs<"eth">) {
const { namespace } = args;
const {
handleNameRegistered,
handleNameRegisteredByController,
handleNameRenewedByController,
handleNameRenewed,
handleNameTransferred,
} = makeRegistrarHandlers(ownedName);
} = makeRegistrarHandlers(args);

ponder.on(namespace("BaseRegistrar:NameRegistered"), async ({ context, event }) => {
await handleNameRegistered({
Expand Down
11 changes: 4 additions & 7 deletions apps/ensindexer/src/plugins/eth/handlers/Registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,10 @@ async function shouldIgnoreRegistryOldEvents(context: Context, node: Hex) {
return domain?.isMigrated ?? false;
}

export default function ({ ownedName, namespace }: PonderENSPluginHandlerArgs<"eth">) {
const {
handleNewOwner, //
handleNewResolver,
handleNewTTL,
handleTransfer,
} = makeRegistryHandlers(ownedName);
export default function (args: PonderENSPluginHandlerArgs<"eth">) {
const { namespace } = args;
const { handleNewOwner, handleNewResolver, handleNewTTL, handleTransfer } =
makeRegistryHandlers(args);

ponder.on(namespace("RegistryOld:setup"), setupRootNode);

Expand Down
14 changes: 14 additions & 0 deletions apps/ensindexer/src/plugins/eth/ponder.plugin.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { Node } from "@ensnode/utils/types";
import { createConfig } from "ponder";
import { DEPLOYMENT_CONFIG } from "../../lib/globals";
import {
Expand All @@ -6,16 +7,28 @@ import {
networkConfigForContract,
networksConfigForChain,
} from "../../lib/plugin-helpers";
import { healReverseAddresses } from "../../lib/ponder-helpers";

// uses the 'eth' plugin config for deployments
export const pluginName = "eth" as const;

// the Registry/Registrar handlers in this plugin manage subdomains of '.eth'
const ownedName = "eth" as const;

// namehash('addr.reverse')
const reverseRootNode: Node = "0x91d1777781884d03a6757a803996e38de2a42967fb37eeaca72729271025a9e2";

const { chain, contracts } = DEPLOYMENT_CONFIG[pluginName];
const namespace = createPluginNamespace(ownedName);

// `eth` plugin can heal reverse addresses from its reverse root node
const canHealReverseAddressFromParentNode = (parentNode: Node): boolean => {
const isReverseRootNode = (maybeReverseRootNode: Node): boolean =>
maybeReverseRootNode.toLowerCase() === reverseRootNode.toLowerCase();

return healReverseAddresses() && isReverseRootNode(parentNode);
};

export const config = createConfig({
networks: networksConfigForChain(chain),
contracts: {
Expand Down Expand Up @@ -53,6 +66,7 @@ export const config = createConfig({
});

export const activate = activateHandlers({
canHealReverseAddressFromParentNode,
ownedName,
namespace,
handlers: [
Expand Down
5 changes: 3 additions & 2 deletions apps/ensindexer/src/plugins/linea/handlers/EthRegistrar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,16 @@ import { PonderENSPluginHandlerArgs } from "../../../lib/plugin-helpers";
*/
const tokenIdToLabelhash = (tokenId: bigint): Labelhash => uint256ToHex32(tokenId);

export default function ({ ownedName, namespace }: PonderENSPluginHandlerArgs<"linea.eth">) {
export default function (args: PonderENSPluginHandlerArgs<"linea.eth">) {
const { namespace } = args;
const {
handleNameRegistered,
handleNameRegisteredByController,
handleNameRenewedByController,
handleNameRenewed,
handleNameTransferred,
ownedSubnameNode,
} = makeRegistrarHandlers(ownedName);
} = makeRegistrarHandlers(args);

ponder.on(namespace("BaseRegistrar:NameRegistered"), async ({ context, event }) => {
await handleNameRegistered({
Expand Down
Loading