Customizing nodeos for your dApp
What if your dApp environment needs to know about specific events in your contract and specific changes in its state memory? The usual way would be to write a poller script which would periodically retrieve this data from a
nodeos server via its HTTP interface. Slow and inefficient, and you depend on the remote server provider not to block your excessive traffic.
Much more can actually be done at a moderate cost: you can create your own, application-specific plugin for
How ZMQ plugin works
Let’s have a short look at the plugin source code. Around the bottom of the page, it adds itself to
chain.applied_transaction signal handler. It means, our callback is executed whenever there’s a transaction in a signed block that arrived from the BP. Around Line 115, the plugin dissects the transaction into action traces, and each action trace is also recursively searched for inline actions.
What happens next, the plugin finds all accounts involved in the transaction. There’s also a workaround for
eosio system account actions, such as
buyram, because they don’t leave a trace, and we need to know who was involved .
Then, for each involved account and each involved token cointract, it retrieves the current token balance. Basically it reads from the token contract’s
accounts table. This is important to understand how we can customize this.
Once all data is collected, it is transformed into a JSON object and spit out the ZMQ PUSH socket. The difference between PUSH and PUB sockets is that PUSH socket will block if the reader is not ready. The PUB socket will just discard the message if the reader cannot read it.
How your own plugin works
Now that we understand the mechanics, we can use them for our own dApp.
The plugin would ignore all transactions that are happening with other contracts, and only react on transactions that are happening around our own contracts. This filtering is easy to implement with
std::set<name> array that will let you easily check if
at.act.account (the contract account where the action was executed) or
at.receipt.receiver (the receiver contract account which may differ from the former) is your own contract account.
Once we determined that this is an action executed on our contract, we can dig through the action traces and collect data depending on the action name. For example, an action creates an entry in a table. The contract also stores the ID of the new record in its state table. So, the plugin can easily determine the ID of the record, read the record itself, and add it to the resulting JSON that will be pushed toward the ZMQ socket.
If your dApp is tolerant to missing such events (for example, a poller script is executed every few minutes to do the cleanup), you can use a PUB socket in your plugin instead of PUSH socket. In this case,
nodeos will not be paused whenever the receiver stops or its input buffer overflows.
Setting up your server infrastructure
In my previous articles [1, 2] I explained the steps needed to launch your own instance of
nodeos. A fairly moderate server will do the job. You would need 8GB RAM, mainly because the EOS compilation requires that, and at least 100GB of SSD storage. If you have a possibility to have an additional HDD partition, the blocks log can be written there instead of SDD. But it’s highly recommended to have the state database on SSD storage.
You would also need an instance of
nodeos which would be shut down periodically, in order to make a snapshot of blockchain state. In case of abrupt crash of the main
nodeos process, you would be able to restore the state database quickly, without having to re-play the whole blockchain again. I’m going to experiment with ZFS, as it’s designed for quick and cheap snapshots.
I’ll be happy to offer my consultancy or take the whole implementation, or even outsource the hosting and operations for such a service. Feel free to contact me at firstname.lastname@example.org for more details.