Skip to main content

Using Aztec for ENS Privacy


1. Introduction

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 at one place, or any other text records associated with the user. While convenient, it certainly is bad for privacy. It is a common practice to have your ENS published on social media where everyone can see it. This also means anyone can lookup who is sending funds to a user and how they are spending their funds.

ENS privacy dilemma is interesting! On one hand, we want to have a convenient means of sharing the address where anyone can send assets and on the other, we do not want any and everyone to watch our on-chain activity associated to that address. With on-chain analysis tools, this can be extrapolated to create a graph and generate an in-depth analysis of spending patterns of the user.

We also want the 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 be 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.

In our research document we outlined a few approaches and possible directions that we can take. This document specifies the integration between Aztec and ENS and how this mitigates 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, 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.

There is a possibility to set a custom resolver for a given name by the controller. By default, the PublicResolver contract's address is set on ENS registration. Custom resolver can be set by calling 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 provides fully confidential Ethereum transactions and is based on ZK-Rollup and is secured by PLONK proving mechanism. For an in-depth understanding of Aztec, please refer to their docs. In this section we highlight the aspects that we will refer in our solution. Throughout the document, L1 refers to Ethereum mainnet and L2 refers to 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 zk proofs. Of these, the contract relevant for our solution is RollupProcessor.sol.
  2. Falafel, a reference implementation of an Aztec rollup service. It's responsibilities are mentioned here.
  3. Halloumi, 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, doesn't matter. 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 similar to UTXO model where instead of account having balances, there are owners of notes. More details here.

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 have two different key pairs, account(view) key and spending(signer) key. There is also a 3rd key, recovery key, but that is not relevant for our solution. Account key is used to decrypt the notes that are meant for them and spending key allows to spend those notes. There can be many spending keys that a user can register for one account.

Private part of all keys can be any random 32 bytes. But Aztec provides a way to deterministically derive those keys from a signed Ethereum message. More details here. This means, as long as users have their Ethereum private keys safe, 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 works seamlessly with Aztec.

4. ENS <-> Aztec Integration

Our solution is dependent on Aztec protocol. The solution is specific for ENS i.e how we can de-link receiving and spending addresses for users of ENS. For now, consider the following flow, explanation of each follows later:

  • Sender is someone on L1, who uses an ENS name of the receiver to send funds.
  • Receiver has set up their ENS resolver to point to our custom resolver, which redirects funds to Aztec's L1 contract, RollupProcessor.
  • Once a sender sends funds on L1, 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's term, the receiver is the owner of the incoming funds.
  • 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 steps that are required to accomplish our purpose.

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 resolver contract. If a receiver is using some sub-domain, the controller for the domain should set the custom resolver address. For registering on ENS, follow the process as it 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. But for the receiver to claim the funds, they need to register on Aztec. This can also be done later but we recommend doing it before hand. For registering on Aztec, follow the process as it is at: https://zk.money/. (NOTE: A minimum of 0.01 ETH is required for registration).

Most importantly, during registration of ENS, the address that gets resolved for an ENS name should be the same when registering 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 UI for our solution to work. The UIs from ENS and Aztec suffices.

4.2 Deposit

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

Custom Resolver Contract for ENS

As explained in section 2, the Resolver contract is the one responsible for finally 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 available here.

For the custom resolver, we can have all the resolver profiles as it 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 here.

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 the ENS looks like:

  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 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.

Step 4 & 5 will have to be done manually by the sender, not a good thing for the UX. We can:

  • Option 1: Provide another helper method in CustomAddrResolver which will return the hex encoded form of the function call which the sender can 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. 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 a 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 some privacy on 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 needs to be done first. Installation steps can be found here. 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 their 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 withdraw 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 for protecting their privacy. E.g. not using the same registered address for withdrawal (duh!), not withdrawing any idiosyncratic amounts, etc. For more details on best practices on privacy sets, refer here.

5. Pros and Cons

When using the proposed solution for adding a layer of privacy integrated 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 on L1. No linking between how the receiver spends their funds.

Pros

  • Very few new components are introduced by this solution. We are using the existing infrastructure and UI as is and simply integrating them.
  • Privacy guarantees are equivalent to that of Aztec, which so far has no incidents regarding its security and privacy (AFAIK).
  • 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 for this or not, considering the trade-off between ease-of-use and privacy. They can switch back and forth at their will.
  • Sender 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 security and privacy perspective. Aztec does give an option of 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 liveness of Aztec network.
    • Side Note: The solutions team have been working on alternative solutions like the Offchain & Scriptless Mixer design. Eth research forum post is available here.
  • Even though sender does not need to register anywhere, they would need a helper UI (using Snaps or anything else) for constructing the transaction to be sent.
  • Currently, Aztec only supports ETH and DAI. Our solution is restrcited to these tokens. If someone sends other tokens or NFTs to an ENS name, they can be used from that account as one would if this solution was not used.
  • Gas costs are definitely higher than normal send transactions but cheaper compared to on-chain mixers.

6. Conclusion

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

  • Different receiving and spending addresses and de-linking them using Aztec.
  • No separate communication channel required between sender and receiver. Can use existing infrastructure asynchronously.
  • Utilizing UIs from ENS and Aztec.
  • Services looking for simple resolution and not sending transaction can still continue using it.

For a test implementation of this integration, the source code is available here.