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
Changes from 3 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
9 changes: 9 additions & 0 deletions apps/ensindexer/.env.local.example
Original file line number Diff line number Diff line change
@@ -57,3 +57,12 @@ 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 reverse address healing
# If this is set to true, the indexer will attempt to heal reverse addresses
# for the ENS names that are owned by the address that is being healed.
# If this is not set, the default value is set to `DEFAULT_HEAL_REVERSE_ADDRESSES`.
#
# WARNING: This is a breaking change for the ENS Subgraph compatibility. To avoid
# breaking changes, set this to `false`.
HEAL_REVERSE_ADDRESSES=true
38 changes: 33 additions & 5 deletions apps/ensindexer/src/handlers/Registrar.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,39 @@
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 { OwnedName } from "../lib/types";
import { EventWithArgs, canHealReverseAddresses } from "../lib/ponder-helpers";
import type { OwnedName, ReverseRootNode } from "../lib/types";

const GRACE_PERIOD_SECONDS = 7776000n; // 90 days in seconds

interface MakeRegistrarHandlersArgs {
ownedName: OwnedName;

/**
* Optional, defines the reverse registrar root node.
* Some plugins might not need it at the moment.
**/
reverseRootNode?: ReverseRootNode;
}

/**
* makes a set of shared handlers for a Registrar contract that manages `ownedName`
*
* @param ownedName the name that the Registrar contract manages subnames of
*/
export const makeRegistrarHandlers = (ownedName: OwnedName) => {
export const makeRegistrarHandlers = ({
ownedName,
reverseRootNode,
}: MakeRegistrarHandlersArgs) => {
const ownedNameNode = namehash(ownedName);
const sharedEventValues = createSharedEventValues(ownedName);

@@ -81,7 +98,18 @@ export const makeRegistrarHandlers = (ownedName: OwnedName) => {

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

// if the label has not healed with ENSRainbow query
// and healing label from reverse addresses is enabled, give it a go
if (!healedLabel && canHealReverseAddresses()) {
healedLabel = labelByReverseAddress({
senderAddress: owner,
parentNode: node,
labelhash,
reverseRootNode,
});
}

// only update the label if it is healed & indexable
// undefined value means no change to the label
37 changes: 32 additions & 5 deletions apps/ensindexer/src/handlers/Registry.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
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 EventWithArgs, canHealReverseAddresses } from "../lib/ponder-helpers";
import type { OwnedName, ReverseRootNode } from "../lib/types";

/**
* Initializes the ENS root node with the zeroAddress as the owner.
@@ -68,12 +73,22 @@ async function recursivelyRemoveEmptyDomainFromParentSubdomainCount(context: Con
}
}

interface MakeRegistryHandlersArgs {
ownedName: OwnedName;

/**
* Optional, defines the reverse registrar root node.
* Some plugins might not need it at the moment.
**/
reverseRootNode?: ReverseRootNode;
}

/**
* makes a set of shared handlers for a Registry contract that manages `ownedName`
*
* @param ownedName the name that the Registry contract manages subnames of
*/
export const makeRegistryHandlers = (ownedName: OwnedName) => {
export const makeRegistryHandlers = ({ ownedName, reverseRootNode }: MakeRegistryHandlersArgs) => {
const sharedEventValues = createSharedEventValues(ownedName);

return {
@@ -124,7 +139,19 @@ export const makeRegistryHandlers = (ownedName: OwnedName) => {

// 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 = await labelByHash(labelhash);

// if the label has not healed with ENSRainbow query
// and healing label from reverse addresses is enabled, give it a go
if (!healedLabel && canHealReverseAddresses()) {
Copy link
Contributor

Choose a reason for hiding this comment

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

maybe check that first and if not try healing with ENSRainbow

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah ok, so first we try healing based of the in memory data (using reverse address method), and only if it didn't return any result we should try healing with ENSRainbow. Did I get that right?

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, maybe it will be faster?

healedLabel = labelByReverseAddress({
senderAddress: event.transaction.from,
parentNode: node,
labelhash,
reverseRootNode,
});
}

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

// to construct `Domain.name` use the parent's name and the label value (encoded if not indexable)
8 changes: 7 additions & 1 deletion 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";
@@ -9,7 +10,7 @@ import {
rpcEndpointUrl,
rpcMaxRequestsPerSecond,
} from "./ponder-helpers";
import type { OwnedName, PluginName } from "./types";
import type { OwnedName, PluginName, ReverseRootNode } from "./types";

/**
* A factory function that returns a function to create a namespaced contract
@@ -172,7 +173,12 @@ export interface PonderENSPlugin<PLUGIN_NAME extends PluginName, CONFIG> {
*/
export type PonderENSPluginHandlerArgs<OWNED_NAME extends OwnedName> = {
ownedName: OwnedName;

namespace: ReturnType<typeof createPluginNamespace<OWNED_NAME>>;

// Optional, defines the reverse registrar root node.
// Some plugins might not need it at the moment.
reverseRootNode?: OWNED_NAME extends "eth" ? ReverseRootNode : undefined;
};

/**
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 { merge as tsDeepMerge } from "ts-deepmerge";

@@ -245,3 +245,49 @@ 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 canHealReverseAddresses = (): 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";
};
10 changes: 10 additions & 0 deletions apps/ensindexer/src/lib/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { Node } from "@ensnode/utils/types";

/**
* An owned name for a plugin. Must end with `eth`.
*
@@ -8,6 +10,14 @@
*/
export type OwnedName = string;

/**
* A root node for Reverse registrar. Used to distinguish between plugins that
* handle different reverse registrar root nodes. For example `eth` plugin will
* have a reverse root node of `namehash("addr.reverse")`, while other plugins
* might have different reverse root nodes or none at all.
*/
export type ReverseRootNode = Node;

/**
* In this project we use the notion of 'plugins' to describe which registries and subregistries
* of a given ENS deployment are being indexed by ponder. In this project, a plugin's name is the
2 changes: 1 addition & 1 deletion apps/ensindexer/src/plugins/base/handlers/Registrar.ts
Original file line number Diff line number Diff line change
@@ -24,7 +24,7 @@ export default function ({ ownedName, namespace }: PonderENSPluginHandlerArgs<"b
handleNameRenewed,
handleNameTransferred,
ownedSubnameNode,
} = makeRegistrarHandlers(ownedName);
} = makeRegistrarHandlers({ ownedName });

// support NameRegisteredWithRecord for BaseRegistrar as it used by Base's RegistrarControllers
ponder.on(namespace("BaseRegistrar:NameRegisteredWithRecord"), async ({ context, event }) => {
2 changes: 1 addition & 1 deletion apps/ensindexer/src/plugins/base/handlers/Registry.ts
Original file line number Diff line number Diff line change
@@ -8,7 +8,7 @@ export default function ({ ownedName, namespace }: PonderENSPluginHandlerArgs<"b
handleNewResolver,
handleNewTTL,
handleTransfer,
} = makeRegistryHandlers(ownedName);
} = makeRegistryHandlers({ ownedName });

ponder.on(namespace("Registry:setup"), setupRootNode);
ponder.on(namespace("Registry:NewOwner"), handleNewOwner(true));
11 changes: 9 additions & 2 deletions apps/ensindexer/src/plugins/eth/handlers/EthRegistrar.ts
Original file line number Diff line number Diff line change
@@ -13,14 +13,21 @@ import { PonderENSPluginHandlerArgs } from "../../../lib/plugin-helpers";
*/
const tokenIdToLabelhash = (tokenId: bigint): Labelhash => uint256ToHex32(tokenId);

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

ponder.on(namespace("BaseRegistrar:NameRegistered"), async ({ context, event }) => {
await handleNameRegistered({
16 changes: 9 additions & 7 deletions apps/ensindexer/src/plugins/eth/handlers/Registry.ts
Original file line number Diff line number Diff line change
@@ -18,13 +18,15 @@ 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 ({
ownedName,
namespace,
reverseRootNode,
}: PonderENSPluginHandlerArgs<"eth">) {
const { handleNewOwner, handleNewResolver, handleNewTTL, handleTransfer } = makeRegistryHandlers({
ownedName,
reverseRootNode,
});

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

5 changes: 5 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 {
@@ -13,6 +14,9 @@ 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);

@@ -55,6 +59,7 @@ export const config = createConfig({
export const activate = activateHandlers({
ownedName,
namespace,
reverseRootNode,
handlers: [
import("./handlers/Registry"),
import("./handlers/EthRegistrar"),
Original file line number Diff line number Diff line change
@@ -24,7 +24,7 @@ export default function ({ ownedName, namespace }: PonderENSPluginHandlerArgs<"l
handleNameRenewed,
handleNameTransferred,
ownedSubnameNode,
} = makeRegistrarHandlers(ownedName);
} = makeRegistrarHandlers({ ownedName });

ponder.on(namespace("BaseRegistrar:NameRegistered"), async ({ context, event }) => {
await handleNameRegistered({
2 changes: 1 addition & 1 deletion apps/ensindexer/src/plugins/linea/handlers/Registry.ts
Original file line number Diff line number Diff line change
@@ -8,7 +8,7 @@ export default function ({ ownedName, namespace }: PonderENSPluginHandlerArgs<"l
handleNewResolver,
handleNewTTL,
handleTransfer,
} = makeRegistryHandlers(ownedName);
} = makeRegistryHandlers({ ownedName });

ponder.on(namespace("Registry:setup"), setupRootNode);
ponder.on(namespace("Registry:NewOwner"), handleNewOwner(true));
Loading