changed 4 years ago
Linked with GitHub

ZK NFTs

(written for 0xPARC applied ZK learning group final project proposal)

A spec for NFTs with default private ownership. Can easily be made ERC721 compliant.

NFTs come in two types:

  • shielded: ownership is not known. if this is ERC721 compliant, all shielded NFTs have ownerOf() return contract address
  • unshielded: ownership is known. if this is ERC721 compliant then this is just a regular NFT

ZK NFTs can also be implemented as wrappers around existing ERC721 contracts.

State variables

uint256[] ids
MerkleTree utxoHashes
uint256[] nullifiers
mapping(uint256 => address) knownOwners

Data types

UTXOs are ownerAddress | nftID | salt

Functions

mint(id, address):

  • purpose: mints an unshielded NFT with public ID id and public owner address
  • check id not already minted
  • id is appended to ids
  • set knownOwners[id] = address

(optional) mintShielded(id, utxoHash, proof):

  • purpose: mints a "shielded NFT" with public ID id and private owner (and private salt)
  • utxoHash is H(ownerAddress | nftID | salt).
  • id is appended to ids
  • utxoHash is added to utxoHashes
  • proof is ZKP that utxoHash is indeed the hash of a secret ownerAddress and secret salt and publicly-declared id.

transferShielded(nullifier, newUtxoHash, proof):

  • purpose: transfers ownership of a shielded NFT
  • check that nullifier is not already in nullifiers, then appends it to the end of list
  • check that newUtxoHash doesn't already exist (would mean salt is reused, which is bad)
  • proof is ZKP (provided by recipient) that recipient knows five secret values: oldOwner, newOwner, oldSalt, newSalt, id such that H(oldOwner | id | oldSalt) is in utxoHashes (merkle proof), G(oldOwner | id | oldSalt) = nullifier, and H(newOwner | id | newSalt) = newUtxoHash
  • (optional): is there a way to make it so that sender doesn't have to reveal their address to recipient? probably yes, if you have a sender-provided proof and a recipient-provided proof

unshield(address, id, salt, merkleIdx):

  • purpose: reveals that address owns NFT with id id, turning it into an unshielded NFT
  • contract hashes G(address, id, salt) (nullifier) and verifies it is not in nullifier set already (so this UTXO is not already spent). then appends it to nullifier set
  • contract verifies that H(address, id, salt) == utxoHashes[merkleIdx] (merkleIdx is provided for fast lookup)
  • contract sets knownOwners[id] = address

shield(address, id, utxoHash, proof):

  • purpose: turns an unshielded NFT into a shielded NFT
  • check address != 0
  • check knownOwners[id] == address, then set it to 0
  • proof is ZKP that i know secret value salt such that H(address | id | salt) = utxoHash
  • check utxoHash doesn't already exist (would mean salt is reused, which is bad)
  • add utxoHash to utxoHashes

(optional) proveOwnershipOfSomeNFT(address, stateRoot, proof):

  • purpose: ideally this would be a view function proving that this address owns some NFT in the pool (at least, the pool with state root stateRoot, which is hopefully a recent state). proofs can be augmented to prove additional properties as well about the NFT owned
  • Right now this is hard because you have to prove non-inclusion in the nullifier set. Not only that, but you also have to prove that there is no salt such that the id, salt, and address are in the nullifier set
  • one dumb solution is to make the nullifier set a max size (1000) and the range of possible salts small (ie 10). so you just have to prove that the address and secret id, when hashed with each possible salt, is not any of the nullifiers (nullifier set size * valid salt set size).

  • or with this construction, you can just prove that for each salt in [0, ..., MAX_SALT], the value associated with key hash(address, id, salt) in your merkelized mapping is false. but now your merkelized mapping must have a root corresponding to a root tracked in the contract
  • note that this actually does NOT require proof of knowledge of private key (and therefore, does not require ECDSA snark)

  • or this

escrow contracts

shielded NFTs can be auctioned, with transfer and payment occurring in the same atomic transaction. these contracts will require ZKPs as well that are proxied to the shieldedTransfer function that is called internally

Select a repo