It seems like every month now, there is a new fully on-chain project that crops up. This past fall we had Anonymice. Soon after came Chainrunners. And now Flipmap, a derivative of Blitmap, seems to be the latest big one. Each of these have their own approach, but the one thing that separates them from 99% of projects out there: fully on-chain.
There are multiple flavors of on-chain NFT’s. Dom does a great job explaining in this tweet:
For the non-technical folk reading this, from top to bottom each flavor of NFT is storing and doing more things on the blockchain. The top-most solution, simply stores what is affectively a URL to the image on the blockchain. The major issue with this, is that the person storing that image for you can simply stop. And now your NFT is pointing to nothing.
As we move down the list, each solution stores all of the data describing the NFT and its image somewhere on the blockchain. This provides more assurance of permanence (your NFT is as sure as Ethereum itself), and reduces the number of external requirements to view and interact with your NFT.
But also there is a positive externality to fully on-chain NFT’s: composability. Because the NFT’s traits and image data are all stored in a smart contract, developers can build on top of the NFT and create derivative projects more easily. This is what Flipmap did by generating all of the “lost” Blitmap’s, except flipped their image. The developers could do this completely on-chain because Blitmap’s image data (pixel indices & RGB colors) are all stored on their smart contract.
On-chain projects are still fairly rare; the vast majority of NFT’s are stored on a centralized solution and don’t even make Dom’s list. However I do think the benefits of on-chain are huge and so they will become more popular, especially as Ethereum becomes more scalable.
On-chain art oftentimes is no more than 32x32 pixels. This is not necessarily an aesthetic choice, but rather it is a constraint due to the limitations of storage and computation on Ethereum. To store a byte of data in regular Ethereum storage (SSTORE), it costs roughly 30 cents. This means if you’d like to store your 3 MB iPhone photo, you’d have to pay close to $1,000,000 USD in gas fees. And so in order to avoid, selling their kidneys, on-chain developers have opted for creating smaller, pixelated images.
It’s not just about storage though. In order to achieve Dom’s three Michelin star NFT rating, the contract needs to also render the image, rather than relying on some outside service for rendering. This becomes difficult due to gas constraints when reading from contracts. That’s right, not only does it cost gas to store data on-chain, but there are also gas limitations when it comes to reading and computing data.
I have seen a number of interesting rendering techniques, however the most common is rendering the image as an SVG like in Anonymice, Chainrunners, and many more. Due to this being a proven technique in many other projects, it’s how I chose to implement my own on-chain project: Pixelations.xyz.
At this point, I’m going to get pretty technical into my project, and so if you just want the high level, you can skip this and head over to my project overview here.
If you are into the technical stuff, and don’t care about the high-level, here’s the TLDR on Pixelations:
On our website, we let the minter provide a base image. We then perform a number of off-chain image processing steps in order to viably store and render the image on-chain. The result is a 32x32 Pixelated NFT.
The very first thing that needs to be done when someone provides an image, is we need to crop it into a square. Rather than guessing which part of the image should be cropped, we integrated with the react-image-crop library so that the minter may choose.
From there, we post the full image and some metadata regarding how to crop it to a Python API we built. This endpoint crops and pixelates the image into a 32x32 RGB image using the Pillow library. At this point, we’ve successfully taken an image that could be any size, and shrunk it down to 3 KB of data.
From here, we extract the 32 colors that best represent the image using k-means. K-means is a clustering algorithm, that groups data points together into k clusters. For Pixelations, we use k=32, and the data points are RGB values. Using only 32 colors, allows us to represent each pixel as an index into an array of colors, enabling us to compress the image even further. At this point, we’ve compressed it down to 1 KB.
But there is even more compression we can get out of it. Since there are only 32 colors, we don’t need all 8 bits in a byte to represent a color. In fact we only need 5 since 2^5=32. And so we pack the bits together. Now we’ve completed our compression and the output is a bitmap of 736 bytes.
736 bytes is still a considerable amount to be storing on a contract. We could’ve performed even more compression like RLE (used by Nouns), however this would’ve resulted in Pixelations differing in storage sizes. I liked the simplicity of being able to accept a fixed number of arbitrary bytes. The great thing is that we discovered an innovative way of reducing gas storage costs: SSTORE2.
There are multiple ways to store data in Ethereum. The most common is SSTORE, which is used whenever you write to one of the properties in your smart contract. However the folks at Sequence realized that storing data as code in a new smart contract is significantly cheaper than SSTORE for larger amounts of data.
They built a solidity library called SSTORE2 that allows you to write arbitrary data as contract code and then read that data later on. And due to how much data we were storing on-chain for each Pixelation (736 bytes), we were able to cut our gas storage costs in half. The result for us is that whenever a new Pixelation get’s minted, it costs 385,000 units of gas to mint (between 0.01Ξ and 0.04Ξ depending on gas prices).
When it came to rendering, we borrowed a lot of code from Chainrunners. We deployed two contracts, an ERC-721 contract and a rendering contract. This allows for upgradability, and in the case of a bug in the rendering contract, the ability to actually fix it.
Our ERC-721 contract stores the data using SSTORE2, and at a high-level our rendering contract iterates through each of the 1024 pixels and constructs an SVG rect for each pixel. If you peak at the code though, we’re doing a lot of little hacks like pre-base64 encoding metadata and SVG data, and many other things in order to reduce gas usage as much as possible.
After all of the sweat needed to pull this off, we now have a minting site where anyone can provide an image of their choosing, we’ll compress it down to 736 bytes, ship an entirely new contract for storing that NFT’s data, and render the pixelated image at run-time.
Check it out at Pixelations.xyz.
We are very proud of this project, regardless of what happens. It has pushed us in many ways, and every time a new Pixelation get’s minted, we get to see our hard work in action.
Thanks for reading this far. Cheers @everyone!