ERC721 Extension for zk-SNARKs

Only Stealth Address is not enough to cut off the linkage between sender and receiver, but combining with a Merkle tree(like Tornado Cash’s), and using the stealth address’s private key as the Merkle tree leaf’s committed value to withdraw the money, can work here. This is how Eigen Network(https://www.eigen.cash) implements its anonymous payment.

3 Likes

The main difference between a Zcash-style zk-SNARKs + Merkle tree method vs stealth addresses method mentioned by Vitalik above is in terms of confidentiality vs anonymity–the token id transacted can be hidden with zk-SNARKs but not with stealth addresses (which only provide anonymity). Note that token ids transacted does represent some information leak. In the particular case of transfers, we can aim for full confidentiality of transactions instead of just anonymity. However, for supporting more complicated smart contract logic taking token type and amount (ERC20 / 1155), then we can only hope to achieve anonymity.

An additional downside to stealth address is that if it is applied to anything beyond 721 (like 20 or 1155), then there is very little privacy added as the chain of transfers can be traced. Whereas a zk-SNARK based method can preserve confidentiality or anonymity completely.

Ideally, L1s should support privacy-preserving tokens that can be used by smart contract applications (anonymity for composable on-chain transactions and confidentiality for P2P swaps / transfers). This can be achieved with known techniques. Effectively, one can take the best part of privacy-focused L2s like Aztec, ZkOpru, Railgun, and Eigen and make the privacy-preserving token accounting default on the L1. This is described in FLAX. The main problem to have built-in privacy on Ethereum is that we have a fixed gas fee payment mechanism tied to EOAs, making all privacy-preserving token standard moot unless we have a privacy-preserving gas payment method. This is why privacy is currently best done in a separate layer for Ethereum unless privacy-preserving gas payments are possible. A privacy layer also has the added benefit of not needing to change token standards on L1–the smart contract own assets on behalf of its “L2” users on L1.

However, there are ways to salvage this with anonymous gas payments and privacy-preserving ERC20/721/1155s. For gas payments, we can combining a shielded token pool (e.g. Zcash style zk-SNARKs + Merkle tree) into EIP-4337. Composable usage of tokens in these shielded pools require alternative to ERC20 approve that is atomic and stateless (can be done by expanding call stack access as described here). The caveat is that this means we need to change user transaction / ERC20 / Defi ecosystems entirely, and at that point, might as well build a privacy layer on-top of Ethereum.

7 Likes

Likely this is a better fit for Soulbound Tokens, where we

  • don’t necessarily desire anonymity but just unlinkability of nyms
  • gas less of an issue / out-of-band gas more viable (DAO can provide gas station for SBT holders to do SBT-related stuff)

If SBTs:

  • each go to a fresh stealth address (if the mint is self-serve the user can generate a private key themselves, rather than going through the stealth address generation protocol)
  • can optionally be linked
  • can be issued/revoked via semaphore/interep proof (there’s a question of whether all-or-nothing sharing is achievable here)
  • can be added/removed from semaphore/interep group

This pretty much gets us to DeSoc, except of course you’d need at least one social recovery set per connected component of nyms.

2 Likes

The reason why this needs to be standardized is that the sender needs the ability to automatically generate an address belonging to the recipient without contacting the recipient.

2 Likes

I like the idea and it makes full sense to implement it in the way suggested
Removing the zk-SNARK part comes with further simplification, which is great.

Only one thing:
In the case, C representing a contract (going to be) deployed on A through CREATE2, the sender would have to use a different method (using the recipient’s address, salt, bytecode) to generate the address for C, which would destroy privacy, or do I miss something here? How would the recipient be able to have a contract already waiting at A to receive the token? In other words, would the sender have to anticipate a specific CREATE2 call eventually executed by the recipient at address A?

Addressing the remaining challenge mentioned, I’m generally a fan of specialized searchers (as implemented in Surrogeth) implemented in the final app. However, this would require to have some additional functionality for handling related incentives for frontrunners within the standard. Therefore I agree that the “pay for the recipients’ transfers and then tornado-refill” is the better approach.

I’m keen to implement it.
EDIT: Find a minimal poc with stealth addresses here.

I guess, the approach suggested by @vbuterin is the one that we should go for:
I’ve started drafting an EIP here.

Please feel free to provide feedback!

Also, I’m looking for contributors with experience in the EIP process and Crypto/Ethereum in general, to help me implementing it.

In other words, would the sender have to anticipate a specific CREATE2 call eventually executed by the recipient at address A?

Yes, this is exactly the idea. In general, this is a common pattern for smart contract wallets, which is necessary to replicate the existing functionality where users can receive coins to an address they generate offline without having to spend coins to register that address on-chain first.

@Nerolation now that I think more about this, I am realizing that it doesn’t actually make sense to make this standard part of ERC721 per se. There are lots of potential use cases for it. So it probably should be an independent ERC that lets users generate new addresses that belong to other users, and both ERC721 applications and other applications can use that ERC.

Aside from that, my main feedback to the EIP so far is that I do hope that something like the generateStealthAddress method idea from my earlier post can be added so that we can support smart contract wallets.

1 Like

How would calculating p * S for every S of every potential transfer (of many implementing contracts) work for private keys stored on e.g. hardware wallets / secure elements? Would this mean that this scheme is only tractable for low value assets (since sender cannot assume the receiver ever finds out)?

Maybe only specially designated addresses can be used (e.g. signalling their ability to receive this kind of transfer in some public way) and a special registry will be needed for those. And to have such an address one would use some ephemeral private key generation scheme every time to scan for those transfers and transfer them to some other address (that is controlled by the actual private key, but isn’t public).

The value s represents a secret value and not the senders private key.
Using a random one-time secret, no private information gets leaked.

I think, it’s best to implement the suggested generateStealthAddress(bytes32 key) into a smart contract wallet (see sample implementation here), as outlined above.

There are still some points open for discussion:

  1. How would p*S be implemented (at recipient’s side) to not expose the wallet to any security risk?
  2. Should P be immutable or updateable? (I’d prefer immutability and using new contracts for changing Ps)
  3. Adding CREATE2 functionality to “force” the recipient to create a smart contract wallet to “claim” the transfer or transfers to EOAs.
  4. How does the sender publsih S? (Could be done by the smart contract wallet after executing the token transfer)

The current workflow looks like the following:

  1. Sender calls recipients smart contract wallet locally to receive publishableData and a stealthAddress
  2. Sender sends to recipient’s stealth address and publishes publishableData
  3. Recipient parses all S and checks if some address contains a token

see sample implementation here

I added a quick PoC using Gnosis Safe Modules here.

Basically, every compatible smart contract wallet would require to have a public key (of the owner) coded into it (might be upgradeable).
The privateTransfer Module (check the link above for Gnosis safe example) would broadcast a PrivateTransfer Event containing publishableData for every transfer using the privateETHTransfer function (the same priciples can then be applied to tokens).
This way, the standard can easily be extended.

Addressing, which contract should eventually publish S, it would be best to have it in the token contract itself, which would require extensions to the existing standards and some separat implementation for ETH transfers.
In case, all users use the same module, then the module contract may simply broadcast the Event containing publishableData (S) - probably together with the asset address interacted with for filtering.

2 Likes

I would say you should just have a contract that issues a log containing the S value, with the recipient address as a topic. This makes it easy for the recipient to search, as they can just scan through all logs that have their address as a topic, and it keeps the stealth address mechanism separate from the ERC721 mechanism (as I do think that the stealth address mechanism is a general-purpose tool, which could also be used for ERC20 sends and other applications, there’s no need to tie it to one specific application).

2 Likes

That’s a good idea! Logging the recipient’s stealth address enables the recipient to compare the indexed topic (stealthRecipient) with the address calculated from the shared secret S to check for ownership.
Consequently, no need to ask the chain if the derived (from S) address possesses an asset.

EDIT: This implementation may require a single immutable contract that can take over the role to exclusively emit PrivateTransfer events for every kind of asset transfer. This then allows wallets to subscribe to a single contract instead of every other SM wallet.

This is the current draft :

pragma solidity ^0.8.6;
...
interface ERC-N {
    
    /// @notice Public Key coordinates of the wallet owner
    /// @dev Is used by other wallets to generate stealth addresses
    ///  on behalf of the wallet owner.
    bytes publicKey;


    /// @notice Generates a stealth address that can be accessed only by the recipient.
    /// @dev Function is executed locally by the sender on the recipient's wallet to
    ///  generate a stealthAddress and publishableData S. The Caller/Sender must select a secret
    ///  value s and compute the stealth address of the wallet owner and the matching public key S
    ///  to the selected secret s.
    /// @param secret A secret value selected by the sender
    function generateStealthAddress(uint256 secret) returns (bytes publishableData, address stealthAddress)

}

interface PubStealthInfoContract {

    /// @noticeImmutable contract that broadcasts an 
    ///  event with the address of the stealthRecipient and 
    ///  publishableData S for every privateTransfer. 
    /// @dev Emits event with private transfer information S and the recipient's address.
    ///  S is generated by the sender and represents the public key to the secret s.
    ///  The sender broadcasts S for every private transfer. Users can use S to check if they were
    ///  the recipients of a respective transfer by comparing it to stealthRecipient.
    /// @param stealthRecipient The address to send the funds to
    /// @param publishableData The public key to the sender's secret
    event PrivateTransfer(address indexed stealthRecipient, bytes publishableData)
}

Thanks for all the great input so far!
I tried to summarize the idea and the current status in a blog post.
You can find it here:

Please feel free to suggest any feedback!

Find the EIP at the following place:

1 Like

Admittedly I only skimmed the above EIP, but wanted to mention that we’ve built Umbra which has been live for a little over a year and supports ETH/ERC-20 payments to stealth addresses. We never got around to writing up an EIP, but the current implementation should support NFTs and smart contract wallets without many changes (e.g. the Umbra contract would need to be modified since it can only transfer ERC-20 tokens). I’d recommend taking a look at our implementation and using that as a starting point for an EIP since I think it may already cover most or all of your goals.

Our FAQ contains all the details. There’s a single core Umbra contract, along with a StealthKeyRegistry which maps an address to the public key to use when computing a stealth address for that recipient. This can support smart contracts wallets and multisigs by having them publish a public key that at least 1 person on the contract wallet has access to.

We’ve also considered supporting things like view tags to help reduce scanning time. We don’t have plans to implement it yet since it’d require migrating to a new contract, however adding both view tags and NFT support could be sufficient enough to justify the migration.

2 Likes

It’s cool to see what you’ve built, thanks for your feedback!

The goal of this EIP is the pure implementation into SM wallets so that it depends only on the upgradable SM wallet which assets to support (ETH, ERC20, ERC-721, etc.). Therefore a standardized generateStealthAddress function is implemented. This EIP won’t have a contract acting as a registry but put the publicly visible public keys directly into the individual SM wallets. This then allows senders to locally execute the generateStealthAddress function on the recipient’s wallet to retrieve a stealth address (for every kind of assets possible).

I’d recommend taking a look at our implementation and using that as a starting point for an EIP since I think it may already cover most or all of your goals.

The starting point is the idea of implementing the idea described by Vitalik in this threat. I also believe that implementing it into SM wallets makes much sense. Check out the latest version of this EIP?

…things like view tags to help reduce scanning time.

In the current implementation, recipients would only have to compare the computed (…derived from S) stealth Address with the address logged in the same topic to check if they were the recipient. This can be done fully off-chain. A single, immutable contract can be used to broadcast the Event.

Feel free to reach out to me!

Ah I misunderstood then, I thought it was a generic stealth address EIP with contract wallet support as part of it. But I see now the actual EIP title is “Stealth addresses for smart contract wallets” and not “Stealth addresss wallets” as shown above. Though that raises the question: why limit the EIP to just contract wallets?

Could you expand more on the rationale for preferring generateStealthAddress over something like the StealthKeyRegistry? It does seems more simpler and flexible to have the pubkeys stored outside of the wallet since it doesn’t require upgrades to each contract wallet implementation. This also makes it easier to allow someone to set keys on your behalf—the current registry’s setStealthKeysOnBehalf only supports EOAs, but you can imagine extending it to support EIP-1271.

With either approach, a registry contract or generateStealthAddress you locally generate the stealth address anyway.

A few things worth considering based on how StealthKeyRegistry works:

  • By storing a compressed pubkey as bytes32 instead of the full key as bytes, you cut down from 2-4 slots (depending on how you choose to encode the full pubkey) to 1 slot. To use just one slot, the prefix can be a key in a mapping so it doesn’t actually need to be stored, and a getter method checks the slots of each prefix and returns the nonzero one.
  • Viewing keys. The StealthKeyRegistry supports storing two keys: a spending key and a viewing key. Users can give the viewing key to a trusted third party / have it live on a server and scan for funds. If the key is compromised, you lose privacy, but not your tokens, since withdrawals require the spending key. We generate stealth addresses a bit differently (sender also generates a random number) to help support this. With compressed keys, this only needs 2 slots, with uncompressed keys you’d need 4-8 slots.

Yep, this is how Umbra behaves too! This requires two elliptic curve multiplications: one to compute the shared secret, and a second to compute the stealth address. My understanding (which could be wrong) is that computationally elliptic curve multiplication is the slowest operation involved here. Using view tags replaces one of these with a hash and should help reduce scanning time.

1 Like

You raised some really good points.

Using an immutable contract for storing the public keys of users might come with additional benefits, however, putting the public keys directly into sm wallets could also help bring more EOA-users to sm wallets. So, instead of asking another user if he supports stealth addresses, you would check the registry if the other user has registered a public key (while the same immutable contract takes over the role to emit the events - a contract is required anyways).
I think this is a great point for discussion. I’m still not 100% sure about the downsides of a registry contract, instead of encoding public keys into the wallets themselves. Anyway, I get your point and agree that registries could not only allow for EOAs but also allow for enabling additional functionality.

This is a nice idea. I will look into this. It may add complexity for efficiency. Off-chain. I think the trade-off is worth it.

Ok yeah, I see. So, instead of generating the stealth address though sharedSecret + PubK_{Recipient}, you would instead use sharedSecret as the stealthAddress and transmit the secret s required to decrypt the encrypted s \mid sharedSecret = s * G.

This is a good point too, I fully agree. Still might add some complexity (off-chain), but looks worth it.

How about changing some names(I get these names from airline & amazon control tower terms) and add gas station contract & use meta tx(gasless tx) for first funding problem.

gas station contract could be multiple ones

By modifying GSN to private GS

// SPDX-License-Identifier: CC0-1.0
pragma solidity ^0.8.6;
...
interface IERC5564 {
    
    /// @notice Public Key coordinates of the wallet owner
    /// @dev Is used by other wallets to generate stealth addresses
    ///  on behalf of the wallet owner.
    bytes32 publicKey;


    /// @notice Generates a stealth address that can be accessed only by the recipient.
    /// @dev Function is executed locally by the sender on the recipient's wallet to
    ///  generate a stealthAddress and publishableData S. The Caller/Sender must select a secret
    ///  value s and compute the stealth address of the wallet owner and the matching public key S
    ///  to the selected secret s.
    /// @param secret A secret value selected by the sender
    function takeOff(uint256 secret) returns (bytes publishableData, address stealthAddress);

}

interface ControlTower {
    /// @notice Immutable contract that broadcasts an 
    ///  event with the address of the stealthRecipient and 
    ///  publishableData S for every privateTransfer. 

    /// @dev Returns boolean whether the stealth is generated from the ControlTower
    /// @param stealthAddress The address to check identity 
    function check(address stealthAddress) returns(bool);

    /// @dev Returns bytes for meta transaction result
    /// @param stealthAddress The address to use its assets
    /// @param data encoded data for actions(transfer, swap, ...)
    function requestAction(address stealthAddress, bytes data) returns(bytes approvalData);

    /// @dev Emits event with private transfer information S and the recipient's address.
    ///  S is generated by the sender and represents the public key to the secret s.
    ///  The sender broadcasts S for every private transfer. Users can use S to check if they were
    ///  the recipients of a respective transfer by comparing it to stealthRecipient.
    /// @param stealthRecipient The address to send the funds to
    /// @param publishableData The public key to the sender's secret
    event PrivateTransfer(address indexed stealthRecipient, bytes publishableData)
}

interface GasStation{
    /// @notice ETH gas reserves
    /// Whitelisted control tower addresses using mapping 
    /// and use check whether the meta tx from is generated by whitelisted control towers
    /// @dev Execute the transaction
    /// @param maxPossibleGas Maximum gas amount to execute the transaction
    /// @param approvalData The transaction data made by the valid control tower
    function affirm(uint256 maxPossibleGas, bytes approvalData) returns(bytes memory context, bool rejectOnRecipientRevert);
}

Reference
https://docs.opengsn.org/#architecture
https://aws.amazon.com/ko/controltower/features/

Hello @Nerolation ! Thank you for this EIP, I really like the idea and the implementation. I have a question - how do you think the payment-receipt scheme may look like in this case?

Let’s say Alice asks Bob to send her 2 ETH. Imagine Bob has two wallets:

  • st:Bob:1, 0.5 ETH
  • st:Bob:2, 1.5 ETH

From the privacy-preserving point of view, it would be nice if Bob could do two separate transactions:

  • st:Bob:1 – 0.5 ETH → st:Alice:1
  • st:Bob:2 – 1.5 ETH → st:Alice:2

So now, Bob needs to provide Alice a receipt, confirming the fact of payment. No one else should be able to “use” Bob’s transactions as their own. Of course, Alice can first tell Bob some payment_id, which should be included in the view tag, but it will create a link between transactions.