Working pictures of THORChain. Work in progress. Please provide feedback in the Dev discord server - DevOps channel.

High Level App Overview

High Level App View These are the major components within THORChain. THORChain extends the Cosmos BaseApp and has its own module for specific message handling and processing.

Swap Example

Example BTC to ETH Swap overview - High Level.

High Level App View See larger PDF version here

Some points on the Diagram:

  1. THORChain will see the BTC Tx in the Mempool – but wait times are applied before processing (depending on the size of the swap (inbound liquidity)). This is seen by the blockscanner which can see the mempool of the full BTC node. This Tx is translated to a standard witness transaction and sent to the Observer service. From there they are packaged, signed and sent to THORChain for processing (via the bridge).
  2. Inbound funds are sent to the Asgard Vault.
  3. VM does not apply to THORChain, is a blockchain application or a replicated state machine.
  4. THORChain has two levels of processing – external and internal.
  5. Tendermint broadcasts messages via the Gossip protocol (same as flooding) and gets consensus on the txs, but it is up to THORChain to check everything is good, e.g. Tx Out matches Swap Tx In, BlockHeight is correct and so on.
  6. Nodes agree on the out tx (as per above) but only one node is selected to send the ETH Tx.
  7. Asgard/TSS is not required for outbound Txs, usually sent to a Ygg vault. Asgard will send funds if 1. Outbound is too large for a node to do it or the node failed to do it within a timely fashion (like 2 minutes but I forget the exact time), in which case, 2/3 of TSS Key Gen set would be required to sign the outgoing ETH Tx.
  8. Bifrost can only send messages that it gets from the THORChain bridge, so indirectly when a node send an ETH Tx, it already has 2/3 consensus. That said, as a node has the private key to its own YGG vault, the node does not need consensus to send from it’s own vault (but it will be slashed 1.5x value if it does).
  9. Yep, Tx goes to ETH’s mempool as normal.
  10. Once finds leave THORChain, it creates an OutboundTx which is then sent THORChain. This ensures that what was sent is what was meant to be sent.

Some memo types like swap will create an outbound message, others like add do not.

Swap Example Code Flow

This follows in inbound only. There would be a separate flow for the outbound MsgOutboundTx once the ETH is sent. Swap Code Flow See larger PDF version here

High Level Add Liquidity Example

High Level App View In this example, there is no outbound message.

Add Liquidity Example Code Flow

See larger PDF version here

Write up by HildisvíniÓttar on MsgSend and MsgDeposit

In handler.go there are various ways of receiving incoming. You basically have Bifrost observations that require 2/3 consensus, cli node functions (require auth), and the two generic ones (i.e. supported by Ledger): ~MsgSend and MsgDeposit

// consensus handlers
m[MsgTssPool{}.Type()] = NewTssHandler(mgr)
m[MsgObservedTxIn{}.Type()] = NewObservedTxInHandler(mgr)
m[MsgObservedTxOut{}.Type()] = NewObservedTxOutHandler(mgr)
m[MsgTssKeysignFail{}.Type()] = NewTssKeysignHandler(mgr)
m[MsgErrataTx{}.Type()] = NewErrataTxHandler(mgr)
m[MsgMimir{}.Type()] = NewMimirHandler(mgr)
m[MsgBan{}.Type()] = NewBanHandler(mgr)
m[MsgNetworkFee{}.Type()] = NewNetworkFeeHandler(mgr)

// cli handlers (non-consensus)
m[MsgSetNodeKeys{}.Type()] = NewSetNodeKeysHandler(mgr)
m[MsgSetVersion{}.Type()] = NewVersionHandler(mgr)
m[MsgSetIPAddress{}.Type()] = NewIPAddressHandler(mgr)

// native handlers (non-consensus)
m[MsgSend{}.Type()] = NewSendHandler(mgr)
m[MsgDeposit{}.Type()] = NewDepositHandler(mgr)
m[MsgSolvency{}.Type()] = NewSolvencyHandler(mgr)

External Message Mapping for the THORChain Module. These are received and routed by the ExternalHandler.

Anybody can post a MsgSend or MsgDeposit to the /txs endpoint - see x\thorchain\client\rest. It’s just some JSON. Cosmos does require you have a valid signature for one of the fields, e.g. from_address or signer. This ensures you are authorised to perform the function.

Deposit Msg

Note: Deposit Msg is when Rune gets sent in, e.g. Bond - it does not come via Bifrost.

At its heart, THORChain is a key-value database containing everybody’s balances, plus a bunch of other stuff. It starts at genesis state, and processes every Msg sent to it in order, and mutates the state. Until you get to the current state. Cosmos/Tendermint handles the underlying message stuff. Thornode is the bit that reads these messages and mutates its database to keep track of everything.

In MsgSend handler, it just checks the KV database to ensure you have enough balance for what you asked to send, subtracts fee and sends balance to whomever you specified. Done. At the beginning it stops early if “HaltTHORChain” is enabled. Which is currently set.

In MsgDeposit handler (handler_deposit.go) it’s a little more complex. It deducts the coins you sent in with MsgDeposit from your balance, then reads the MEMO you sent, works out what kind of “internal” message (function) you are trying to perform, and executes one of the internal handlers, such as Bond, Unbond, LEAVE, …..

    m[MsgOutboundTx{}.Type()] = NewOutboundTxHandler(mgr)
    m[MsgYggdrasil{}.Type()] = NewYggdrasilHandler(mgr)
    m[MsgSwap{}.Type()] = NewSwapHandler(mgr)
    m[MsgReserveContributor{}.Type()] = NewReserveContributorHandler(mgr)
    m[MsgBond{}.Type()] = NewBondHandler(mgr)
    m[MsgUnBond{}.Type()] = NewUnBondHandler(mgr)
    m[MsgLeave{}.Type()] = NewLeaveHandler(mgr)
    m[MsgDonate{}.Type()] = NewDonateHandler(mgr)
    m[MsgWithdrawLiquidity{}.Type()] = NewWithdrawLiquidityHandler(mgr)
    m[MsgAddLiquidity{}.Type()] = NewAddLiquidityHandler(mgr)
    m[MsgRefundTx{}.Type()] = NewRefundHandler(mgr)
    m[MsgMigrate{}.Type()] = NewMigrateHandler(mgr)
    m[MsgRagnarok{}.Type()] = NewRagnarokHandler(mgr)
    m[MsgSwitch{}.Type()] = NewSwitchHandler(mgr)
    m[MsgNoOp{}.Type()] = NewNoOpHandler(mgr)
    m[MsgConsolidate{}.Type()] = NewConsolidateHandler(mgr)
    m[MsgManageTHORName{}.Type()] = NewManageTHORNameHandler(mgr)

Internal Message Mapping. This is handled by InternalHandler

Some of these are disabled by MsgDeposit handler, so you only have a finite list of things you can do. These are the things being audited now. So that’s thornode. Basically send it a MsgSend or MsgDeposit and it authorises you via digital signature and reads your memo and tries to perform the function. Plus you can send it Set IP Address, Node Keys and Version via CLI tools (but these require signer to be an active node, etc).


The other side of THORChain is “Bifrost”. This is where a user use an external chain to interact with THORChain

This is a process that reads every block (and sometimes mempool) from all the supported chains: ETH, BNB, BTC, BCH, LTC. Call it a block scanner. It also has the ability to sign transactions out.

For observations, Bifrost will “see” a transaction inbound to one of its monitored addresses. Say you send some BNB.RUNE-B1A to the BNB vault with memo "switch:<my rune address>". Bifrost reads this and goes “Yep that’s legit” and sends a MsgObservedTxIn to Thornode. This gets passed to the observed_txin handler. The first thing it does is “vote” on this transaction being legit. If you are the first bifrost to “see” this, nothing happens - you actually get slashed. Then the next 1-2 seconds as all the other Bifrost also “see” this tx in, and send MsgObservedTxIn to their thornodes, the “vote count” increases, until 2/3 of active nodes have seen this tx, and it’s considered legit. You get your slash removed, and your tx in handler processes the rest of the transaction.

Message Types

func (tx TxType) IsOutbound() bool {
    switch tx {
    case TxOutbound, TxRefund, TxRagnarok:
        return true
        return false

func (tx TxType) IsInternal() bool {
    switch tx {
    case TxYggdrasilFund, TxYggdrasilReturn, TxMigrate, TxConsolidate:
        return true
        return false

// HasOutbound whether the txtype might trigger outbound tx
func (tx TxType) HasOutbound() bool {
    switch tx {
    case TxAdd, TxBond, TxDonate, TxYggdrasilReturn, TxReserve, TxMigrate, TxRagnarok, TxSwitch:
        return false
        return true