Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
@@ -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`.
143 changes: 69 additions & 74 deletions apps/ensapi/src/lib/resolution/forward-resolution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
type Node,
namehashInterpretedName,
} from "enssdk";
import type { PublicClient } from "viem";

import { DatasourceNames, maybeGetDatasource } from "@ensnode/datasources";
import {
Expand All @@ -22,21 +23,25 @@ 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";
import { accelerateENSIP19ReverseResolver } from "@/lib/resolution/accelerate-ensip19-reverse-resolver";
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,
Expand All @@ -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<Operation[]> {
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.
*
Expand Down Expand Up @@ -158,34 +194,22 @@ async function _resolveForward<SELECTION extends ResolverRecordsSelection>(

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,
);
Comment thread
shrugs marked this conversation as resolved.

// when we cannot attempt acceleration or ENSv2 is deployed (temp), delegate to UniversalResolver
if (!canAttemptAcceleration || isENSv2Namespace) {
operations = await resolveViaUniversalResolver(name, operations, publicClient);
Comment thread
shrugs marked this conversation as resolved.
logOperations(operations, logger);
return makeRecordsResponse<SELECTION>(operations);
}
Expand Down Expand Up @@ -302,52 +326,23 @@ async function _resolveForward<SELECTION extends ResolverRecordsSelection>(
}

////////////////////////////////////////////////////////////////////////////
// 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<SELECTION>(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)) {
Expand All @@ -356,7 +351,7 @@ async function _resolveForward<SELECTION extends ResolverRecordsSelection>(
);
}

// return record values
// return records response from operations
logOperations(operations, logger);
return makeRecordsResponse<SELECTION>(operations);
},
Expand Down
Loading