owned this note
owned this note
Published
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).
![](https://i.imgur.com/MofXQWS.png)
- 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)
![](https://i.imgur.com/7tsFVtM.png)
- 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