Published on

Implement stealth address with Solidity

Authors
  • avatar
    Name
    Fabrisme
    Twitter

Privacy is a top priority for many cryptocurrency enthusiasts, and stealth addresses in Ethereum are a key component in achieving it. In this article, we dive into the concept of stealth addresses and their relevance to Ethereum. We examine the technical details of how stealth addresses work using elliptic curve cryptography and their importance in Web3 and NFTs by implementing an ERC721 compliant with stealth addresses.

Before continuing I recommend you this article on Vitalik's blog.

Creating the smart contract

Our StealthNFT contract will inherit from openzeppelin's ERC721 and use this elliptic curve library.

We also want to implement the following interface:

interface IStealthNFT {
    /// @dev Emitted when a nft is stealthily transferred to `stealthRecipient`
    event StealthTransfer(address indexed stealthRecipient, bytes32 publishedDataX, bytes32 publishedDataY);

    /// @dev Mints a nft with `tokenId` and transfers it to `recipient`.
    function mint(address recipient, uint256 tokenId) external;

    /**
     * @dev Registers the given public key of `msg.sender` onto the contract.
     * Reverts if the public key does not belong to `msg.sender`.
     * @param publicKeyX X coordinate of the public key.
     * @param publicKeyY Y coordinate of the public key.
     */
    function register(bytes32 publicKeyX, bytes32 publicKeyY) external;

    /**
     * @dev Transfers a nft to a stealth address.
     *
     * Emits a {StealthTransfer} event. This provides the
     * necessary information for the true recipient to
     * compute the private key to `stealthRecipient` address.
     *
     * @param stealthRecipient Stealth address to transfer the token to.
     * @param tokenId Token id of the nft to transfer.
     * @param publishedDataX X coordinate of the published data.
     * @param publishedDataY Y coordinate of the published data.
     */
    function stealthTransfer(
        address stealthRecipient,
        uint256 tokenId,
        bytes32 publishedDataX,
        bytes32 publishedDataY
    ) external;

    /**
     * @dev Generates a stealth address and its published data.
     * This should be called off-chain.
     *
     * Should revert if the given `recipientAddress` has not
     * had their public keys registered.
     *
     * @param recipientAddress Address of the true recipient.
     * @param secret Secret value decided by the sender.
     *
     * @return stealthAddress Generated stealth address.
     * @return publishedDataX X coordinate of the published data.
     * @return publishedDataY Y coordinate of the published data.
     */
    function getStealthAddress(
        address recipientAddress,
        uint256 secret
    ) external view returns (
        address stealthAddress,
        bytes32 publishedDataX,
        bytes32 publishedDataY
    );
}

Adding secp256k1

We begin coding StealthNFT by adding secp256k1 curve parameters and necessary method to perform the cryptographic operations.

contract StealthNFT is IStealthNFT, ERC721 {
    uint256 public constant GX = 0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798;
    uint256 public constant GY = 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8;
    uint256 public constant AA = 0;
    uint256 public constant BB = 7;
    uint256 public constant PP = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F;

    constructor() ERC721("Stealth NFT", "SNFT") {}

    function invMod(uint256 val, uint256 p) pure internal returns (uint256)
    {
        return EllipticCurve.invMod(val, p);
    }

    function expMod(uint256 val, uint256 e, uint256 p) pure internal returns (uint256)
    {
        return EllipticCurve.expMod(val, e, p);
    }


    function getY(uint8 prefix, uint256 x) pure internal returns (uint256)
    {
        return EllipticCurve.deriveY(prefix, x, AA, BB, PP);
    }


    function onCurve(uint256 x, uint256 y) pure internal returns (bool)
    {
        return EllipticCurve.isOnCurve(x, y, AA, BB, PP);
    }

    function inverse(uint256 x, uint256 y) pure internal returns (uint256,
        uint256) {
        return EllipticCurve.ecInv(x, y, PP);
    }

    function subtract(uint256 x1, uint256 y1, uint256 x2, uint256 y2) pure internal returns (uint256, uint256) {
        return EllipticCurve.ecSub(x1, y1, x2, y2, AA, PP);
    }

    function add(uint256 x1, uint256 y1, uint256 x2, uint256 y2) pure internal returns (uint256, uint256) {
        return EllipticCurve.ecAdd(x1, y1, x2, y2, AA, PP);
    }

    function derivePubKey(uint256 privKey) pure internal returns (uint256, uint256) {
        return EllipticCurve.ecMul(privKey, GX, GY, AA, PP);
    }
}

Implementing mint, register and stealthTransfer

To register we need the points of the public key. The formula to get the public address from the points is:

  • sha3(publicX, publicY)[12:]

The mint function will just call _mint.

contract StealthNFT is IStealthNFT, ERC721 {
    //...

    struct PublicKey {
        uint256 X;
        uint256 Y;
    }

    mapping(address => PublicKey) public publicKeys;

   //...

    function mint(address recipient, uint256 tokenId) public {
        _mint(recipient, tokenId);
    }

    function register(bytes32 publicKeyX, bytes32 publicKeyY) public {
        address pub = address(uint160(uint256((keccak256(abi.encodePacked(publicKeyX, publicKeyY))))));
        require(msg.sender == pub);
        publicKeys[msg.sender] = PublicKey({
        X: uint256(publicKeyX),
        Y: uint256(publicKeyY)
        });
    }

    function stealthTransfer(
        address stealthRecipient,
        uint256 tokenId,
        bytes32 publishedDataX,
        bytes32 publishedDataY
    ) public {
        _transfer(msg.sender, stealthRecipient, tokenId);
        emit StealthTransfer(stealthRecipient, publishedDataX, publishedDataY);
    }

    //...
}

Implementing getStealthAddress

This method is used to get the stealthRecipient address. The formula is the following:

  • Sender chooses a secret value and computes some publishedData
  • publishedData = G ∗ secret
  • sharedSecret = P ∗ s
  • stealthPublicKey = P + G ∗ sha3(sharedSecret)

⚠️ For a maximum of privacy, I highly recommend you to calculate it offline.

contract StealthNFT is IStealthNFT, ERC721 {
    //...

    function getStealthAddress(
        address recipientAddress,
        uint256 secret
    ) public view returns (
        address stealthAddress,
        bytes32 publishedDataX,
        bytes32 publishedDataY
    ) {
        require(publicKeys[recipientAddress].X != 0);
        uint256 tmpPublishedDataX;
        uint256 tmpPublishedDataY;

        (tmpPublishedDataX, tmpPublishedDataY) = derivePubKey(secret);
        publishedDataX = bytes32(tmpPublishedDataX);
        publishedDataY = bytes32(tmpPublishedDataY);

        PublicKey memory pubKey = publicKeys[recipientAddress];
        (tmpPublishedDataX, tmpPublishedDataY) = EllipticCurve.ecMul(secret, pubKey.X, pubKey.Y, AA, PP);
        uint256 hQ = uint256(keccak256(abi.encodePacked(tmpPublishedDataX, tmpPublishedDataY)));

        (tmpPublishedDataX, tmpPublishedDataY) = derivePubKey(hQ);
        (tmpPublishedDataX, tmpPublishedDataY) = add(tmpPublishedDataX, tmpPublishedDataY, pubKey.X, pubKey.Y);
        stealthAddress = address(uint160(uint256((keccak256(abi.encodePacked(tmpPublishedDataX, tmpPublishedDataY))))));
    }

   //...
}

Compute your private key with JavaScript

To know if you received an NFT you'll have to read the StealthTransfer events and applies this formula:

  • Create a point from publishedData
  • sharedSecret = publishedData ∗ P
  • stealthPrivateKey = P + sha3(sharedSecret) % n

If the public key of stealthPrivateKey is the same as the stealthRecipient you are the receiver.

const { keccak256 } = require('ethers/lib/utils')
const { Point } = require('@noble/secp256k1')
const { BigNumber } = require('ethers')

const EC = require('elliptic').ec
const ec = new EC('secp256k1')

;(async () => {
  const data = {
    stealthRecipient: '0xB9c8ec9cE5e018A7973d57AE1c74C6E697aC0D96',
    publishedDataX: '0x7701a569affde4c369884dcc58852f733f5e0d35042295a151ac07e72588260d',
    publishedDataY: '0xd793591c89ac13c8a85b29e2f4245870f673b0dc366169346865941b5f1a9465',
  }
  const privateKey = '0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef'
  const pD = new Point(BigInt(data.publishedDataX), BigInt(data.publishedDataY))
  const sharedSecret = pD.multiply(BigInt(privateKey))

  const stealthPrivateKey = BigNumber.from(privateKey)
    .add(keccak256(`0x${sharedSecret.toHex()}`))
    .mod(`0x${ec.n.toString(16)}`)
})()

Final code

StealthNFT.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.16;

import { ERC721 } from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import { IStealthNFT } from "./interfaces/IStealthNFT.sol";
import { EllipticCurve } from "./library/EllipticCurve.sol";

contract StealthNFT is IStealthNFT, ERC721 {
    uint256 public constant GX = 0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798;
    uint256 public constant GY = 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8;
    uint256 public constant AA = 0;
    uint256 public constant BB = 7;
    uint256 public constant PP = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F;

    struct PublicKey {
        uint256 X;
        uint256 Y;
    }

    mapping(address => PublicKey) public publicKeys;

    constructor() ERC721("Stealth NFT", "SNFT") {}

    function mint(address recipient, uint256 tokenId) public {
        _mint(recipient, tokenId);
    }

    function register(bytes32 publicKeyX, bytes32 publicKeyY) public {
        address pub = address(uint160(uint256((keccak256(abi.encodePacked(publicKeyX, publicKeyY))))));
        require(msg.sender == pub);
        publicKeys[msg.sender] = PublicKey({
        X : uint256(publicKeyX),
        Y : uint256(publicKeyY)
        });
    }

    function stealthTransfer(
        address stealthRecipient,
        uint256 tokenId,
        bytes32 publishedDataX,
        bytes32 publishedDataY
    ) public {
        _transfer(msg.sender, stealthRecipient, tokenId);
        emit StealthTransfer(stealthRecipient, publishedDataX, publishedDataY);
    }

    function getStealthAddress(
        address recipientAddress,
        uint256 secret
    ) public view returns (
        address stealthAddress,
        bytes32 publishedDataX,
        bytes32 publishedDataY
    ) {
        require(publicKeys[recipientAddress].X != 0);
        uint256 tmpPublishedDataX;
        uint256 tmpPublishedDataY;

        (tmpPublishedDataX, tmpPublishedDataY) = derivePubKey(secret);
        publishedDataX = bytes32(tmpPublishedDataX);
        publishedDataY = bytes32(tmpPublishedDataY);

        PublicKey memory pubKey = publicKeys[recipientAddress];
        (tmpPublishedDataX, tmpPublishedDataY) = EllipticCurve.ecMul(secret, pubKey.X, pubKey.Y, AA, PP);
        uint256 hQ = uint256(keccak256(abi.encodePacked(tmpPublishedDataX, tmpPublishedDataY)));

        (tmpPublishedDataX, tmpPublishedDataY) = derivePubKey(hQ);
        (tmpPublishedDataX, tmpPublishedDataY) = add(tmpPublishedDataX, tmpPublishedDataY, pubKey.X, pubKey.Y);
        stealthAddress = address(uint160(uint256((keccak256(abi.encodePacked(tmpPublishedDataX, tmpPublishedDataY))))));
    }

    function invMod(uint256 val, uint256 p) pure internal returns (uint256)
    {
        return EllipticCurve.invMod(val, p);
    }

    function expMod(uint256 val, uint256 e, uint256 p) pure internal returns (uint256)
    {
        return EllipticCurve.expMod(val, e, p);
    }


    function getY(uint8 prefix, uint256 x) pure internal returns (uint256)
    {
        return EllipticCurve.deriveY(prefix, x, AA, BB, PP);
    }


    function onCurve(uint256 x, uint256 y) pure internal returns (bool)
    {
        return EllipticCurve.isOnCurve(x, y, AA, BB, PP);
    }

    function inverse(uint256 x, uint256 y) pure internal returns (uint256,
        uint256) {
        return EllipticCurve.ecInv(x, y, PP);
    }

    function subtract(uint256 x1, uint256 y1, uint256 x2, uint256 y2) pure internal returns (uint256, uint256) {
        return EllipticCurve.ecSub(x1, y1, x2, y2, AA, PP);
    }

    function add(uint256 x1, uint256 y1, uint256 x2, uint256 y2) pure internal returns (uint256, uint256) {
        return EllipticCurve.ecAdd(x1, y1, x2, y2, AA, PP);
    }

    function derivePubKey(uint256 privKey) pure internal returns (uint256, uint256) {
        return EllipticCurve.ecMul(privKey, GX, GY, AA, PP);
    }
}