Bringing Privacy To ENS: ChainSafe’s Proposed Integration Using Aztec Network

Bringing Privacy To ENS: ChainSafe’s Proposed Integration Using Aztec Network

Authored by Jagrut Kosti

ChainSafe Solutions is ChainSafe's internal applied research and development division. We combine our skill sets, knowledge, and constant research to deliver new perspectives, solve complex problems, and help the community drive innovations forward.

The following blog post details our privacy proposal presented at the Ethereum Name Service (ENS) DAO Weekly Ecosystem Meeting on December 8th, 2022.

The work comprises of an integration between ENS and Aztec Network where the receiving and spending addresses can be effectively de-linked. This is an edited version of the official specification. To review the code, please refer to our GitHub. A user manual, as well as a demo video showing the complete steps for setting up this integration is also available. A friendly reminder that this integration is ⚠️ ❌ ❗️❗️ still in BETA and NOT PRODUCTION READY. ❗️❗️❌ ⚠️

1. Introduction

The Ethereum Name Service (ENS) protocol is a distributed, open, and extensible naming system built on Ethereum. ENS names act as a human-readable representation of an Ethereum address that they can exchange or broadcast in order to receive funds, have their social media information tied to one name, or associate text records. While convenient, one can imagine how this can adversely affect a user's privacy. For example, it is a common practice to have your ENS published on social media where everyone can see it. This also means that anyone can look up an address' complete financial history!

This privacy dilemma is an interesting one indeed. On one hand, we want to have a convenient means of sharing an address. On the other, we do not want just anyone to be able to monitor our on-chain activity associated with that address. With on-chain analysis tools, this can be extrapolated to create a graph and generate an in-depth analysis of a user's spending patterns.

We also want wallet providers to seamlessly translate the name to an address so that they can construct an on-chain transaction. This needs to happen without the intervention of the receiver. ENS does that by providing callable methods on their contracts, which the wallet providers can query.

Essentially, what we need is:

  1. Separate addresses for receiving assets and spending them (or some other way of obfuscating/shielding how funds are spent)

  2. De-linking of those two addresses, i.e., there should not be a computationally feasible means of associating the two addresses on-chain with any tool

  3. Receiving address should be public, i.e., anyone who wishes to send assets can fetch an address from the name and construct a transaction. This can be done manually or using wallet providers

  4. A separate communication channel should NOT be required between sender and receiver, and the transaction should be possible asynchronously

  5. UX should not be overly complicated or require taking care of several additional components

This still leaves us with the main question:

"How do we preserve the convenience of ENS names without sacrificing a user's privacy?"

In our ENS Privacy research document, we outlined a few approaches and possible directions that we can take. The following blog specifies an integration between the Aztec Network and ENS and how it can mitigate what we set out to solve.

2. Understanding ENS

For an in-depth understanding of ENS, please refer to their docs. In this section, we highlight the aspects that we will utilize in our solution. The links reference the relevant source code.

For resolving a name for an address (e.g., linking foo.eth to 0x2345), the first query is made to the Registry contract's resolver() method. This will return the address of the resolver contract.

The second query is made on the PublicResolver contract's addr() method. This returns the actual address associated with the ENS name, which can be then used to form a transaction and send assets.

By default, the PublicResolver contract's address is set upon ENS registration. But the protocol allows the controller to manually set a custom resolver for a given ENS name. The custom resolver can be manually set by calling the Registry contract's setResolver() method. This is an important aspect that we will use in our design. This allows the current version of the ENS to run as-is while allowing existing and new users of ENS to use our privacy-focused custom resolver.

3. Understanding Aztec

Aztec Network provides fully confidential Ethereum transactions. It is based on zk-rollups and is secured by the PLONK proving mechanism. For an in-depth understanding of Aztec, please refer to the Aztec Docs. In this section, we highlight the aspects that we will refer to in our solution.

🎗 Throughout the blog, we will refer to "L1" (or layer-1) as Ethereum mainnet and "L2" (or layer-2) as Aztec's rollup service.

3.1 Architecture overview

Aztec's architecture has 3 main components:

  1. L1 contracts that hold, verify, and update the state of L2 using zero knowledge (zk) proofs. Of these, the contract relevant for our solution is RollupProcessor.sol.

  2. Falafel, the reference implementation of the Aztec rollup service. Its responsibilities are mentioned here.

  3. Halloumi, the proof-generation server for all kinds of proofs: deposit proof, transfer proof, withdrawal proof, etc.

Falafel and Halloumi can either be run on the same machine or a different one - a technical detail that matters little for now. A UI for interacting with them is currently available at https://zk.money.

Falafel processes L2 transactions, constructs new rollups, and publishes them on L1. It also listens for events on L1 and makes the state changes on L2 accordingly. Each rollup can have a maximum of 896 transactions per rollup. If a user wants to have faster confirmation, they can pay for the "empty space" of a rollup and ask the rollup service to submit immediately.

Aztec works similarly to Bitcoin's UTXO model, where instead of accounts having balances, there are owners of notes. More details on the Ain't Note Fun section from Aztec's Privacy Architecture.

3.2 Accounts

Ethereum accounts cannot be used to sign transactions on Aztec as it uses a different curve, Grumpkin curve.

A user in Aztec has two different key pairs: account(view) key and spending(signer) key. There is also a 3rd key, recovery key, but this is not relevant to our solution. The account key is used to decrypt the notes that are meant for a user, and the spending key allows them to spend those notes. There can be many spending keys that a user can register for one account.

For all the public-private key pairs of Aztec, the private part can be any random 32 bytes. But Aztec provides a way to deterministically derive those keys from a signed Ethereum message. More details in Aztec's Add Accounts to the SDK. This means as long as users have access to their Ethereum private keys, they can derive Aztec keys. There is no requirement to additionally store different mnemonics or private keys for Aztec. Your existing wallets, like MetaMask and WalletConnect, work seamlessly with Aztec.

4. ENS <-> Aztec integration

Our solution is dependent on the Aztec protocol. It will attempt to resolve privacy issues for ENS by considering how we can de-link receiving and spending addresses for users of ENS. For now, consider the following flow, with explanations to follow:

  1. Sender is someone on L1 who types in the ENS name of a receiver in order to send funds.

  2. Receiver has set up their ENS resolver to point to our custom resolver, which redirects funds to Aztec's L1 contract, RollupProcessor.

  3. Once a sender sends funds on L1, the receiver can view they have some incoming funds on L2 (processed by Falafel) from L1 which they can claim and spend on L2. In L2 terms, the receiver is the owner of the incoming funds.

  4. All transactions within L2 are private, as guaranteed by Aztec. Users can also withdraw from L2 to a completely fresh address on L1.

To understand how the integration works, we will go through all the steps that are required to achieve privacy for ENS names.

4.1 Registration

For our entire solution, the sender need not register anywhere or use any specific protocol. This gives the flexibility for anyone on L1 to send funds to the receiver who wants to use our solution without the sender needing to register on any additional platforms.

Receiver should be registered on ENS and have the control to update the address of the resolver contract. If a receiver is using some subdomain, the controller for the domain should set the custom resolver address. For registering on ENS, you can follow the process as-is at: https://app.ens.domains/. After the registration is finished, update the resolver address to our custom resolver.

For transferring the funds from sender to receiver, neither of them needs to be registered on Aztec beforehand. However, for the receiver to claim the funds, even though registration can be done later, we recommend that they register on Aztec beforehand. For registering on Aztec, you can follow the process as-is at: https://zk.money/. (NOTE: A minimum of 0.01 ETH is required for registration).

Most importantly, the address that gets registered and resolved for the ENS name should be the same address registered with Aztec. All keys derived in Aztec should use the same address for signing the messages for key derivation.

We do not need any additional user interface (UI) for our solution to work. The UIs from ENS and Aztec suffice.

4.2 Deposit

This section describes the core of our proposed solution, which ties ENS together with Aztec.

Custom resolver contract for ENS

As explained in section 2, the Resolver contract is the one responsible for resolving the address for a given ENS name. The default PublicResolver contract is responsible for resolving everything related to the ENS name. A list of all profiles that are inherited by PublicResolver is available here.

The base criteria for writing a new resolver is to have an implementation of the method:

function supportsInterface(bytes4 interfaceID) constant returns (bool);

More details are available on ENS' Writing a Resolver.

For the custom resolver, we can have all the resolver profiles as-is in the default PublicResolver, except for the AddrResolver.sol. Mainly, what the CustomAddrResolver modifies is:

  • Introduce a new payable method, sendPrivate(), for forwarding the call to Aztec's RollupProcessor's depositPendingFunds(). (Explained in next section)

For consistency, we use the same terminology as in ENS. E.g., Node: A cryptographic hash uniquely identifying a name. All terminology is available ENS Terminologies.

A function signature code for the CustomAddrResolver.sol is as follows:

pragma solidity >=0.8.4;

abstract contract CustomAddrResolver {
  /*
  * Sets true for the user's node to enable accepting private transaction on Aztec.
  */
  function setSendPrivate(bytes32 node, bool sendPrivate) external;

  /*
  * Resolves the ENS name to address and then constructs a call to RollupProcessor's
  * depositPendingFunds() by setting the assetId and proofHash to 0, owner to receiver and amount to
  * msg.value (in Wei) and forwards the call.
  *
  * This can be achieved either by having a contract ABI available when deploying this contract or hard
  * coding the function signature and using abi.encodeWithSignature() with incoming parameters.
  *
  * @param node ENS name of the receiver
  */
  function sendPrivate(bytes32 node) external payable;

  /**
  * Following are simply the requirement of EIP-137 that MUST be adhered
  * https://github.com/ethereum/EIPs/blob/master/EIPS/eip-137.md
  */
  event AddrChanged(bytes32 indexed node, address a);
  function supportsInterface(bytes4 interfaceID) public view virtual override returns (bool);
  function addr(bytes32 node) constant returns(address);
  function setAddr(bytes32 node, address addr);
  
  function() {
    throw;
  }
 }

The modified user flow of ENS looks like this:

  1. User code/wallets call the Registry contract's resolver() method.

  2. This returns the address of our custom public resolver, given that the resolver was updated during the registration process.

  3. User code/wallets queries resolver's addr() function, which will return the address mapped to an ENS name.

  4. User code/wallets checks if the resolver supports privacy by calling supportsPrivacy().

  5. User code/wallets constructs a transaction to send funds to our custom resolver's sendPrivate() function with the required parameters.

Steps 1–3 are read-only calls and are enough to resolve the address for the given name. Step 5 is where the actual transfer of funds is done.

Steps 4 & 5 will have to be done manually by the sender, which is not great for UX. Instead, we have laid out 3 options:

  • Option 1: Provide another helper method in CustomAddrResolver, which will return the hex encoded form of the function call. The sender can then simply paste in the "Hex Data" field of wallet providers when sending the transaction. This still requires the sender to call the helper method OR if wallet providers find this solution interesting, they can inherently include it as a part of their release.

  • Option 2 (Specific to MetaMask): Create a custom Snap (See Extending MetaMask: A Snap For Filecoin). The Snap will watch for the custom resolver address when an ENS name gets resolved. If it matches the address of our privacy-enabled resolver, it will construct a transaction to send to sendPrivate() function call and prompt the sender to sign using their MetaMask.

  • Option 3: Create standardization by introducing a new EIP. Once a wallet code identifies that a resolver supports privacy (e.g., using supportsPrivacy()), the wallet should be forced to send funds through sendPrivate(). The implementation details of sendPrivate() can be different for different resolvers in case more people come up with new ideas to add privacy to ENS.

Connecting with Aztec

Looking at Aztec's RollupProcessor contract, we came across depositPendingFunds() function:

function depositPendingFunds(
  uint256 assetId, // For identifying ERC20 tokens, ETH = 0
  uint256 amount, // Amount in Wei, should equal msg.value
  address owner, // Who can claim this amount, L1 address
  bytes32 proofHash // OPTIONAL, Submit the proof to make the *amount* spendable by the *owner*
) external payable;

This external payable function is callable by anyone. Originally, this was intended for anyone to deposit their funds from L1 to start using in L2. If you are the owner of the fund, you can submit the proof in the same transaction (saving some gas) to start using the funds in L2. Once the funds are approved, the owner can spend the associated zkETH / zkDAI on L2 or withdraw to a fresh address on L1.

Alternatively, a sender can send funds to this function using the receiver's address as the owner. Our custom resolver's sendPrivate() function forwards the calls to this function. Falafel watches for the emitted events, and the receiver can see there are pending funds whenever they log in to zkMoney's UI.

The receiver can later, asynchronously, submit the proof for their address using the approveProof() function of RollupProcessor contract.

function approveProof(bytes32 _proofHash) external;

If the receiver is using zkMoney's UI, they can simply "shield" their pending funds. The UI takes care of proof generation and submission, making the funds available to spend for the receiver.

Alternatively, this can be also done using the Aztec Connect SDK. If using the SDK with a custom dApp, there are several steps that need to be done first. Installation steps can be found in Aztec's Setup Docs. After that, they can use DepositController's createProof() method to generate the proof for the account on which the controller is instantiated.

Figure describing the flow of the proposed solution

Who pays for the gas?

Both the calls, from sender and receiver (proof submission), are made on the L1 contract, and the gas needs to be paid in ETH. Sender will pay the gas cost when calling the sendPrivate() function of the custom resolver, which in turn calls the depositPendingFunds() on Aztec.

For approving the proof to make the funds spendable, the receiver bears the gas cost of the transaction when calling approveProof(). This address is the public address used during registration and can be funded without any privacy concerns. The receiver need not call the proof for every transaction from the sender(s). They can wait as long as they want and cumulatively submit proof for all their pending funds in a single call, saving gas.

For the transactions made within L2, the gas costs are paid in zkETH by the receiver and are substantially lower than L1.

Cost of transactions:

  • Deposit: 51,000 gas

  • Internal send: 17,000 gas

  • Withdraw: 5,000 gas for ETH to EOA; 30,000 gas to contract.

4.3 Withdrawal

Aztec guarantees private transactions within L2, meaning that a sender's identity or balance is not visible to anyone, not even the recipient. Users can simply use the zkMoney's UI for withdrawal to L1 from L2. The UI creates a withdrawal proof using WithdrawController's createProof() for the account, which is instantiated by the controller.

When withdrawing, users can use any address on L1, create and submit proof for that address, and execute the transaction. Usually, single withdraw transactions will be expensive. Therefore, the rollup service provider waits for all the transactions within a rollup to fill up and then submit the rollup proof. For a block explorer on L1, the transaction will appear as though coming from the RollupProcessor contract to the fresh address.

There are, of course, some caveats that the user needs to take care of to protect their privacy, e.g., not using the same registered address for withdrawal (duh!), not withdrawing any idiosyncratic amounts (patterned withdrawals), etc. For more details on best practices for privacy sets, refer to Aztec's Infinite Privacy (Sets).

5. Pros and cons

When using the proposed solution for adding a layer of privacy with ENS, consider the following aspects for someone observing both L1 and L2:

Publicly available information:

  • An ENS name is using the custom resolver.

  • For all ENS names, the funds are relayed and deposited to Aztec's RollupProcessor contract through the resolver.

  • Sender and receiver addresses on L1 are linkable. For a given receiver address registered on ENS and Aztec, one can easily see the addresses of the senders.

  • Aztec's RollupProcessor contract sending x amount to a new address.

Non-linkable information:

  • How and with which addresses the receiver address interacted on L2.

  • To which account did a receiver withdraw funds to on L1. No linkage between how the receiver spends their funds.

Pros

  • Very few new components are introduced by this solution. We are largely using pre-existing infrastructure and UI as-is and simply integrating them.

  • Privacy guarantees are equivalent to that of Aztec, which so far has had no incidents regarding its security and privacy (as far as we can tell).

  • Backward compatibility: Existing ENS users can register on Aztec with their address and set their resolver to the custom resolver to start using this system.

  • Optional: Receivers on ENS can choose to opt-in for this privacy option or not, leaving them to consider for themselves the trade-off between ease-of-use and privacy. They can switch back and forth at will.

  • Senders need not register anywhere.

  • A custom UI can be built integrating ENS and Aztec if required. Aztec has good SDK support.

Cons

  • The solution relies on Aztec from a security and privacy perspective. Aztec does give an option of an escape hatch during which anyone can submit rollup proof in case a rollup service provider is offline. The liveness of this solution is tightly coupled with the liveness of the Aztec Network.

Side Note: The ChainSafe Solutions team have been working on alternative solutions like the Offchain & Scriptless Mixer design provided to the Eth Research forum.

  • Even though the sender does not need to register anywhere, they would need a helper UI (using Snaps or something else) to construct the transaction to be sent.

  • Currently, Aztec supports only ETH and DAI (as zkETH and zkDAI). As a result, our solution is restricted to these tokens. If someone sends any other ERC-20 tokens or NFTs to an ENS name, the tokens would be sent to that account as if the solution had not been invoked.

  • Gas costs are definitely higher than normal send transactions but cheaper compared to on-chain mixers.

6. Conclusion

This blog describes one of the solutions we came up with which can be used to protect an ENS user's privacy, i.e., keep private how they spend their funds. Using Aztec, we established an integration with ENS' custom resolver contract.

We also established:

  • The requirement for different receiving and spending addresses and that this can be achieved by de-linking them through the Aztec.

  • That no separate communication channels are required between sender and receiver. We can use existing infrastructure asynchronously.

  • The solution utilizes existing UIs from ENS and Aztec.

  • That services looking for simple name resolution (and not sending private transactions) can still continue using it.

About ChainSafe

ChainSafe is a leading blockchain research and development firm specializing in infrastructure solutions for web3. Alongside its contributions to major ecosystems such as Ethereum, Polkadot, Filecoin, and more, ChainSafe creates solutions for developers and teams across the web3 space utilizing our expertise in gaming, bridging, NFTs, and decentralized storage.

As part of its mission to build innovative products for users and improved tooling for developers, ChainSafe embodies an open source and community-oriented ethos to advance the future of the internet. To learn more, click here.

Website | Twitter | Linkedin | GitHub | Discord | YouTube| Newsletter