diff --git a/apps/ensindexer/src/ponder/config.ts b/apps/ensindexer/src/ponder/config.ts index 300a49995..220236a14 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 { buildIndexingBehaviorInjectionContracts } from "./indexing-behavior-injection-contract"; + //////// // Merge the active plugins' configs into a single ponder config. //////// @@ -19,29 +20,22 @@ const ponderConfig = activePlugins.reduce( {}, ) as AllPluginsMergedConfig; -// NOTE: here we inject all values from the ENSIndexerConfig that alter the indexing behavior of the +// NOTE: here we inject all values 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. -// +// 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" ->; +// +// Since Ponder Build ID is generated based on the value of +// the `contracts` field in `ponderConfig`, we inject +// a special "indexing behavior injection" contract here to make sure +// all indexing behavior dependencies are included and can drive the Ponder Build ID. +ponderConfig.contracts = Object.assign( + {}, + ponderConfig.contracts, + buildIndexingBehaviorInjectionContracts(), +); //////// // 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..a1e218dfe --- /dev/null +++ b/apps/ensindexer/src/ponder/indexing-behavior-injection-contract.ts @@ -0,0 +1,60 @@ +import config from "@/config"; + +import { getENSRootChainId } from "@ensnode/datasources"; + +import type { EnsIndexerConfig } from "@/config/types"; +import { ensDbClient } from "@/lib/ensdb/singleton"; + +/** + * Indexing Behavior Dependencies + * + * The following values are indexing behavior dependencies: + * - ENSDb: Schema checksum + * - Different ENSDb Schema definitions may influence indexing behavior. + * - ENSIndexer: config fields + * - `namespace` + * - Changes the datasources used for indexing, which influences the indexing behavior. + * - `plugins` + * - Can change the indexed chains and contracts, which influences the indexing behavior. + * - `globalBlockrange` + * - Changes the blockrange of indexed chains, which influences the indexing behavior. + * - `isSubgraphCompatible` + * - Changes the indexing logic, which influences the indexing behavior. + * - `labelSet` + * - Changes the label set used for healing labels during indexing, which influences the indexing behavior. + */ +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: ensDbClient.ensDbSchemaChecksum, +} satisfies Pick< + EnsIndexerConfig, + "namespace" | "plugins" | "globalBlockrange" | "isSubgraphCompatible" | "labelSet" +> & { ensDbSchemaChecksum: string }; + +// We use the root chain ID to build a minimal representation of a valid contract config for ENSIndexer. +const rootChainId = getENSRootChainId(config.namespace); + +/** + * Build a special "indexing behavior injection" contracts config for ENSIndexer + * to inject into `contracts` field of the Ponder config. The contracts config + * does not represent any real contracts to index, but rather serves as + * a container for all indexing behavior dependencies that should be reflected + * in the Ponder build ID. + */ +export function buildIndexingBehaviorInjectionContracts() { + return { + IndexingBehaviorInjectionContract: { + chain: `${rootChainId}`, + indexingBehaviorDependencies, + }, + } as const; +} diff --git a/packages/ensdb-sdk/src/client/ensdb-reader.ts b/packages/ensdb-sdk/src/client/ensdb-reader.ts index ac6b0b72e..bb677a1f6 100644 --- a/packages/ensdb-sdk/src/client/ensdb-reader.ts +++ b/packages/ensdb-sdk/src/client/ensdb-reader.ts @@ -13,6 +13,7 @@ import { buildIndividualEnsDbSchemas, type EnsDbDrizzleClient, type EnsNodeSchema, + getDrizzleSchemaChecksum, } from "../lib/drizzle"; import { EnsNodeMetadataKeys } from "./ensnode-metadata"; import type { @@ -94,6 +95,23 @@ export class EnsDbReader< return this.drizzleClient; } + /** + * Getter for the ENSDb Schema Checksum + * + * The Schema Checksum is a hash based on the "concrete" ENSIndexer Schema and + * the ENSNode Schema definitions used in the Drizzle client for ENSDb. + * + * It can be used to verify that the {@link ensDb} Drizzle client + * is using the expected schema definitions while interacting with + * the ENSDb instance. + */ + get ensDbSchemaChecksum(): string { + return getDrizzleSchemaChecksum({ + ...this._concreteEnsIndexerSchema, + ...this._ensNodeSchema, + }); + } + /** * Getter for the "concrete" ENSIndexer Schema definition used in the Drizzle client * for ENSDb instance. diff --git a/packages/ensdb-sdk/src/lib/drizzle.ts b/packages/ensdb-sdk/src/lib/drizzle.ts index 051955846..1f8a8a1eb 100644 --- a/packages/ensdb-sdk/src/lib/drizzle.ts +++ b/packages/ensdb-sdk/src/lib/drizzle.ts @@ -1,6 +1,9 @@ /** * Utilities for Drizzle ORM integration with ENSDb. */ + +import { createHash } from "node:crypto"; + import type { Logger as DrizzleLogger } from "drizzle-orm/logger"; import { drizzle, type NodePgDatabase } from "drizzle-orm/node-postgres"; import { isPgEnum } from "drizzle-orm/pg-core"; @@ -169,3 +172,39 @@ 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 - The Drizzle schema definition. + * @returns A 10-character checksum string for the schema. + */ +export function getDrizzleSchemaChecksum(schema: Record): string { + const stringifiedSchema = safeStringifyDrizzleSchema(schema); + + return createHash("sha256").update(stringifiedSchema).digest("hex").slice(0, 10); +}