[RFC] Privacy-Preserving Names: ENSIP for Stealth Address Resolution

Context

As part of our SPP3 work, we will be drafting an ENSIP that defines standards for stealth address resolution with ENS names.

Below we are outlining the proposed scope and considerations for this ENSIP and would like to engage the ENS community and Ethereum wallet ecosystem for comments and suggestions, before publishing the full ENSIP draft in September.

This ENSIP will be the basis for all other deliverables of the linked SPP3 application.

The Problem

Most ENS names resolve to one static address. This means that every sender or user looking up an ENS sees the recipient’s entire activity and financial history.

By combining ENS and stealth addresses, users can benefit from the UX of having one static identifier (the ENS name) while protecting their privacy by returning a new address for every resolution.

We have been running this in production with Fluidkey for over 2 years, supporting over $1b in transfer volumes across 28k users.

Now we want to ensure other apps can do the same without having to re-build everything from scratch, while creating a standard that ensures interoperability.

Existing Standards & Background Reading

This ENSIP builds on top of ERC-5564: Stealth Addresses.

An incomplete guide to stealth addresses by Vitalik Buterin is a good starting point to learn more about stealth addresses.

Proposed ENSIP Scope

We propose that the ENSIP specifies the three items that follow. These will enable client-side and gateway-based stealth address resolution from an ENS record as outlined in this diagram:

1. A text record for stealth meta-addresses

A new global text record key, registered per ENSIP-5 conventions:

  • Key: stealth-meta-address[<schemeId>], e.g. stealth-meta-address[1] for SECP256k1 with view tags

  • Value: the ERC-5564 meta-address string verbatim, e.g. st:eth:0x<spendPubKey><viewPubKey>

Why text records: they work today with every deployed resolver, the ENS manager app, and existing tooling. Any wallet can support registration by writing one record.

The default record is valid across all EVM chains (the same keys derive the same addresses on every EVM chain). Users who want distinct keys per chain can set an optional chain-specific record, stealth-meta-address[<schemeId>][<coinType>], resolved with ENSIP-11 precedence: specific coinType first, falling back to the default. Non-EVM scoping would be an ERC-5564 extension and is out of scope here.

2. The resolution flow

Two paths from the same record:

Client-side: the sender’s wallet reads the text record via a standard text() lookup, generates an ephemeral keypair locally, derives the stealth address per ERC-5564, and sends funds. The ENSIP only specifies which record to read and how to parse it.

Gateway-based (CCIP-Read): an offchain resolver generates the stealth address and returns it with the ephemeral secret, verified onchain before the resolution result is returned (see step 3).

Note that addr() itself may already return one-time stealth addresses (the gateway path like with Fluidkey): wallets then get stealth resolution transparently, without any awareness of this standard. The meta-address record is what makes such resolution verifiable onchain and additionally enables client-side derivation.

3. Onchain verification of gateway responses

For the gateway path, the returned stealth address is verified onchain inside the same read call. Resolution reverts on mismatch, so a gateway can never redirect funds to an address that does not derive from the registered meta-address.

schemeId() → uint256

verifyStealthResolution(bytes32 node, address stealthAddr, bytes calldata verificationData) → address | revert

Each verifying resolver is immutable and verifies exactly one ERC-5564 scheme, advertised via schemeId(). Future schemes (e.g. ZK-based verification) are standardized via the ERC process and deployed as new immutable resolvers, with name owners opting in through normal resolver selection. This ensures security by removing the need for an upgradeable contract.

Each ERC-5564 scheme defines its own verificationData. For schemes verified by recomputing the derivation onchain (e.g. scheme 1), it carries the ephemeral secret. The secret exists only inside the sender’s own read call and is never broadcast.

This is a security feature that ensures offchain resolved names are always verified onchain: a gateway can never redirect funds to an address that does not derive from the registered meta-address. Because verification runs in the read path, it adds zero gas to the payment.

–-

Operational guidance such as caching and key rotation best practices will live in the integration guide rather than the ENSIP, to keep the spec minimal.

We will also implement a stateless bridge contract that follows the ERC-6538 read interface backed entirely by ENS records, but this does not need to be included in the ENSIP in our view.

We’d be grateful for your input and questions on the above.

7 Likes

Apologies for the brevity, but to just jump into it:

ERC-5564, CCIP-read, and resolver behavior are already defined elsewhere, so it kind of sounds like the entire ENSIP would simply reserve a key name for publishing stealth address public keys as a text record. Is that right?

Item 1 explains the key name and what value we expect to find there.

Item 2 describes retrieving a text record (Client-side) and doing a CCIP-read (Gateway-based), which both follow existing interface declarations, so it doesn’t sound like there is anything new to define.

In item 3, you said “the returned stealth address is verified onchain inside the same read call”, but then you also mentioned a verifyStealthResolution method.

If the flow for clients was to first call addr() to get a CCIP-read-provided address, and then call verifyStealthResolution on a smart contract to validate the address, that wouldn’t be “onchain” verification, because you are still relying on an (offchain) client to do the verification step and honor the result.

I would only call it “onchain” verification if the addr() read function performed the CCIP-read and then refused to return any value unless it passed validation using on-chain code (which matches your “inside the same read call” wording). But in this case, there is no need for verifyStealthResolution to be an external method, so again there are no interface changes for the ENSIP to explain. You could just build and deploy a resolver that does the behavior you describe, and it would be compatible with the existing definitions that are out there.

Is this interpretation correct, or am I missing something?

2 Likes

Thanks for the helpful feedback jkm.eth.

The intent was always for verifyStealthResolution to be called within the resolution flow of addr() but I can see how this wasn’t clear from the post.

Here’s a sharpened version of the ENSIP scope based on your points:

  1. Normative

    a. The stealth meta address text record shape and encoding

    b. The gateway response payload schema for CCIP-read resolution: (stealthAddress, bytes verificationData), where each ERC-5564 scheme defines its own verificationData (e.g. the ephemeral secret for scheme 1). Verification is done by the resolver’s callback within resolution.

    c. Resolution semantics: the CCIP-read callback MUST verify that the gateway-provided address derives from the name’s registered meta-address and MUST revert otherwise. Since ERC-3668 requires the callback to execute before any value is returned, addr() structurally cannot return an unverified address.

    d. The verifying resolver interface: schemeId() and verifyStealthResolution, with one immutable resolver per scheme. verifyStealthResolution is the specified callback from (c). It is external by ERC-3668’s nature. Specifying it normatively (i) binds the payload schema in (b) to a concrete, detectable contract surface, (ii) lets anyone re-verify a past resolution out of flow, and (iii) makes stealth resolutions verifiable onchain by other contracts. URLs are stored per-name, set by the name owner, so multiple integrators can run independent gateways against the same canonical resolver deployment. Since every response is verified against the owner-set meta-address, gateways are trustless by construction.

  2. Reference implementation of the verifying resolver and gateway, which we’ll also run in production for Fluidkey names.

So beyond reserving the record key, the new surface is the payload schema, the verification semantics, and the resolver interface. Does this make sense? We want to keep the standard as minimal as possible while making stealth resolution verifiable rather than trusted.

2 Likes

Thank you for the sharpened scoping @moritz, excited to see this progress!

A few suggestions and ideas from my end for the upcoming ENSIP:

  1. Text record key format. I think it should be mentioned explicitly that
    <schemeId> refers to ERC-5564’s schemeId (a uint256) and that 1 refers to
    its own reference scheme. It’s also worth stating how that uint256 is rendered
    in the key, given that [1], [01] and [0x1] are three distinct records.
  2. Default vs chain-specific records. Instead of introducing two distinct text
    record keys, it might be simpler to have only
    stealth-meta-address[<schemeId>][<coinType>] and use the appropriate
    coinTypes.
  3. Text record value. ERC-5564 only gives an example for Ethereum,
    st:eth:0x<spendingPubKey><viewingPubKey>, whereas ERC-6538 documents the
    format as st:<shortName>:0x<spendingPubKey>:<viewingPubKey> and attributes it
    to ERC-5564 and ERC-3770. Those differ both in the <shortName> placeholder and
    in the separator between the two keys, so it would be good for the ENSIP to pin
    one down. Worth noting that ERC-6538 sidesteps this by dropping the string form
    entirely and storing the compressed spendingPubKey and viewingPubKey
    concatenated as bytes, with the chain implicit from the deployment.
  4. Failure semantics. What should a client do with an absent, malformed or
    unsupported-scheme record?
  5. Rotation. Even if key rotation is covered in the integration guide, it would
    be good to state how it is handled at the record level, for example whether
    multiple concurrent values per (schemeId, coinType) are allowed.
  6. Onchain verification. Since verifyStealthResolution consumes a node, I
    assume it resolves the record itself, in which case the ENSIP should require
    that the meta-address lives onchain.
  7. Record type. I’m also wondering whether a data() record (ENSIP-24)
    wouldn’t be a better fit for the meta-address, since the st:eth: prefix gets
    stripped before any computation anyway and a verifying resolver would otherwise
    have to parse a hex string onchain on every resolution. ENSv2’s
    PermissionedResolver already supports that profile, and as noted above ERC-6538
    stores meta-addresses as bytes as well.
  8. On verification. You write that “gateways are trustless by construction”. To
    my understanding verifyStealthResolution(node, s, v) only confirms that s
    derives from the owner-set meta-address M(node) and the supplied ephemeral
    secret v. If the gateway uses a fixed v for every request, every sender gets
    the same address and privacy is lost, but verification still passes, since the
    check only tests that s and v are consistent and not where v came from. The
    callback is a stateless view function, so it has no way to detect reuse. Do I
    have a misunderstanding somewhere, or would freshness of v need to be a
    separate requirement on the gateway?
1 Like

Thank you @workemon.eth!

Agree on your points 1-7 and we’ll make sure to integrate them in the ENSIP.

Regarding 8, what I meant is that the gateway guarantees safety as it can never return an address that is not a validly derived stealth address belonging to the owner of the meta-address. You’re right that a malicious gateway could however breach a user’s privacy by returning the same address multiple times. Unfortunately I don’t think there is a viable solution to preventing this with onchain logic. We will make sure the ENSIP states that gateways MUST derive v freshly per request and that v MUST NOT be reused.

Senders with strict privacy requirements can also bypass the gateway entirely and derive locally per ERC-5564, since the meta-address is onchain.

2 Likes