diff --git a/.changeset/forward-resolution-delegate-universal-resolver.md b/.changeset/forward-resolution-delegate-universal-resolver.md new file mode 100644 index 000000000..c14d96b71 --- /dev/null +++ b/.changeset/forward-resolution-delegate-universal-resolver.md @@ -0,0 +1,5 @@ +--- +"ensapi": patch +--- + +Forward Resolution now fully delegates to the `UniversalResolver` whenever records cannot be accelerated, correctly implementing the [ENSv2-Readiness](https://docs.ens.domains/web/ensv2-readiness/) check for `ur.integration-test.eth`. Unaccelerated requests are always delegated to the `UniversalResolver`. diff --git a/apps/ensapi/src/lib/resolution/forward-resolution.ts b/apps/ensapi/src/lib/resolution/forward-resolution.ts index 02bf83a5a..d5f12cec3 100644 --- a/apps/ensapi/src/lib/resolution/forward-resolution.ts +++ b/apps/ensapi/src/lib/resolution/forward-resolution.ts @@ -8,6 +8,7 @@ import { type Node, namehashInterpretedName, } from "enssdk"; +import type { PublicClient } from "viem"; import { DatasourceNames, maybeGetDatasource } from "@ensnode/datasources"; import { @@ -22,13 +23,12 @@ import { } from "@ensnode/ensnode-sdk"; import { isBridgedResolver, - isExtendedResolver, isKnownENSIP19ReverseResolver, isStaticResolver, } from "@ensnode/ensnode-sdk/internal"; import di from "@/di"; -import { withActiveSpanAsync, withSpanAsync } from "@/lib/instrumentation/auto-span"; +import { withActiveSpanAsync } from "@/lib/instrumentation/auto-span"; import { makeLogger } from "@/lib/logger"; import { findResolver } from "@/lib/protocol-acceleration/find-resolver"; import { areResolverRecordsIndexedByProtocolAccelerationPluginOnChainId } from "@/lib/protocol-acceleration/resolver-records-indexed-on-chain"; @@ -36,7 +36,12 @@ import { accelerateENSIP19ReverseResolver } from "@/lib/resolution/accelerate-en import { accelerateKnownOnchainStaticResolver } from "@/lib/resolution/accelerate-known-onchain-static-resolver"; import { executeOperations } from "@/lib/resolution/execute-operations"; import { makeRecordsResponse } from "@/lib/resolution/make-records-response"; -import { isOperationResolved, logOperations, makeOperations } from "@/lib/resolution/operations"; +import { + isOperationResolved, + logOperations, + makeOperations, + type Operation, +} from "@/lib/resolution/operations"; import { addEnsProtocolStepEvent, withEnsProtocolStep, @@ -45,6 +50,37 @@ import { const logger = makeLogger("forward-resolution"); const tracer = trace.getTracer("forward-resolution"); +/** + * Resolves `operations` by delegating wholesale to the UniversalResolver's ENSIP-10 `resolve()` on the + * ENS Root Chain. The UniversalResolver performs findResolver + ENSIP-10 + CCIP-Read on-chain, so this + * is the protocol-faithful path whenever ENSApi is not accelerating from indexed data. + */ +async function resolveViaUniversalResolver( + name: InterpretedName, + operations: Operation[], + publicClient: PublicClient, +): Promise { + const universalResolver = getDatasourceContract( + di.context.namespace, + DatasourceNames.ENSRoot, + "UniversalResolver", + ); + + return withEnsProtocolStep( + TraceableENSProtocol.ForwardResolution, + ForwardResolutionProtocolStep.ExecuteResolveCalls, + {}, + () => + executeOperations({ + name, + resolverAddress: universalResolver.address, + operations, + publicClient, + useENSIP10Resolve: true, + }), + ); +} + /** * Implements Forward Resolution of record values for a specified ENS Name. * @@ -158,34 +194,22 @@ async function _resolveForward( const publicClient = di.context.rootChainPublicClient; - //////////////////////////// - /// 0. Temporary ENSv2 Bailout - //////////////////////////// - // TODO: re-enable protocol acceleration for ENSv2 - // NOTE: gate on the namespace containing an ENSv2Root datasource rather than the ENSv2 - // plugin being configured — a namespace may be ENSv1-only even when the Unigraph plugin is - // defined, and forward resolution must follow the ENSv1 path in that case. - if (maybeGetDatasource(di.context.namespace, DatasourceNames.ENSv2Root)) { - const universalResolver = getDatasourceContract( - di.context.namespace, - DatasourceNames.ENSRoot, - "UniversalResolver", - ); + //////////////////////////////////////////////////////////////// + /// 0 Non-Accelerated Resolution: delegate to UniversalResolver + //////////////////////////////////////////////////////////////// - operations = await withEnsProtocolStep( - TraceableENSProtocol.ForwardResolution, - ForwardResolutionProtocolStep.ExecuteResolveCalls, - {}, - () => - executeOperations({ - name, - resolverAddress: universalResolver.address, - operations, - publicClient, - useENSIP10Resolve: true, - }), - ); + // whether we can attempt to accelerate this resolution request + const canAttemptAcceleration = accelerate && canAccelerate; + + // TODO: re-enable protocol acceleration for ENSv2 + const isENSv2Namespace = !!maybeGetDatasource( + di.context.namespace, + DatasourceNames.ENSv2Root, + ); + // when we cannot attempt acceleration or ENSv2 is deployed (temp), delegate to UniversalResolver + if (!canAttemptAcceleration || isENSv2Namespace) { + operations = await resolveViaUniversalResolver(name, operations, publicClient); logOperations(operations, logger); return makeRecordsResponse(operations); } @@ -302,52 +326,23 @@ async function _resolveForward( } //////////////////////////////////////////////////////////////////////////// - // 4. Determine Resolver ENSIP-10 support + requirement. - // From here, we MUST execute EVM code to be compliant with ENS Protocol + // 4. Resolve remaining operations via RPC //////////////////////////////////////////////////////////////////////////// - const extended = await withEnsProtocolStep( - TraceableENSProtocol.ForwardResolution, - ForwardResolutionProtocolStep.RequireResolver, - { chainId, activeResolver, requiresWildcardSupport }, - async (stepSpan) => { - const extended = await withSpanAsync( - tracer, - "isExtendedResolver", - { chainId, address: activeResolver }, - () => isExtendedResolver({ address: activeResolver, publicClient }), - ); - - stepSpan.setAttribute("isExtendedResolver", extended); - - return extended; - }, - ); - - // if we require wildcard support and this is NOT an extended resolver, the resolver is - // not valid, i.e. there is no active resolver for the name - // https://docs.ens.domains/ensip/10/#specification - if (requiresWildcardSupport && !extended) { - return makeRecordsResponse(operations); - } - /////////////////////////////////////////// - // 5. Resolve remaining operations via RPC - /////////////////////////////////////////// - operations = await withEnsProtocolStep( - TraceableENSProtocol.ForwardResolution, - ForwardResolutionProtocolStep.ExecuteResolveCalls, - {}, - () => - executeOperations({ - name, - resolverAddress: activeResolver, - // NOTE: ENSIP-10 specifies that if a resolver supports IExtendedResolver, - // the client MUST use the ENSIP-10 resolve() method over the legacy methods. - useENSIP10Resolve: extended, - operations, - publicClient, - }), - ); + // On the ENS Root Chain, we have access to the UniversalResolver, so delegate to it + // rather than calling the discovered resolver directly. + // + // The reason for this is that a resolver's on-chain behavior can depend on being + // called by the canonical UniversalResolver. for example the URTestResolver gates + // IExtendedResolver support on `msg.sender == UniversalResolver.implementation()` — which + // ENSApi cannot reproduce off-chain. Delegating keeps Root Chain resolution 1:1 with + // the on-chain UniversalResolver. + // + // Finally, if we are resolving on a shadow Registry chain (e.g. Basenames/Lineanames) for + // which we have recursed into _resolveForward AND the operations were not resolved above, + // then we need to execute EVM code, for which calling the UniversalResolver is also the + // correct approach. + operations = await resolveViaUniversalResolver(name, operations, publicClient); // Invariant: all operations must be resolved if (!operations.every(isOperationResolved)) { @@ -356,7 +351,7 @@ async function _resolveForward( ); } - // return record values + // return records response from operations logOperations(operations, logger); return makeRecordsResponse(operations); },