ERC721 Extension for zk-SNARKs

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.