Skip to content

Commit

Permalink
fix(registration): use unique registration ID (#82)
Browse files Browse the repository at this point in the history
Co-authored-by: lightwalker.eth <[email protected]>
  • Loading branch information
tk-o and lightwalker-eth authored Jan 28, 2025
1 parent 2a7d24b commit bdfcd08
Show file tree
Hide file tree
Showing 12 changed files with 166 additions and 37 deletions.
28 changes: 18 additions & 10 deletions apps/ensnode/src/handlers/NameWrapper.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
import { type Context, type Event, type EventNames } from "ponder:registry";
import { type Context } from "ponder:registry";
import schema from "ponder:schema";
import { checkPccBurned } from "@ensdomains/ensjs/utils";
import { decodeDNSPacketBytes, tokenIdToLabel } from "ensnode-utils/subname-helpers";
import type { Node } from "ensnode-utils/types";
import { type Address, type Hex, hexToBytes, namehash } from "viem";
import { sharedEventValues, upsertAccount } from "../lib/db-helpers";
import { bigintMax } from "../lib/helpers";
import { makeEventId } from "../lib/ids";
import { EventWithArgs } from "../lib/ponder-helpers";
import type { OwnedName } from "../lib/types";

// if the wrappedDomain has PCC set in fuses, set domain's expiryDate to the greatest of the two
async function materializeDomainExpiryDate(context: Context, node: Hex) {
const wrappedDomain = await context.db.find(schema.wrappedDomain, { id: node });
async function materializeDomainExpiryDate(context: Context, node: Node) {
const wrappedDomain = await context.db.find(schema.wrappedDomain, {
id: node,
});
if (!wrappedDomain) throw new Error(`Expected WrappedDomain(${node})`);

// NOTE: the subgraph has a helper function called [checkPccBurned](https://github.com/ensdomains/ens-subgraph/blob/master/src/nameWrapper.ts#L63)
Expand Down Expand Up @@ -70,7 +74,7 @@ async function handleTransfer(
});
}

export const makeNameWrapperHandlers = (ownedName: `${string}eth`) => {
export const makeNameWrapperHandlers = (ownedName: OwnedName) => {
const ownedSubnameNode = namehash(ownedName);

return {
Expand All @@ -80,7 +84,7 @@ export const makeNameWrapperHandlers = (ownedName: `${string}eth`) => {
}: {
context: Context;
event: EventWithArgs<{
node: Hex;
node: Node;
owner: Hex;
fuses: number;
expiry: bigint;
Expand Down Expand Up @@ -128,7 +132,7 @@ export const makeNameWrapperHandlers = (ownedName: `${string}eth`) => {
event,
}: {
context: Context;
event: EventWithArgs<{ node: Hex; owner: Hex }>;
event: EventWithArgs<{ node: Node; owner: Hex }>;
}) {
const { node, owner } = event.args;

Expand Down Expand Up @@ -157,13 +161,15 @@ export const makeNameWrapperHandlers = (ownedName: `${string}eth`) => {
event,
}: {
context: Context;
event: EventWithArgs<{ node: Hex; fuses: number }>;
event: EventWithArgs<{ node: Node; fuses: number }>;
}) {
const { node, fuses } = event.args;

// NOTE: subgraph no-ops this event if there's not a wrappedDomain already in the db.
// https://github.com/ensdomains/ens-subgraph/blob/master/src/nameWrapper.ts#L144
const wrappedDomain = await context.db.find(schema.wrappedDomain, { id: node });
const wrappedDomain = await context.db.find(schema.wrappedDomain, {
id: node,
});
if (wrappedDomain) {
// set fuses
await context.db.update(schema.wrappedDomain, { id: node }).set({ fuses });
Expand All @@ -184,13 +190,15 @@ export const makeNameWrapperHandlers = (ownedName: `${string}eth`) => {
event,
}: {
context: Context;
event: EventWithArgs<{ node: Hex; expiry: bigint }>;
event: EventWithArgs<{ node: Node; expiry: bigint }>;
}) {
const { node, expiry } = event.args;

// NOTE: subgraph no-ops this event if there's not a wrappedDomain already in the db.
// https://github.com/ensdomains/ens-subgraph/blob/master/src/nameWrapper.ts#L169
const wrappedDomain = await context.db.find(schema.wrappedDomain, { id: node });
const wrappedDomain = await context.db.find(schema.wrappedDomain, {
id: node,
});
if (wrappedDomain) {
// update expiryDate
await context.db.update(schema.wrappedDomain, { id: node }).set({ expiryDate: expiry });
Expand Down
38 changes: 29 additions & 9 deletions apps/ensnode/src/handlers/Registrar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,22 @@ import {
makeSubnodeNamehash,
tokenIdToLabel,
} from "ensnode-utils/subname-helpers";
import type { Labelhash } from "ensnode-utils/types";
import { type Hex, labelhash, namehash } from "viem";
import { sharedEventValues, upsertAccount, upsertRegistration } from "../lib/db-helpers";
import { makeRegistrationId } from "../lib/ids";
import { EventWithArgs } from "../lib/ponder-helpers";
import type { OwnedName } from "../lib/types";

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

/**
* A factory function that returns Ponder indexing handlers for a specified subname.
*/
export const makeRegistrarHandlers = (ownedName: `${string}eth`) => {
export const makeRegistrarHandlers = (ownedName: OwnedName) => {
const ownedSubnameNode = namehash(ownedName);

async function setNamePreimage(context: Context, name: string, label: Hex, cost: bigint) {
async function setNamePreimage(context: Context, name: string, label: Labelhash, cost: bigint) {
// NOTE: ponder intentionally removes null bytes to spare users the footgun of
// inserting null bytes into postgres. We don't like this behavior, though, because it's
// important that 'vitalik\x00'.eth and vitalik.eth are differentiable.
Expand All @@ -42,7 +45,11 @@ export const makeRegistrarHandlers = (ownedName: `${string}eth`) => {
.set({ labelName: name, name: `${name}.${ownedName}` });
}

await context.db.update(schema.registration, { id: label }).set({ labelName: name, cost });
await context.db
.update(schema.registration, {
id: makeRegistrationId(ownedName, label, node),
})
.set({ labelName: name, cost });
}

return {
Expand All @@ -68,7 +75,7 @@ export const makeRegistrarHandlers = (ownedName: `${string}eth`) => {
const labelName = undefined;

await upsertRegistration(context, {
id: label,
id: makeRegistrationId(ownedName, label, node),
domainId: node,
registrationDate: event.block.timestamp,
expiryDate: expires,
Expand Down Expand Up @@ -96,7 +103,11 @@ export const makeRegistrarHandlers = (ownedName: `${string}eth`) => {
event,
}: {
context: Context;
event: EventWithArgs<{ name: string; label: Hex; cost: bigint }>;
event: EventWithArgs<{
name: string;
label: Labelhash;
cost: bigint;
}>;
}) {
const { name, label, cost } = event.args;
await setNamePreimage(context, name, label, cost);
Expand All @@ -107,7 +118,7 @@ export const makeRegistrarHandlers = (ownedName: `${string}eth`) => {
event,
}: {
context: Context;
event: EventWithArgs<{ name: string; label: Hex; cost: bigint }>;
event: EventWithArgs<{ name: string; label: Labelhash; cost: bigint }>;
}) {
const { name, label, cost } = event.args;
await setNamePreimage(context, name, label, cost);
Expand All @@ -125,7 +136,11 @@ export const makeRegistrarHandlers = (ownedName: `${string}eth`) => {
const label = tokenIdToLabel(id);
const node = makeSubnodeNamehash(ownedSubnameNode, label);

await context.db.update(schema.registration, { id: label }).set({ expiryDate: expires });
await context.db
.update(schema.registration, {
id: makeRegistrationId(ownedName, label, node),
})
.set({ expiryDate: expires });

await context.db
.update(schema.domain, { id: node })
Expand All @@ -152,11 +167,16 @@ export const makeRegistrarHandlers = (ownedName: `${string}eth`) => {

const label = tokenIdToLabel(tokenId);
const node = makeSubnodeNamehash(ownedSubnameNode, label);
const registrationId = makeRegistrationId(ownedName, label, node);

const registration = await context.db.find(schema.registration, { id: label });
const registration = await context.db.find(schema.registration, {
id: registrationId,
});
if (!registration) return;

await context.db.update(schema.registration, { id: label }).set({ registrantId: to });
await context.db
.update(schema.registration, { id: registrationId })
.set({ registrantId: to });

await context.db.update(schema.domain, { id: node }).set({ registrantId: to });

Expand Down
7 changes: 4 additions & 3 deletions apps/ensnode/src/handlers/Registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Context } from "ponder:registry";
import schema from "ponder:schema";
import { encodeLabelhash } from "@ensdomains/ensjs/utils";
import { ROOT_NODE, makeSubnodeNamehash } from "ensnode-utils/subname-helpers";
import type { Labelhash, Node } from "ensnode-utils/types";
import { type Hex, zeroAddress } from "viem";
import { sharedEventValues, upsertAccount, upsertResolver } from "../lib/db-helpers";
import { makeResolverId } from "../lib/ids";
Expand Down Expand Up @@ -114,7 +115,7 @@ export const handleNewOwner =
event,
}: {
context: Context;
event: EventWithArgs<{ node: Hex; label: Hex; owner: Hex }>;
event: EventWithArgs<{ node: Node; label: Labelhash; owner: Hex }>;
}) => {
const { label, node, owner } = event.args;

Expand Down Expand Up @@ -178,7 +179,7 @@ export async function handleNewTTL({
event,
}: {
context: Context;
event: EventWithArgs<{ node: Hex; ttl: bigint }>;
event: EventWithArgs<{ node: Node; ttl: bigint }>;
}) {
const { node, ttl } = event.args;

Expand All @@ -200,7 +201,7 @@ export async function handleNewResolver({
event,
}: {
context: Context;
event: EventWithArgs<{ node: Hex; resolver: Hex }>;
event: EventWithArgs<{ node: Node; resolver: Hex }>;
}) {
const { node, resolver: resolverAddress } = event.args;

Expand Down
31 changes: 21 additions & 10 deletions apps/ensnode/src/handlers/Resolver.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { type Context } from "ponder:registry";
import schema from "ponder:schema";
import type { Node } from "ensnode-utils/types";
import { Hex } from "viem";
import { sharedEventValues, upsertAccount, upsertResolver } from "../lib/db-helpers";
import { hasNullByte, uniq } from "../lib/helpers";
Expand All @@ -17,7 +18,7 @@ export async function handleAddrChanged({
event,
}: {
context: Context;
event: EventWithArgs<{ node: Hex; a: Hex }>;
event: EventWithArgs<{ node: Node; a: Hex }>;
}) {
const { a: address, node } = event.args;
await upsertAccount(context, address);
Expand Down Expand Up @@ -49,7 +50,7 @@ export async function handleAddressChanged({
event,
}: {
context: Context;
event: EventWithArgs<{ node: Hex; coinType: bigint; newAddress: Hex }>;
event: EventWithArgs<{ node: Node; coinType: bigint; newAddress: Hex }>;
}) {
const { node, coinType, newAddress } = event.args;
await upsertAccount(context, newAddress);
Expand Down Expand Up @@ -80,7 +81,7 @@ export async function handleNameChanged({
event,
}: {
context: Context;
event: EventWithArgs<{ node: Hex; name: string }>;
event: EventWithArgs<{ node: Node; name: string }>;
}) {
const { node, name } = event.args;
if (hasNullByte(name)) return;
Expand All @@ -105,7 +106,7 @@ export async function handleABIChanged({
event,
}: {
context: Context;
event: EventWithArgs<{ node: Hex; contentType: bigint }>;
event: EventWithArgs<{ node: Node; contentType: bigint }>;
}) {
const { node, contentType } = event.args;
const id = makeResolverId(event.log.address, node);
Expand All @@ -130,7 +131,7 @@ export async function handlePubkeyChanged({
event,
}: {
context: Context;
event: EventWithArgs<{ node: Hex; x: Hex; y: Hex }>;
event: EventWithArgs<{ node: Node; x: Hex; y: Hex }>;
}) {
const { node, x, y } = event.args;
const id = makeResolverId(event.log.address, node);
Expand All @@ -156,7 +157,12 @@ export async function handleTextChanged({
event,
}: {
context: Context;
event: EventWithArgs<{ node: Hex; indexedKey: string; key: string; value?: string }>;
event: EventWithArgs<{
node: Node;
indexedKey: string;
key: string;
value?: string;
}>;
}) {
const { node, key, value } = event.args;
const id = makeResolverId(event.log.address, node);
Expand Down Expand Up @@ -185,7 +191,7 @@ export async function handleContenthashChanged({
event,
}: {
context: Context;
event: EventWithArgs<{ node: Hex; hash: Hex }>;
event: EventWithArgs<{ node: Node; hash: Hex }>;
}) {
const { node, hash } = event.args;
const id = makeResolverId(event.log.address, node);
Expand All @@ -209,7 +215,7 @@ export async function handleInterfaceChanged({
event,
}: {
context: Context;
event: EventWithArgs<{ node: Hex; interfaceID: Hex; implementer: Hex }>;
event: EventWithArgs<{ node: Node; interfaceID: Hex; implementer: Hex }>;
}) {
const { node, interfaceID, implementer } = event.args;
const id = makeResolverId(event.log.address, node);
Expand All @@ -233,7 +239,12 @@ export async function handleAuthorisationChanged({
event,
}: {
context: Context;
event: EventWithArgs<{ node: Hex; owner: Hex; target: Hex; isAuthorised: boolean }>;
event: EventWithArgs<{
node: Node;
owner: Hex;
target: Hex;
isAuthorised: boolean;
}>;
}) {
const { node, owner, target, isAuthorised } = event.args;
const id = makeResolverId(event.log.address, node);
Expand All @@ -259,7 +270,7 @@ export async function handleVersionChanged({
event,
}: {
context: Context;
event: EventWithArgs<{ node: Hex; newVersion: bigint }>;
event: EventWithArgs<{ node: Node; newVersion: bigint }>;
}) {
// a version change nulls out the resolver
const { node, newVersion } = event.args;
Expand Down
42 changes: 42 additions & 0 deletions apps/ensnode/src/lib/ids.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { Labelhash, Node } from "ensnode-utils/types";
import type { Address, Hex } from "viem";

// NOTE: subgraph uses lowercase address here, viem provides us checksummed, so we lowercase it
Expand All @@ -10,3 +11,44 @@ export const makeEventId = (blockNumber: bigint, logIndex: number, transferIndex
[blockNumber.toString(), logIndex.toString(), transferIndex?.toString()]
.filter(Boolean)
.join("-");

/**
* Makes a cross-registrar unique registration ID.
*
* The ENS Subgraph has the potential to index "selected data" for any ENS name
* (ex: through the Registry or the NameWrapper). However, the "selected data"
* indexed by the ENS Subgraph varies depending on attributes of the name. For
* example, the ENS Subgraph only indexes Registration records for direct
* subnames of ".eth". This allows the ENS Subgraph to assign distinct
* Registration ids using only the labelhash of the direct subname being
* registered. (i.e. for the registration of "test.eth", the Registration's id
* is `labelhash('test'))`.
*
* ENSNode (with multiple plugins activated) indexes Registration records from
* multiple Registrars (like the base.eth and linea.eth Registrars). Therefore,
* we use this function to avoid Registration id collisions that would otherwise
* occur. (i.e. this function provides unique registration ids for "test.eth",
* "test.base.eth", and "test.linea.eth", etc.
*
* @param registrarName the name of the registrar issuing the registration
* @param labelHash the labelHash of the subname that was registered directly
* beneath `registrarName`
* @param node the node of the full name that was registered
* @returns a unique registration id
*/
export const makeRegistrationId = (registrarName: string, labelHash: Labelhash, node: Node) => {
if (registrarName === "eth") {
// For the "v1" of ENSNode (at a minimum) we want to preserve backwards
// compatibility with Registration id's issued by the ENS Subgraph.
// In the future we'll explore more fundamental solutions to avoiding
// Registration id collissions. For now are consciously mixing `labelHash`
// and `node` (namehash) values as registration ids. Both are keccak256
// hashes, so we take advantage of the odds of a collision being
// practically zero.
return labelHash;
} else {
// Avoid collisions between Registrations for the same direct subname from
// different Registrars.
return node;
}
};
4 changes: 3 additions & 1 deletion apps/ensnode/src/lib/plugin-helpers.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { OwnedName } from "./types";

/**
* A factory function that returns a function to create a namespaced contract
* name for Ponder indexing handlers.
Expand Down Expand Up @@ -88,7 +90,7 @@ type PluginNamespacePath<T extends PluginNamespacePath = "/"> =
* @param availablePlugins is a list of available plugins
* @returns the active plugins
*/
export function getActivePlugins<T extends { ownedName: string }>(
export function getActivePlugins<T extends { ownedName: OwnedName }>(
availablePlugins: readonly T[],
): T[] {
/** @var comma separated list of the requested plugin names (see `src/plugins` for available plugins) */
Expand Down
Loading

0 comments on commit bdfcd08

Please sign in to comment.