From b132adec58f403532e60320caf896977f508c2ca Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Fri, 10 Apr 2026 16:59:43 +0200 Subject: [PATCH 1/5] Export `ENSDB_SCHEMA_CHECKSUM` const from ENSDb SDK The value of this const is a checksum that is unique for the ENSDb Schema definition. --- packages/ensdb-sdk/src/client/ensdb-config.ts | 20 +++++++ packages/ensdb-sdk/src/lib/checksum.test.ts | 57 +++++++++++++++++++ packages/ensdb-sdk/src/lib/checksum.ts | 12 ++++ packages/ensdb-sdk/src/lib/drizzle.ts | 38 +++++++++++++ 4 files changed, 127 insertions(+) create mode 100644 packages/ensdb-sdk/src/lib/checksum.test.ts create mode 100644 packages/ensdb-sdk/src/lib/checksum.ts diff --git a/packages/ensdb-sdk/src/client/ensdb-config.ts b/packages/ensdb-sdk/src/client/ensdb-config.ts index 148837e6f..c2a74e15e 100644 --- a/packages/ensdb-sdk/src/client/ensdb-config.ts +++ b/packages/ensdb-sdk/src/client/ensdb-config.ts @@ -1,3 +1,7 @@ +import * as abstractEnsIndexerSchema from "../ensindexer-abstract"; +import * as ensNodeSchema from "../ensnode"; +import { getDrizzleSchemaChecksum } from "../lib/drizzle"; + /** * ENSDb Config */ @@ -19,3 +23,19 @@ export interface EnsDbConfig { */ ensIndexerSchemaName: string; } + +/** + * ENSDb Schema Checksum + * + * Checksum representing the ENSDb Schema definition, which is a combination of + * - the ENSIndexer Schema definition, and + * - the ENSNode Schema definition. + * + * This checksum can be used to verify compatibility between + * the ENSDb Schema definition expected by any client app connecting to ENSDb + * instance and the actual ENSDb Schema definition present in ENSDb SDK. + */ +export const ENSDB_SCHEMA_CHECKSUM = getDrizzleSchemaChecksum({ + ...abstractEnsIndexerSchema, + ...ensNodeSchema, +}); diff --git a/packages/ensdb-sdk/src/lib/checksum.test.ts b/packages/ensdb-sdk/src/lib/checksum.test.ts new file mode 100644 index 000000000..6e5e19dec --- /dev/null +++ b/packages/ensdb-sdk/src/lib/checksum.test.ts @@ -0,0 +1,57 @@ +import { describe, expect, it } from "vitest"; + +import { createChecksum } from "./checksum"; + +describe("createChecksum", () => { + it("returns a 10-character hex string", () => { + const checksum = createChecksum("test data"); + + expect(checksum).toHaveLength(10); + expect(checksum).toMatch(/^[a-f0-9]{10}$/); + }); + + it("returns consistent results for the same input", () => { + const input = "consistent input"; + const checksum1 = createChecksum(input); + const checksum2 = createChecksum(input); + + expect(checksum1).toBe(checksum2); + }); + + it("returns different results for different inputs", () => { + const checksum1 = createChecksum("input one"); + const checksum2 = createChecksum("input two"); + + expect(checksum1).not.toBe(checksum2); + }); + + it("handles empty string input", () => { + const checksum = createChecksum(""); + + expect(checksum).toHaveLength(10); + expect(checksum).toMatch(/^[a-f0-9]{10}$/); + }); + + it("handles Buffer input", () => { + const buffer = Buffer.from("buffer data"); + const checksum = createChecksum(buffer); + + expect(checksum).toHaveLength(10); + expect(checksum).toMatch(/^[a-f0-9]{10}$/); + }); + + it("handles Uint8Array input", () => { + const uint8Array = new Uint8Array([1, 2, 3, 4, 5]); + const checksum = createChecksum(uint8Array); + + expect(checksum).toHaveLength(10); + expect(checksum).toMatch(/^[a-f0-9]{10}$/); + }); + + it("produces expected checksum for known input", () => { + // SHA-256 of "hello" starts with "2cf24dba5f..." + const checksum = createChecksum("hello"); + + expect(checksum).toBe("2cf24dba5f"); + }); +}); diff --git a/packages/ensdb-sdk/src/lib/checksum.ts b/packages/ensdb-sdk/src/lib/checksum.ts new file mode 100644 index 000000000..43a3ffabc --- /dev/null +++ b/packages/ensdb-sdk/src/lib/checksum.ts @@ -0,0 +1,12 @@ +import { type BinaryLike, createHash } from "node:crypto"; + +/** + * Create a checksum for the given data + * + * @param data - The data to create a checksum for + * @returns A 10-character hash string representing the checksum of the data + * + */ +export function createChecksum(data: BinaryLike): string { + return createHash("sha256").update(data).digest("hex").slice(0, 10); +} diff --git a/packages/ensdb-sdk/src/lib/drizzle.ts b/packages/ensdb-sdk/src/lib/drizzle.ts index 051955846..cbcd1f2b5 100644 --- a/packages/ensdb-sdk/src/lib/drizzle.ts +++ b/packages/ensdb-sdk/src/lib/drizzle.ts @@ -12,6 +12,7 @@ import { isTable, Table } from "drizzle-orm/table"; // directly to build a Drizzle client for ENSDb. import * as abstractEnsIndexerSchema from "../ensindexer-abstract"; import * as ensNodeSchema from "../ensnode"; +import { createChecksum } from "./checksum"; /** * Abstract ENSIndexer Schema @@ -169,3 +170,40 @@ export function buildEnsDbDrizzleClient): string { + const seen = new WeakSet(); + + return JSON.stringify(schema, (_key, value) => { + if (typeof value === "object" && value !== null) { + if (seen.has(value)) return "[circular]"; + seen.add(value); + } + + return value; + }); +} + +/** + * Get a checksum for a Drizzle schema definition. + * + * @param schema - A Drizzle schema definition to get the checksum for. + * @returns A 10-character checksum string for the schema. + */ +export function getDrizzleSchemaChecksum(schema: Record): string { + const stringifiedSchema = safeStringifyDrizzleSchema(schema); + + return createChecksum(stringifiedSchema); +} From 282dc83386fbf1b409d93a6d41ac2ceb51e72b06 Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Fri, 10 Apr 2026 17:14:48 +0200 Subject: [PATCH 2/5] Make ENSIndexer to inject a special `IndexingBehaviorInjectionContract` config into Ponder Config This change integrates values from `indexingBehaviorDependencies` object to influence the Ponder Build ID. --- apps/ensindexer/src/ponder/config.ts | 40 +++--- .../indexing-behavior-injection-contract.ts | 124 ++++++++++++++++++ 2 files changed, 140 insertions(+), 24 deletions(-) create mode 100644 apps/ensindexer/src/ponder/indexing-behavior-injection-contract.ts diff --git a/apps/ensindexer/src/ponder/config.ts b/apps/ensindexer/src/ponder/config.ts index 300a49995..1df00d1b6 100644 --- a/apps/ensindexer/src/ponder/config.ts +++ b/apps/ensindexer/src/ponder/config.ts @@ -1,9 +1,10 @@ import config from "@/config"; -import type { ENSIndexerConfig } from "@/config/types"; import { mergePonderConfigs } from "@/lib/merge-ponder-configs"; import { ALL_PLUGINS, type AllPluginsMergedConfig } from "@/plugins"; +import { IndexingBehaviorInjectionContract } from "./indexing-behavior-injection-contract"; + //////// // Merge the active plugins' configs into a single ponder config. //////// @@ -19,29 +20,20 @@ const ponderConfig = activePlugins.reduce( {}, ) as AllPluginsMergedConfig; -// NOTE: here we inject all values from the ENSIndexerConfig that alter the indexing behavior of the -// Ponder config in order to alter the ponder-generated build id when these options change. -// -// This ensures that running ENSIndexer with different configurations maintains compatibility with -// Ponder's default crash recovery behavior. -// -// https://ponder.sh/docs/api-reference/ponder/database#build-id-and-crash-recovery -(ponderConfig as any).indexingBehaviorDependencies = { - // while technically not necessary, since these configuration properties are reflected in the - // generated ponderConfig, we include them here for clarity - namespace: config.namespace, - plugins: config.plugins, - globalBlockrange: config.globalBlockrange, - - // these config properties don't explicitly affect the generated ponderConfig and need to be - // injected here to ensure that, if they are configured differently, ponder generates a unique - // build id to differentiate between runs with otherwise-identical configs (see above). - isSubgraphCompatible: config.isSubgraphCompatible, - labelSet: config.labelSet, -} satisfies Pick< - ENSIndexerConfig, - "namespace" | "plugins" | "globalBlockrange" | "isSubgraphCompatible" | "labelSet" ->; +/** + * NOTE: By injecting the {@link IndexingBehaviorInjectionContract} into + * the `contracts` field of the Ponder Config, we ensure that any changes to + * the indexing behavior dependencies defined in + * {@link IndexingBehaviorInjectionContract.indexingBehaviorDependencies} will + * result in a different Ponder Build ID. This ensures that running ENSIndexer + * with different configurations maintains compatibility with Ponder's default + * crash recovery behavior. + * + * @see https://ponder.sh/docs/api-reference/ponder/database#build-id-and-crash-recovery + */ +ponderConfig.contracts = Object.assign({}, ponderConfig.contracts, { + IndexingBehaviorInjectionContract, +}); //////// // Set indexing order strategy diff --git a/apps/ensindexer/src/ponder/indexing-behavior-injection-contract.ts b/apps/ensindexer/src/ponder/indexing-behavior-injection-contract.ts new file mode 100644 index 000000000..01cf33a70 --- /dev/null +++ b/apps/ensindexer/src/ponder/indexing-behavior-injection-contract.ts @@ -0,0 +1,124 @@ +import config from "@/config"; + +import type { ContractConfig } from "ponder"; + +import { getENSRootChainId } from "@ensnode/datasources"; +import { ENSDB_SCHEMA_CHECKSUM } from "@ensnode/ensdb-sdk"; + +import type { EnsIndexerConfig } from "@/config/types"; + +/** + * Indexing Behavior Dependencies + * + * Defines all values that influence the indexing behavior of the ENSIndexer + * instance. + */ +interface IndexingBehaviorDependencies { + /** + * ENS Namespace + * + * When `namespace` changes, the datasources used for indexing may change, + * which influences the indexing behavior. + */ + namespace: string; + + /** + * ENSIndexer Plugins + * + * When `plugins` change, the indexed chains and contracts may change, + * which influences the indexing behavior. + */ + plugins: EnsIndexerConfig["plugins"]; + + /** + * Global Blockrange + * + * When `globalBlockrange` changes, the blockrange of indexed chains may change, + * which influences the indexing behavior. + */ + globalBlockrange: EnsIndexerConfig["globalBlockrange"]; + + /** + * Subgraph Compatibility + * + * When `isSubgraphCompatible` changes, the indexing logic may change, + * which influences the indexing behavior. + */ + isSubgraphCompatible: boolean; + + /** + * Label Set + * + * When `labelSet` changes, the label "healing" results may change during indexing, + * which influences the indexing behavior. + */ + labelSet: EnsIndexerConfig["labelSet"]; + + /** + * ENSDb Schema Checksum + * + * When `ensDbSchemaChecksum` changes, the ENSDb schema definition may have + * changed, which influences the indexing behavior. + */ + ensDbSchemaChecksum: string; +} + +/** + * A special "indexing behavior injection" contract config + * + * This config does not reference any real onchain contract to be indexed. + * Instead, it serves as a placeholder to collect all values that influence + * the indexing behavior of the ENSIndexer instance. + * + * This contract config is designed to be injected into the `contracts` field + * of the Ponder Config object. + */ +interface IndexingBehaviorInjectionContractConfig extends ContractConfig { + indexingBehaviorDependencies: IndexingBehaviorDependencies; +} + +/** + * Build a contract config placeholder with the necessary fields to be included in + * the `contracts` field of the Ponder Config. + */ +function buildContractConfigPlaceholder(): ContractConfig { + return { + // The placeholder contract does not reference any real chain, + // but we need to provide a valid chain id to satisfy the ContractConfig type. + // The ENS Root Chain ID is a reasonable choice since it's guaranteed to be + // a valid indexed chain ID for any ENSIndexer instance. + chain: `${getENSRootChainId(config.namespace)}`, + // The placeholder contract does not have any real ABI, + // but we need to provide an empty array to satisfy the ContractConfig type. + abi: [], + }; +} + +/** + * Indexing Behavior Dependencies + */ +const indexingBehaviorDependencies = { + // while technically not necessary, since these configuration properties are reflected in the + // generated ponderConfig, we include them here for clarity + namespace: config.namespace, + plugins: config.plugins, + globalBlockrange: config.globalBlockrange, + // these config properties don't explicitly affect the generated ponderConfig and need to be + // injected here to ensure that, if they are configured differently, ponder generates a unique + // build id to differentiate between runs with otherwise-identical configs (see above). + isSubgraphCompatible: config.isSubgraphCompatible, + labelSet: config.labelSet, + ensDbSchemaChecksum: ENSDB_SCHEMA_CHECKSUM, +} satisfies IndexingBehaviorDependencies; + +/** + * A special "indexing behavior injection" contract config + * + * This config is designed to be injected into the `contracts` field of + * the Ponder Config object in order to make Ponder create + * a unique build ID for any changes to {@link indexingBehaviorDependencies}. + */ +export const IndexingBehaviorInjectionContract = { + ...buildContractConfigPlaceholder(), + indexingBehaviorDependencies, +} satisfies IndexingBehaviorInjectionContractConfig; From 192945fc4ff8d561fb997ffe4b174bac7d372194 Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Fri, 10 Apr 2026 17:22:56 +0200 Subject: [PATCH 3/5] docs(changeset): Exported `ENSDB_SCHEMA_CHECKSUM` const which changes when ENSDb Schema change. --- .changeset/purple-chefs-tan.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/purple-chefs-tan.md diff --git a/.changeset/purple-chefs-tan.md b/.changeset/purple-chefs-tan.md new file mode 100644 index 000000000..9b1dc514b --- /dev/null +++ b/.changeset/purple-chefs-tan.md @@ -0,0 +1,5 @@ +--- +"@ensnode/ensdb-sdk": minor +--- + +Exported `ENSDB_SCHEMA_CHECKSUM` const which changes when ENSDb Schema definition changes. From e381726bc6f8902d2527cc323759cb35e90b543c Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Fri, 10 Apr 2026 17:26:15 +0200 Subject: [PATCH 4/5] docs(changeset): Updated Ponder Config object to include values that indexing behavior depends on. This is to ensure that the Ponder Build ID changes when any value in indexing behaviour dependencies changes. --- .changeset/few-chefs-bathe.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/few-chefs-bathe.md diff --git a/.changeset/few-chefs-bathe.md b/.changeset/few-chefs-bathe.md new file mode 100644 index 000000000..cbb5fa218 --- /dev/null +++ b/.changeset/few-chefs-bathe.md @@ -0,0 +1,5 @@ +--- +"ensindexer": minor +--- + +Updated Ponder Config object to include values that indexing behavior depends on. This is to ensure that the Ponder Build ID changes when any value in indexing behavior dependencies changes. From deb0b7e551512563e793c6f9456df468d7551cb9 Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Sat, 11 Apr 2026 07:44:48 +0200 Subject: [PATCH 5/5] Apply PR feedback --- apps/ensindexer/src/ponder/config.ts | 8 ++++++-- .../src/ponder/indexing-behavior-injection-contract.ts | 9 +++++++-- packages/ensdb-sdk/src/lib/drizzle.ts | 6 ++++++ 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/apps/ensindexer/src/ponder/config.ts b/apps/ensindexer/src/ponder/config.ts index 1df00d1b6..68029bbf3 100644 --- a/apps/ensindexer/src/ponder/config.ts +++ b/apps/ensindexer/src/ponder/config.ts @@ -31,9 +31,13 @@ const ponderConfig = activePlugins.reduce( * * @see https://ponder.sh/docs/api-reference/ponder/database#build-id-and-crash-recovery */ -ponderConfig.contracts = Object.assign({}, ponderConfig.contracts, { +ponderConfig.contracts = { + ...ponderConfig.contracts, + // @ts-expect-error - `ponderConfig.contracts` is a constant type, so the type system + // doesn't allow us to add new properties to it, but we have to inject the + // IndexingBehaviorInjectionContract here. IndexingBehaviorInjectionContract, -}); +}; //////// // Set indexing order strategy diff --git a/apps/ensindexer/src/ponder/indexing-behavior-injection-contract.ts b/apps/ensindexer/src/ponder/indexing-behavior-injection-contract.ts index 01cf33a70..311f3a2ae 100644 --- a/apps/ensindexer/src/ponder/indexing-behavior-injection-contract.ts +++ b/apps/ensindexer/src/ponder/indexing-behavior-injection-contract.ts @@ -98,10 +98,15 @@ function buildContractConfigPlaceholder(): ContractConfig { * Indexing Behavior Dependencies */ const indexingBehaviorDependencies = { - // while technically not necessary, since these configuration properties are reflected in the + // while technically not necessary, since these config properties are reflected in the // generated ponderConfig, we include them here for clarity namespace: config.namespace, - plugins: config.plugins, + // Sort plugins to ensure canonical checksum regardless of config order. + // The actual indexing behavior does not depend on plugin order since: + // 1. All plugin checks use Array.includes() which is order-independent + // 2. Plugin execution order is determined by `ALL_PLUGINS`, not config.plugins + // Sorting ensures consistent Build IDs for semantically identical config. + plugins: [...config.plugins].sort(), globalBlockrange: config.globalBlockrange, // these config properties don't explicitly affect the generated ponderConfig and need to be // injected here to ensure that, if they are configured differently, ponder generates a unique diff --git a/packages/ensdb-sdk/src/lib/drizzle.ts b/packages/ensdb-sdk/src/lib/drizzle.ts index cbcd1f2b5..3431660c9 100644 --- a/packages/ensdb-sdk/src/lib/drizzle.ts +++ b/packages/ensdb-sdk/src/lib/drizzle.ts @@ -179,6 +179,12 @@ export function buildEnsDbDrizzleClient