Bitcoin/Ethereum Atomic Swaps In Practice

Matthew Slipper
Kyokan, LLC
Published in
4 min readNov 15, 2018

This post will be the first in a series covering how to build atomic swaps between Bitcoin and Ethereum. Rather than focusing on the theory — since there are already a wealth of resources on that topic — we’ll be getting our hands dirty by writing functional code in Golang to create the swaps.

We’ll start with some background around how atomic swaps are constructed, then move on to the specific implementations for Bitcoin and Ethereum. Finally, we’ll wire them up and perform a swap. Along the way, we’ll discuss the challenges of writing cross-blockchain applications and how to overcome them.

A Primer on HTLCs

One of the most common methods to affect atomic swaps, and the method we’ll use here, uses something called a ‘Hashed Time-Locked Contract’ or HTLC. Any pair of chains that support a common set of hash functions can swap with one another using HTLCs. HTLCs work like this:

  1. Alice generates a sequence of random bytes, called the ‘preimage,’ and hashes them.
  2. Alice broadcasts a transaction on-chain that contains the hash of the preimage. Providing the preimage to this transaction sends funds to Bob.
  3. Bob broadcasts his own transaction with the same hash that sends funds to Alice upon receipt of the preimage.
  4. Alice provides the preimage to Bob’s transaction, thus sending herself funds on Bob’s chain. We call this the ‘redemption’ transaction.
  5. Bob then uses the preimage to redeem Alice’s transaction, thus sending himself funds on Alice’s chain.

Since blockchains are public ledgers, Bob can extract both the hash and the preimage by watching the blockchain for transactions from Alice.

If either party fails redeem the other’s HTLC, funds can be reclaimed after a certain timeout has elapsed.

HTLCs in Bitcoin

Now we’ll outline how to construct an HTLC on Bitcoin. To understand this section, you’ll need some familiarity with how Bitcoin script works — the Bitcoin Wiki entry on the topic is a great resource if you need a refresher.

BIP-199 outlines the Bitcoin script implementation:

OP_IF
[HASHOP] <digest> OP_EQUALVERIFY OP_DUP OP_HASH160 <seller pubkey hash>
OP_ELSE
<num> [TIMEOUTOP] OP_DROP OP_DUP OP_HASH160 <buyer pubkey hash>
OP_ENDIF
OP_EQUALVERIFY
OP_CHECKSIG

[HASHOP] can be one of OP_SHA256 or OP_HASH160. [TIMEOUTOP] can be one of OP_CHECKSEQUENCEVERIFY or OP_CHECKLOCKTIMEVERIFY.

Note: Any raw data added to either the stack or inside a Bitcoin script is implied to be prepended with the appropriate OP_1-OP_75 call, where the number represents the number of bytes to push onto the stack.

Let’s walk through this script as if we’re the Bitcoin script interpreter. Below, see the stack at various points throughout script execution. We’ll use the following scriptSig, which redeems the transaction using the preimage:

OP_TRUE 0xfeedface 0x0363e177...5e09c943

Here, 0xfeedface is our preimage, and 0x0363e177...5e09c943 is our compressed public key. You'll want to use a sequence of random bytes for the preimage in production, however we've used a simple value here for clarity. We'll use following values for the HTLC script:

  • [HASHOP]: OP_SHA256
  • [TIMEOUTOP]: OP_CHECKSEQUENCEVERIFY
  • <digest>: 0x3684d458...14877709
  • <seller pubkey hash>: 0xfa88b8dd...7be47a33
HTLC Redemption Stack

A similar process is followed for timeout transactions, however the only inputs to the scriptSig are OP_TRUE and the buyer's public key.

Let’s implement the above HTLC in Golang. To do so, we’ll use the excellent btcd library from the team at Lightning Labs. First, we'll write a method that generates the HTLC script itself:

func GenHTLCScript(hash [32]byte, sellerPub *btcec.PublicKey, buyerPub *btcec.PublicKey) ([]byte, error) {
instantHash := Hash160(sellerPub.SerializeCompressed())
delayedHash := Hash160(buyerPub.SerializeCompressed())
bldr := txscript.NewScriptBuilder()
bldr.AddOp(txscript.OP_IF)
bldr.AddOp(txscript.OP_SHA256)
bldr.AddData(hash[:])
bldr.AddOp(txscript.OP_EQUALVERIFY)
bldr.AddOp(txscript.OP_DUP)
bldr.AddOp(txscript.OP_HASH160)
bldr.AddData(instantHash)
bldr.AddOp(txscript.OP_ELSE)
bldr.AddOp(txscript.OP_7)
bldr.AddOp(txscript.OP_CHECKSEQUENCEVERIFY)
bldr.AddOp(txscript.OP_DROP)
bldr.AddOp(txscript.OP_DUP)
bldr.AddOp(txscript.OP_HASH160)
bldr.AddData(delayedHash)
bldr.AddOp(txscript.OP_ENDIF)
bldr.AddOp(txscript.OP_EQUALVERIFY)
bldr.AddOp(txscript.OP_CHECKSIG)
return bldr.Script()
}
func Hash160(in []byte) []byte {
sha := sha256.New()
sha.Write(in)
rmd := ripemd160.New()
rmd.Write(sha.Sum(nil))
return rmd.Sum(nil)
}

I’d like to point out a couple of important implementation details in the code above. First, OP_HASH160 performs a SHA256 hash followed by a RIPEMD160 hash, so any data included inside the transaction that will be compared to the result of an OP_HASH160 will need to be hashed using the same method. This is why we use the Hash160 method in the code above.

Next, we compress the public keys before hashing them. This is a convention in Bitcoin scripts, and a protocol requirement for signatures. Public key compression exploits the mathematical properties of elliptic curve public keys to store the same public key in 33 bytes rather than 65. This Stack Overflow post provides a useful primer on the math if you’re interested.

Finally, you’ll see calls to bldr.AddData in the above method. These automatically add the OP_1-OP_75 opcodes based on the data length.

Now we can generate the redemption and timeout scripts. These are much simpler than the HTLC script:

func GenHTLCRedemption(preimage [32]byte) ([]byte, error) {
bldr := txscript.NewScriptBuilder()
bldr.AddData(preimage[:])
bldr.AddOp(txscript.OP_TRUE)
return bldr.Script()
}
func GenHTLCTimeout() ([]byte, error) {
bldr := txscript.NewScriptBuilder()
bldr.AddOp(txscript.OP_FALSE)
return bldr.Script()
}

Note that the public key is not included in either of these methods. The public key is automatically added by the btcd library when the transaction is broadcast. We also include the operations in reverse order here in order to match the wire format of the transaction when it is broadcast on the Bitcoin network.

We’re now ready to broadcast our HTLC. This is a fairly complex process, which we’ll save for a dedicated post since it requires deploying both a simulated Bitcoin node as well as a Bitcoin wallet to secure our private keys. Stay tuned — by the time we’re done, you’ll be able to swap Ethereum for Bitcoin with one CLI command.

Did you enjoy this post? Have questions? Please let us know in the comments below or reach out to Matthew on Twitter via @mslipper.

--

--