The making of EthMadness — a responsive dApp with only client side code
For a recent Hackathon weekend at Nodesmith, we built EthMadness — an NCAA March Madness bracket challenge running on Ethereum via a smart contract. We were thrilled that over 300 unique addresses participated — with 480 entries for the men’s tournament and 220 for the women’s tournament.
The app had a few rough edges, but a number of people complimented the UI polish of EthMadness. In this post, we share details on how we were able to build a responsive app without writing any server side code.
The bracket submission section of the app has little interaction with the Ethereum network (just a single write), so this post focuses on the Leaderboard section of the dApp, and how we used Nodesmith’s smart contract event cache to make that page work well. We also give some more details on the architecture of the event cache itself.
How the leaderboard works
The leaderboard page shows all the submitted brackets for the contest and their current score. When a user submits an entry, the submitEntry solidity function emits an event log which contains the user’s picks, their address, and their bracket’s name. When the leaderboard page loads, it retrieves all of those logs, scores each entry with the current results, and renders the leaderboard table. To retrieve the logs, we use the web3.js library and the getPastEvents function of the EthMadness contract.
Using event logs for storage allows you to build an dApp that doesn’t have a server side component — but there’s a problem with this approach — reading those logs can be quite slow (a very common pain point when Ethereum developers were surveyed).
When calling this method on the Ethereum mainnet, connected to a regular Parity or Geth node, it takes 10+ seconds to read all of the logs from the contract. Worse, the read time gets slower as more brackets are submitted. This is unacceptable from a user experience point of view — users expect response times to be in milliseconds.
Currently, the only way to solve this is to build out a data cache and populate the app UI with cached data instead of reading directly from Ethereum. This turns out to be more difficult than it sounds — you have to account for chain re-orgs, which is non-trivial. And that leaves you with an app with a server side component.
Give me faster reads
This was a problem that we accounted for when building Nodesmith. Getting performant event log reads for EthMadness was as simple as configuring the app to point to Nodesmith’s JSON RPC endpoints rather than the default or injected provider.
A critically important point — the provided Nodesmith JSON RPC endpoints are fully compliant with the Ethereum spec, we make no use of proprietary APIs for this functionality. Your app’s code doesn’t need to change to take advantage of faster event log reads.
In our application, we can create the same web3 object and use the same APIs as before, but configure it to point to Nodesmith endpoints. This time, instead of hitting a Parity or Geth node, Nodesmith will use the event cache to quickly grab the event logs we’ve requested. Compared to vanilla nodes, this returns nearly instantly.
To demonstrate the difference, we created a version of the leaderboard which issues the same request to Nodesmith, Infura (which runs vanilla nodes), and the injected MetaMask if available. You can try it for yourself here.
How the Event Cache Works
The event cache is built on a Nodesmith managed database that is built for speedy retrieval. To build the initial database, we enumerated through and stored every log from every transaction in every historical block (yeap — this is as huge as it sounds).
After that, all new blocks are monitored and each log is added to the cache. Occasionally a reorg on the network will occur — this is one of the more difficult situations to handle technically. Our solution does the tricky work to remove the invalid block’s logs from the cache to ensure the log remains correct.
When a request comes in to the eth_getLogs endpoint (which the getPastEvents method uses), Nodesmith parses the request and returns data from the cache instead of propagating that request to the underlying node. To ensure we are returning accurate data, we also send the request on to a regular Ethereum node and validate the result was equivalent. If a difference is found, we trigger an alert so we can investigate.
The payload returned is the exact same payload that the underlying node would return, which allows a developer to easily swap between Nodesmith, local testnets, or an injected web3 provider. We believe this flexibility is critically important to the health of the Web 3.0 ecosystem.
We’ve built an extensive monitoring framework around our node infrastructure, including the event cache. This gives us the confidence to serve requests from our service instead of sending requests to a regular node. As a final backup plan, if the event cache service ever fails, we simply return the results from a regular node.