Building public crypto art in the Ethereum network
Being inspired by r/place and willing to get hands dirty with blockchain smart-contracts, we recently came across the idea of implementing Ethereum Dapp to paint colourful pixels on a 1000px wide canvas in real-time, collaborate with friends and eventually have fun. Painting shall be free-for-all and we only expect user to cover transaction gas expenses, while it is being confirmed by miners in the Ethereum network. Thus we have released Ethplace.io and are eager to share what did it take us to build this product from scratch.
In this article we would like to give a short technical overview of this project as well as dive into technical details, in particular smart contract implementation.
Given the fact that we were focusing mostly on Ethereum ÐApp implementation, we had no intention to make complicated architecture for reading and writing data into smart contract as well as taking care of data persistence, which is already assured by blockchain.
Though there are few prerequisites that users shall have either Ethereum compatible browser (eg. Mist) or plugin like Metamask installed, in order to be able to paint selected pixels and our server takes care of fetching latest state from blockchain and serving that through Nginx to end-user.
Therefore, we worked out following setup which consists of:
- Ethereum ÐApp — a smart contract written in Solidity and deployed to mainnet.
- Client-side application — a pure Vanilla JS application which relies on web3.js library and requires either Ethereum compatible browser or plugin for smart contract interoperability.
- Server-side API — a standalone app written in Scala using Web3J library for blockchain interoperability.
- Parity node — a single node running in DigitalOcean cloud to keep in sync with blockchain network.
The first thing we decided to take up was a smart contract implementation. Taking into account that it was our first experience designing and implementing smart contracts, we thought it might be a good idea to define state model contract and controller contract as separate entities. One of the huge benefits is having state and controller separately is the ability to deploy new versions of controller without affecting state and migrating the persisted data from one contract to another.
So far, the only disadvantage we noticed when we deployed contracts to mainnet is the initialisation costs which have been relatively high taking into account the current Ethereum price (at the time of writing 1ETH = ca $1,100).
Initial naïve approach
First of all, we didn’t worry much about efficiency and optimization and came up with following robust implementation that stored every pixel sequentially in an array, which had fixed size of 1 million and uint256 data type for every stored element.
At first, this seemed to be a very straight-forward implementation that worked fine for us although with few caveats:
- We tried to read all persisted pixels and immediately failed with timeout due to huge response payload.
- In addition, storing every single pixel in uint256 data type wasn’t cost-effective at all.
Looking back at our first pixels’ storage implementation, things turned out to be more complicated than we had initially imagined and we were quite frustrated with this fact.
Nevertheless, we teamed up to get our solution re-implemented and figure out how to make data more compressed and eventually came up with this idea.
Before, we had represented a single pixel as 1 unit of uint256 data type, therefore requiring 1 million length array to persist all data.
Let’s have a look at Solidity data types and ensure that `uint` type actually stands for `uint256` — an unsigned 256bits integer, which can be used much more efficiently if we apply bitwise operators and therefore encode both pixel x and y coordinates and its color into array element index applying bitwise shift.
Sounds promising, huh? This is the case when bitwise magic comes to the rescue.
We only require 4 bits per pixel to store both its X and Ycoordinates and color if we apply bitwise operators and calculate bitmask from the excerpt below:
Let’s try to evaluate what we have achieved by this implementation.
Origin array size of 1 000 000 pixels / 64bits = 15 625 elements of uint256 data type meaning we have reduced initial array size by 64 times.
Full code snippet of improved implementation is available below:
Interacting with smart contracts
In order to be able to read persisted pixels from smart-contract we have defined just couple of functions like:
function getPixel(uint coordinate) public view returns (uint)
function allPixels() public view returns (uint256)
The initial view of application UI was to make it easy-to-use and let user focus on pixels’ field, having a minimal amount of required extra tools, such as zoom and definitely our fancy color picker.
Painting selected pixels is implemented relying on Canvas API, which is easy to use from developer’s perspective and relatively fast in end-users browser.
Moreover, we had to take care of those end-users, who have neither Ethereum compatible browsers nor supported plugins or are willing to explore application on mobile phone or tablets. Instead of reading state directly from smart-contract on the client-side, we have delegated latest state rendering and blockchain polling to server and eventually have just to fetch latest state in png format served by Nginx server if there were any changes made by other users during the time elapsed.
In order to paint selected pixel we fully rely on web3.js library. Code-snippet showing how to call smart contract function is available below:
Since we had concerns how to deliver full smart contract state to users without Ethereum compatible browsers, we decided to implement lightweight server API and bootstrapped Parity node in DigitalOcean cloud environment.
A small server-side application was developed to poll locally running Parity node every 15 seconds and subscribe to PixelChangedEvents emitted by smart contract and eventually generate current pixels state as an image in png format.
Unfortunately, generating an image out of smart contract state was quite tricky task due to chosen uint256 data type cause we have to deal with following JSON payload:
in order to generate an image out of received data.
A code snippet for above-mentioned image generation in Scala is defined below:
What have we learned
Overall, this has been an extremely fun and challenging pet project for our team, where we teamed up to work on collaborative tool and gained valuable experience working with blockchain as well.
The current initial version of Ethplace.io application will be definitely changing in the near future and we are eager to unveil some more interesting features we are now working on.
Yours Blockchain Enthusiasts crew.