A majority of the NFT projects out there follow the pattern of pre-reveal → revealed through the update of the contract’s base token URI. What this means is the whole collection has to be revealed at once. What if I am not buying Interesting Kangaroo Flying Car Club, but something like this…?
People trade cards, both revealed and sealed in a pack. Some buyers like to buy a sealed pack to get that adrenaline rush from opening the pack. Some sellers are afraid that the cards inside the sealed pack are just not so rare ones, lowering its cash value. Watch this video from Vice to see how people trade these cards. It’s pretty interesting.
Courtyard recently did a Pokemon card pack drop and it gives owners the option to reveal what’s inside. How is it able to do it?
The basic idea is this: Courtyard is able to control the data returned for any token IDs via a centralized server, without compromising immutability. I bet you think this sounds counterintuitive right now, centralized and immutable?
Pre-reveal metadata
This is a sample metadata that I downloaded from one of the sealed packs.
Something immediately caught my eyes. There is a field called proof_of_integrity
, which is a hash. We can probably guess that the NFT’s data is used to generate this hash. The first 4 and last characters of the hash is also included in the token’s name
.
Post-reveal metadata
Now let’s look at the metadata of a revealed Pokemon card.
As expected, we are able to see a fingerprint
that is used to generate the hex_proof
. But what is the salt
for?🕵️
The token’s fingerprint is “Graded Pokemon TCG | PSA 55990818 | 1996 Pokemon Japanese Base Set Holo Gyarados #130 | PSA 9 MINT”.
TCG = Trading Card Game
PSA = Professional Sports Authenticator, a trading card authentication and grading company
Gyarados = The Pokemon
The data included in this fingerprint is predictable, I can probably brute force the hashes by randomizing the year manufactured, the Pokemon’s name and the card’s language?
Not so soon anon…if you have brought any experience from coding in web2, you will immediately realize the salt
is used to prevent a rainbow table attack.
What is a rainbow table attack?
In a web application, passwords are (supposedly) never stored in plain-texts. They are stored in a one-way hash so not even the database administrator has access. However, hackers can curate a long list of passwords and create a key value mapping of passwords and their respective cryptographic hashes.
A rainbow table is a precomputed table for caching the output of cryptographic hash functions, usually for cracking password hashes.1
What if the database access is compromised and the hacker has all the hashed passwords that can be compared using the rainbow table? Then the passwords are practically revealed.
So, the purpose of having a salt (a random number) is to combine it with the password before hashing it, so that the same password will turn into a different hash in different databases, rendering the rainbow table useless.
(Now back to Courtyard)
Proof of integrity
Okay, now that we have the fingerprint
, the salt
and the hex_proof
, where do we prove the metadata has not been tampered with by Courtyard’s malicious developers??? In the proof_of_integrity
JSON, there is a field called proof_contract
. Looks promising, let’s try that.
In the ProofOfIntegrity contract, there is a function generateProof
and another function verifyProof
. Let’s input the data from the Gyarados metadata and see if the proof is valid.
The proof is valid! :-)
The contract is very simple, it just checks the keccak256
hash of the concatenation of the fingerprint and the salt is equal to the proof.
From hex proof to token ID
Another interesting design is that all Courtyard’s current and future NFTs live in the same smart contract called the CourtyardRegistry. Unlike most generic NFT collections, Courtyard’s token IDs are not in sequence from 1 to 10,000. The token ID is actually the bytes32
hex proof converted into an uint256
! This is nothing short of genius.
No matter how many NFTs Courtyard will launch in the future, there will practically never be a hash/token ID collision, 2 ** 256 - 1
is simply astronomical.
Now, where does the proof of integrity come from when a user mints the Pokemon?
What happens during the drop
The CourtyardRegistry
contract is access controlled and only allows addresses with the minter role to mint NFTs.
It trusts whichever minter to provide the correct proof and does no further validations before minting the token.
In Courtyard’s Back To School Pokemon drop, the privileged minter is this contract. It inherits from 2 contracts called the TokenDropBase
and TokenMintingPool
. The contract owner is able to add precomputed token hashes to an array called _availableTokenHashes
, which is later used to take the next available token hashes to be minted.
Before we move on to the actual mint function, we need to take a small detour to look at how randomness is done in Courtyard.
Just by looking at the contract it inherits from and the use of these 3 private variables, it is obvious that Courtyard uses Chainlink’s Verifiable Random Function to generate a random seed to be used for the mint. The process of requesting a Chainlink random seed is as follows:
Make sure the contract has enough LINK to pay for VRF and then request for randomness. The VRF key hash is provided by the contract deployer.
Randomness cannot be generated on-chain within the same transaction. It must be generated off-chain and is returned in a separate transaction.
With the random seed being set, based on each minter’s mint amount, the contract will pick available token hashes using the remainder of the random seed divided by the remaining token supply. Isn’t this quite slick!
The reveal
I don’t see any functions on-chain and I believe it happens off-chain through an API call.
Caveats
Courtyard’s protocol is impressive because it is able to prove metadata immutability while having a centralized metadata API. That said, the base token URI is still a centralized API and there is a risk of server outage.
Also, I am curious to why Courtyard decided not to use a merkle root and instead whitelists addresses one by one.
It looks like the team thinks that by not saving the addresses in plain-texts, user privacy can be protected before the presale. (Using a merkle tree will normally expose the whitelist in the frontend, as the frontend needs the list to generate merkle proofs. But maybe they can also wait until the very last moment to publish the list?)
There is also an incident I noticed on Discord where someone made a bid to a sealed pack on OpenSea, then the buyer opened his pack, saw that it was not Charizard, and accepted the bid.
If the token has an on-chain flag that tells the bidder and the marketplace whether the pack is revealed, the marketplace’s order matching smart contract might be able to take it into consideration to prevent the malicious act above from happening.
https://en.wikipedia.org/wiki/Rainbow_table
Looking through this I don't see any implementation for the burn functionality. Do you have any thoughts on how this is achieved since the Registry contract does not implement ERC721Burnable?
how is the salt generated?