DEV Community

Cover image for Hooked #5: Consensus
Wietse Wind
Wietse Wind

Posted on • Updated on

Hooked #5: Consensus

Please note

Since this blog was published a lot has happened. The Hooks Amendment is now running on a public testnet and has a public "Builder" where you (devs) can code Hooks and deploy them to the testnet, straight from your browser.

Warning: this is a highly technical blog 🤓 🤓 🤓

The Story So Far...

In Q4 of 2020 we released a Tech Preview of Hooks. This preview consisted of a docker container which would run a single instance of xrpld-hooks in stand-alone mode. As the name implies this was for the purpose of demonstrating how hooks will look in the future (without being a production-ready amendment.) Since then we have successfully replaced the web assembly runtime engine Wasmer with SSVM as part of a drive toward production grade efficiency. (See our previous blog.)

Testing Consensus

The 'stand-alone' mode of our Q4 2020 Tech Preview release meant that xrpld did not not communicate with other validators. It is a singular centralised validator simulating a decentralised ledger. This simulation is very useful for debugging and demonstrating behaviour, but achieving real consensus is very important. Without consensus the project can never produce live decentralised ledgers and therefore can never go into production on the live network. To begin tests of consensus we span up a two private testnet validators at our office.

At our first try everything worked as planned (🎉!) except for emitted transactions (that's fixed now). Emitted transactions are produced when a hook exercises its right to create new transactions separate to the triggering (original) transaction: for example the carbon hook emits a 1% payment to a carbon offset address every time the owner of the account sends a payment to someone. This is an emitted transaction.

Emission and Dispute

On our internal testnet the 'carbon hook' fired correctly on both validators and produced the same output. However a race condition emerged due to the way emitted transactions are queued for application to the next ledger.

Our initial strategy for emitting transactions was to insert the emitted transaction directly into the transaction queue, through xrpld's network interface, as though it were submitted by a special user.

One validator would always produce the emitted transaction slightly sooner than the other, and it would propose that transaction to the other validator. The other validator would not be able to find the transaction and would request a copy of it. The first validator would send a copy but the second validator would reject it and mark it as bad because emitted transactions cannot be received over the network (since they lack signatures this would be a massive security issue.)

As a result one validator would always have the emitted transaction and the other would never have it, therefore no consensus could be reached on emitted transactions.

Consensus Protocol to the Rescue

In most cases of consensus failure the answer is to use a consensus protocol to achieve agreement between otherwise disagreeing parties. Fortunately we don't need to reinvent the wheel here, xrpld already contains a consensus protocol called the Ripple Consensus Protocol.

To achieve consensus on emitted transactions we now emit them into a special directory on the ledger itself rather than emitting them directly into the transaction queue. This means all validators have a chance to produce and agree on what the emitted transactions indeed are before they attempt to queue them for application to a future ledger.

Now when a transaction triggers a hook which in turn emits a transaction, the validated triggering (originating) transaction's metadata looks like this:

The closed ledger looks like this:

Processing Emitted Transactions

To apply the emitted transactions to the next ledger xrpld-hooks reads the directory created above as soon as the last ledger closes, and injects those validly emitted and agreed upon transactions into the next ledger at the time it is created. This way, provided the validators achieved agreement on the last ledger they already have accepted the emitted transactions in the next ledger.

Common transaction fields have been modified to optionally include FirstLedgerSequence (as distinct from LastLedgerSequencewhich is already present in all transactions). FirstLedgerSequence is now mandatory on all emitted transactions. To begin with the field will always point to the next ledger (that is: the ledger after the ledger in which the transaction was emitted.) However in future it may be changed to allow a validator-votable figure. In short: this prevents emitted transactions being applied (executed) in the same ledger they were emitted, which could otherwise produce infinite loops (e.g. a hook emits a tx which triggers a hook which emits a tx etc.)

If FirstLedgerSequence is expanded it will allow validators to hold emitted transactions for future execution some specified number of ledgers later. This can be useful for on-chain bots, for example: Automated Market Makers which might need to periodically wake themselves up to do internal accounting even when they are not triggered by any external party.


The triggering (originating) transaction causes the emitted transaction to be added to the emission directory on the ledger. But once added that entry also needs to be removed or xrpld will continue to attempt to apply the same emitted transaction to every future ledger.

In xrpld ledgers are modified by transactions. It's not possible to just clean up the old emitted transactions. The modification of the ledger to delete the nodes associated with those emitted transactions in the emission directory needs to be caused by some transaction.

Fortunately we have a transaction: the emitted transaction! Every emitted transaction executes a callback to the hook that emitted it. This is important because without a callback the hook would have trouble knowing if its emitted transactions were eventually accepted by the network (and therefore whether or not, for example, a payment actually occured.)

Callbacks allow the hook to do internal accounting, modify its own state, and importantly now also automatically trigger a cleanup on the emission directory for that transaction. Callbacks cannot emit transactions or cause transactions to be rejected.

A validated emitted transaction now looks like this:

Calling all Hook Developers

Are you a developer interested in writing hooks? Would you like to share what you've built or ask questions?

Please check the other blogs, the source & README's and ... just a little more patience as we're on track for a Q1 public testnet release :)


Discussions: here »

Next Steps

We are drawing close to public testnet. Some edge and corner cases still need to be debugged with emitted transactions, and the slot component of the Hooks API (which allows hooks to request and read all sorts of different data from the ledger) needs to be finished. We are well on track for a Q1 testnet release 🎉 😎

Photo by Paweł Czerwiński on Unsplash

Top comments (0)