You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This is only a research umbrella issue to analyze complexity and learn about ENS and erc-3668
Blockbook's ENS support (bchain/coins/eth/ethrpc.go) is a generation behind modern ENS. It does a manual namehash -> registry.resolver(bytes32) -> resolver.addr(bytes32) walk, mainnet-only (ensContracts() gates on MainNetChainID == MainNet), ETH-address only. It cannot resolve any offchain / L2 name (*.base.eth, *.linea.eth, *.cb.id, *.uni.eth, most subnames), has no wildcard (ENSIP-10) support, no reverse resolution, no text/contenthash/multicoin records, and no name normalization (unicode/emoji names mis-hash).
The modern resolution path is ERC-3668 CCIP-Read through the ENS UniversalResolver. Reference infra for the proof-verified variant: unruggable/gateways (@unruggable/gateways, MIT), which also demonstrates the ENSIP-10 wildcard resolve(bytes,bytes) flow, multicoin/text/contenthash dispatch, and reverse resolution.
Related: #1109 (reverse resolution shows a wrong/expired primary name), #1622 (ENS aliases unvalidated / not reverted on reorg).
Note: this epic was revised after a deep adversarial review (13 agents; every on-chain claim reproduced against a live mainnet archive node). The review corrected a factually wrong premise in the original trust framing, surfaced a missing mandatory ENSIP-21 batch-gateway layer, and found that #1109 would not actually be fixed by the original plan. The sections below are the corrected version.
Trust model: trust-minimized where the resolver uses a proof verifier; trusted-signer elsewhere
The mechanism this epic buys is verification by execution: because the ERC-3668 callback is performed as an eth_call to sender, the resolver's own callback runs inside our node's EVM, so whatever that resolver checks is checked by the EVM and never by our own parsing. That claim is correct and load-bearing — it was confirmed against four UniversalResolver generations and against CCIPBatcher.ccipBatchCallback / AbstractUniversalResolver._callResolver.
What it does NOT buy is blanket trustlessness. Two distinct resolver classes exist today and both go through the same ERC-3668 loop:
PROOF-VERIFIED — *.linea.eth (resolver 0x1507ce94…, verifier() = 0x9a4b070a…LineaSparseProofVerifier), plus the L1 ENSIP-19 Base/Optimism/Linea/Scroll reverse resolvers in ens-contracts/deployments/mainnet.
TRUSTED-SIGNER (ENS OffchainResolver + SignatureVerifier; owner-rotatable ECDSA key, no verifier contract) — *.base.eth (Coinbase; the resolver's owner() is a plain EOA that can addSigners), *.cb.id (CoinbaseResolver, upgradeable proxy), *.uni.eth (Uniswap).
So, precisely: for a proof-verifying resolver the gateway cannot fabricate a value — it can only serve values provable against a commit already on L1; it can still withhold, and can choose any commit inside the verifier's staleness window. For a trusted-signer resolver the gateway and the signer are the same party and it can lie outright.
Consequences for implementation:
The callback MUST be an eth_call. Never decode and trust gateway-returned bytes directly. That is the line between preserving the resolver's guarantee and discarding it.
No cross-chain RPC wiring for the mainnet instance. The L2 state root is already committed on L1, so resolution runs entirely against the existing L1 RPC — the ethereum instance never dials an L2. Realistic cost is >=2 eth_calls + >=1 gateway fetch per offchain hop (initial call reverts OffchainLookup -> batch-gateway round trip -> callback eth_call, inside which the resolver's callback runs via staticcall). Size recursion depth, timeouts and budgets from that, not from a single fetch. (Only the L2 instances need an L1 endpoint — ENS: L2-native resolution on L2 coins (Base/OP/Arbitrum/Polygon) #1668.)
Blockbook writes no proof code. Verification is per-chain and already audited: MPT for eth/OP/Arbitrum, Mimc sparse tree for Linea, Poseidon for Scroll, Blake2S SMT for zkSync, Pedersen for Starknet. eth_call supports all of them — including chains added after we ship. A Go reimplementation would cover MPT only and would not improve the trust model (our anchor is our own L1 node either way); see ENS: optional Go-side MPT proof cross-check (defense-in-depth, not the primary mechanism) #1670.
⚠️ OPEN DECISION — how to treat trusted-signer resolvers (owner: #1669)
This must be settled before #1673 is implemented, because it decides that issue's acceptance criteria.
Option A — Trustless-only by default. Only proof-verified resolvers resolve; trusted-signer requires an explicit opt-in flag. Strongest guarantee, but *.base.eth / *.cb.id / *.uni.eth — the names users actually type — fail out of the box.
Option B — Resolve + label the trust level. Resolve everything, but classify and expose trust=proof-verified vs trust=trusted-signer on the result. Users get the names they expect; honesty via labelling rather than refusal, at the cost of "trustless" being a property of some results rather than the feature.
Either way, unchecked/unknown is never accepted and the trust class is surfaced, not flattened.
eth_call gas cap (ENS: ERC-3668 CCIP-Read transport + ENSIP-21 batch gateway #1664) — Solidity trie verification is gas-heavy; an out-of-gas callback is indistinguishable from a verification failure unless explicitly classified. Empty revert data is the OOG signal. Note InvalidProof() is not what an MPT failure looks like: EthVerifierHooks never reverts it (real failures are Error(string) from MerkleTrie.sol); a client matching that selector would match nothing and fall through to "unknown error" — the fix(eth): correct ENS nameExpires selector and skip expiration when unreachable #1657 degradation class.
Adjacent, not covered by the children above but required for #1109 to actually close: #1622 (validation + reorg-safety of the NameRegistered alias harvest).
Trust & safety guardrails (apply throughout)
Do the ERC-3668 callback as an eth_call to sender — never trust gateway JSON directly.
Validate the revert's sender equals the address called (else the callback can be redirected to an attacker-chosen target).
Treat gateway URLs as attacker-controlled: https-only for real URLs plus the allowlisted x-batch-gateway: sentinel (handled internally, never fetched); SSRF-block private/link-local/metadata destinations; cap redirects and total fetches per resolution.
Cap the gateway response at ~400–500KB — it roughly doubles as hex inside the JSON-RPC request body, and the node front-end's limit binds first (measured: nginx default 1 MiB returns HTTP 413 with an HTML body and no JSON-RPC error object).
HTTP client: a package-level *http.Client with an explicit Timeout, mirroring feeHTTPClient (bchain/coins/eth/alternativefeeprovider.go:29); leave Transport nil so ProxyFromEnvironment is honored; bound the body with io.LimitReader. (There is no repo-wide proxy helper; server/timeouts.go is inbound http.Server constants and does not apply.)
Metrics & docs: counters in common/metrics.go for gateway fetch count/latency/failure, CCIP hop depth, verifier-class rejections, and the resolution-outcome taxonomy; plus a docs/ page covering the new outbound HTTPS egress requirement and the trust model. Fine to fold into the implementing PRs.
Estimated size
~2.3k–3.2k LOC across #1662–#1670 + #1672–#1673 (production + tests + per-coin boilerplate), revised upward from the original ~1.6k–2.3k after the review added the ENSIP-21 batch gateway (~150–250), the entry-point predicate, expiry/precedence work, and acceptance tests. Excludes#1671 (the ENSIP-15 normalizer, ~1.5k–3k, overwhelmingly generated tables — vendor it) and #1622.
This is only a research umbrella issue to analyze complexity and learn about ENS and erc-3668
Blockbook's ENS support (
bchain/coins/eth/ethrpc.go) is a generation behind modern ENS. It does a manualnamehash -> registry.resolver(bytes32) -> resolver.addr(bytes32)walk, mainnet-only (ensContracts()gates onMainNetChainID == MainNet), ETH-address only. It cannot resolve any offchain / L2 name (*.base.eth,*.linea.eth,*.cb.id,*.uni.eth, most subnames), has no wildcard (ENSIP-10) support, no reverse resolution, no text/contenthash/multicoin records, and no name normalization (unicode/emoji names mis-hash).The modern resolution path is ERC-3668 CCIP-Read through the ENS UniversalResolver. Reference infra for the proof-verified variant: unruggable/gateways (
@unruggable/gateways, MIT), which also demonstrates the ENSIP-10 wildcardresolve(bytes,bytes)flow, multicoin/text/contenthash dispatch, and reverse resolution.Related: #1109 (reverse resolution shows a wrong/expired primary name), #1622 (ENS aliases unvalidated / not reverted on reorg).
Trust model: trust-minimized where the resolver uses a proof verifier; trusted-signer elsewhere
The mechanism this epic buys is verification by execution: because the ERC-3668 callback is performed as an
eth_calltosender, the resolver's own callback runs inside our node's EVM, so whatever that resolver checks is checked by the EVM and never by our own parsing. That claim is correct and load-bearing — it was confirmed against four UniversalResolver generations and againstCCIPBatcher.ccipBatchCallback/AbstractUniversalResolver._callResolver.What it does NOT buy is blanket trustlessness. Two distinct resolver classes exist today and both go through the same ERC-3668 loop:
*.linea.eth(resolver0x1507ce94…,verifier()=0x9a4b070a…LineaSparseProofVerifier), plus the L1 ENSIP-19 Base/Optimism/Linea/Scroll reverse resolvers inens-contracts/deployments/mainnet.OffchainResolver+SignatureVerifier; owner-rotatable ECDSA key, no verifier contract) —*.base.eth(Coinbase; the resolver'sowner()is a plain EOA that canaddSigners),*.cb.id(CoinbaseResolver, upgradeable proxy),*.uni.eth(Uniswap).So, precisely: for a proof-verifying resolver the gateway cannot fabricate a value — it can only serve values provable against a commit already on L1; it can still withhold, and can choose any commit inside the verifier's staleness window. For a trusted-signer resolver the gateway and the signer are the same party and it can lie outright.
Consequences for implementation:
eth_call. Never decode and trust gateway-returned bytes directly. That is the line between preserving the resolver's guarantee and discarding it.ethereuminstance never dials an L2. Realistic cost is >=2eth_calls + >=1 gateway fetch per offchain hop (initial call revertsOffchainLookup-> batch-gateway round trip -> callbacketh_call, inside which the resolver's callback runs viastaticcall). Size recursion depth, timeouts and budgets from that, not from a single fetch. (Only the L2 instances need an L1 endpoint — ENS: L2-native resolution on L2 coins (Base/OP/Arbitrum/Polygon) #1668.)eth_callsupports all of them — including chains added after we ship. A Go reimplementation would cover MPT only and would not improve the trust model (our anchor is our own L1 node either way); see ENS: optional Go-side MPT proof cross-check (defense-in-depth, not the primary mechanism) #1670.This must be settled before #1673 is implemented, because it decides that issue's acceptance criteria.
*.base.eth/*.cb.id/*.uni.eth— the names users actually type — fail out of the box.trust=proof-verifiedvstrust=trusted-signeron the result. Users get the names they expect; honesty via labelling rather than refusal, at the cost of "trustless" being a property of some results rather than the feature.Either way,
unchecked/unknown is never accepted and the trust class is surfaced, not flattened.Where the actual rug surface is — not the proofs
TrustedRollup(signer signs the state root, no proof) andUncheckedRollup.eth_callgas cap (ENS: ERC-3668 CCIP-Read transport + ENSIP-21 batch gateway #1664) — Solidity trie verification is gas-heavy; an out-of-gas callback is indistinguishable from a verification failure unless explicitly classified. Empty revert data is the OOG signal. NoteInvalidProof()is not what an MPT failure looks like:EthVerifierHooksnever reverts it (real failures areError(string)fromMerkleTrie.sol); a client matching that selector would match nothing and fall through to "unknown error" — the fix(eth): correct ENS nameExpires selector and skip expiration when unreachable #1657 degradation class.Approach / sequencing
Land as a series of small PRs (one per child issue), in dependency order. Do the refactor first so the feature work has a clean home:
ethrpc.gointobchain/coins/eth/ens.go(no behavior change). ENS refactor: extract ENS from ethrpc.go into bchain/coins/eth/ens.go #1662addr,text,contenthash. ENS records: multicoin addr(bytes32,uint256), text, contenthash #1665Adjacent, not covered by the children above but required for #1109 to actually close: #1622 (validation + reorg-safety of the
NameRegisteredalias harvest).Trust & safety guardrails (apply throughout)
eth_calltosender— never trust gateway JSON directly.senderequals the address called (else the callback can be redirected to an attacker-chosen target).x-batch-gateway:sentinel (handled internally, never fetched); SSRF-block private/link-local/metadata destinations; cap redirects and total fetches per resolution.*http.Clientwith an explicitTimeout, mirroringfeeHTTPClient(bchain/coins/eth/alternativefeeprovider.go:29); leaveTransportnil soProxyFromEnvironmentis honored; bound the body withio.LimitReader. (There is no repo-wide proxy helper;server/timeouts.gois inboundhttp.Serverconstants and does not apply.)uncheckedverifiers; always surface the trust class (ENS: verifier/resolver trust-level policy + classifier (OPEN DECISION: trustless-only vs resolve+label) #1669).common/metrics.gofor gateway fetch count/latency/failure, CCIP hop depth, verifier-class rejections, and the resolution-outcome taxonomy; plus adocs/page covering the new outbound HTTPS egress requirement and the trust model. Fine to fold into the implementing PRs.Estimated size
~2.3k–3.2k LOC across #1662–#1670 + #1672–#1673 (production + tests + per-coin boilerplate), revised upward from the original ~1.6k–2.3k after the review added the ENSIP-21 batch gateway (~150–250), the entry-point predicate, expiry/precedence work, and acceptance tests. Excludes #1671 (the ENSIP-15 normalizer, ~1.5k–3k, overwhelmingly generated tables — vendor it) and #1622.
Child issues
bchain/coins/eth/ens.goNameRegisteredalias label — what actually lets EVM: ENS aliases are not correctly resolved #1109 close