Make Your Own Bitcoin Exchange in Haskell
A stock exchange is a complex beast, but much of it can be reduced to a single data structure: the Order Book. A Bitcoin exchange uses the same data structure when trading currency pairs of USD, BTC, or ETH. This article will show you how to:
- Design an order book that can handle limit orders and market orders
- Install automated sanity checks that run on every write to the order book, preventing hacks and implementation bugs
- Build an HTTP API that people can use to interact with the order book
We won’t work with actual bitcoins or wallets, since they add a lot of complexity and risk without making the article any more interesting. Instead, we’ll assume the “billing code” has already been written and focus only on the order book portion of the exchange.
Types
So what is an order book, really?
First we’ll define our orders:
A couple points to note:
- We use
Seq
rather thanList
orVector
because we need relatively efficient insert and search. - The higher order
OrderBookF
lets us getTraversable
,Functor
, andFoldable
instances to manipulate the order book without writing any code. - Both
Bid
andAsk
s areLimitOrder
s, but I want to track them separately in the type system. So I useTagged
to attach aTBid
orTAsk
tag.
The
OrderBook
by itself isn’t enough, because we want to track the final amounts actually transferred between buyers and sellers. Lets add a few types for that:
To “execute” an order causes it to turn into at least one
Trade
. There are 4 changes to users’ balances whenever a Trade
occurs for the USD/BTC pair:- The buyer loses his USD
- The seller gains USD
- The buyer gains BTC
- The seller loses BTC
It’s important to use “Double entry accounting” when handling money of any sort. By adding an entry to both sides whenever money changes hands, it’s much more resistant to bookkeeping errors. The total amount of USD and BTC should be zero-sum when transferred internally.
Some accounts are external to our exchange, such as USD held in bank accounts, or BTC held in wallets. In this example, I’ve chosen to eliminate the external account from the exchange’s view, giving us only single-entry bookkeeping for transfers outside the exchange. For accounting purposes, there should be a separate database where full double-entry financial data is held, and it should be routinely reconciled with the exchange, any bank accounts, and any wallets.
Administrators or billing code can create
SingleEntry
records when creating or destroying money supply in the exchange, while all users will create DoubleEntry
records amongst each other.
Finally, here’s the global state that the exchange is going to keep:
In words: An
Exchange
is a collection of external transfers that move money in and out of the system, an order book for a single currency pair full of orders waiting to be filled, and the history of previous trades.
This exchange has no trading fees. Those would go on either the
Trade
or External
types, depending on whether we charge people for internal or external transfers.Order book
What are the fundamental operations we need to use our order book? We want to:
- Add an order to the book, automatically executing trades if able.
- Cancel an order from the book
- List all outstanding orders on the order book
Once we have these, we’ll wrap it in an HTTP API that can be used by scripts or a website to interact with the order book.
I’ll only look at the
Bid
limit and market orders for now - the Ask
versions are very similar. Here are the types for the functions we need:
Since the generated
Foldable
instance does listOrders
for us, I went ahead and implemented it. I’ll leave the implementation of cancelBid
on Github.
For
fillBid
and tryFillMBid
, we’ll write one suitably-generic function for limit orders, and say that a MarketOrder
is a LimitOrder
that can’t be persisted in the order book, and where the user bids his entire account balance:
Our generic
matchBid
function attempts to fill a limit order on the OrderBook
and returns a new Bid
for whatever couldn’t be matched, any Trade
s that were executed, and the new OrderBook
:
Here, we repeatedly find the best price
Ask
on the order book, use it to fill our Bid
, and stop when we run out of qualifying Ask
s.lowestAsk
is pretty easy since our Map
is sorted by price
. I’ll define it and unsafe_addAsk
in the Github repository.mergeBid
is probably the most complex, since it handles 3 distinct things:- It generates new orders if either the bid or ask were partially filled
- It generates a trade if the bid crosses the ask
- It handles the calculations determining these
mergeBid
is subtle enough that you probably don’t feel comfortable trusting its implementation based on visual inspection alone. In the next section, we’ll install automated sanity checks on every write so that any implementation bugs will be blocked.Security
How do we stop our exchange from losing money when we inevitably get hacked?
The most important things to secure are the actual Bitcoin or Ethereum wallets our exchange has. Luckily, we avoided that issue by not having any wallets.
The second most important thing is to have append-only backups that we can roll back to if we detect a hack. That’s not ideal, because we still have to tell all of our customers we got hacked.
The third most important thing is to avoid losing the money in the first place.
In my last article, I showed how you can use theorem provers to formally prove that certain properties hold regarding your data structures. We can’t quite do that in Haskell, but let me define a few invariants for you anyways and I’ll show you a trick.
Invariants
Here are our invariants in words:
- Users can’t trade with themselves
- Users can’t have negative balances
- Users can’t have more money in pending trades than they have in their account.
- Users and OrderBooks can’t have currencies that don’t exist
And here they are in code:
User
s can’t trade with themselves
- Users can’t have negative balances
- Users can’t have more money in pending trades than they have in their account.
User
s andOrderBook
s can’t have nonexistent currencies
As long as those functions always return true, we can have some confidence in the rest of our code.
userBalances
, bookBalances
, and userBookBalances
roll up the trades and external transfers to get a final balance. I’ll leave their implementation in the Github repository.The Trick
People often use triggers or constraints in relational databases to automatically enforce invariants. Using Haskell’s Software Transactional Memory library, we can do something similar:
atomically
enters a “transaction” in the STM
sense. It’s designed as an efficient way to allow multiple threads to concurrently update a shared data structure. Transactions can abort and retry if there are concurrent updates to the same variable. We can also abort if one of our sanity checks fails.
The
alwaysSucceed
function will run the sanity check once, and if it passes run it after every transaction afterwards. It will roll back the transaction with an exception if the sanity check fails with an exception.
We’ll call
installSanityChecks
near the start of our program, after we initialize or load our exchange’s state. Then every write will be automatically sanity-checked and rolled back with an exception. Our HTTP library warp
will catch the exception and abort the request.Networking
We want 5 API endpoints:
- List orders
- Cancel an order
- Add an order
- Create money on the exchange
- List balances
Here are the request types:
And the response types:
Anyone that wants to interact with our exchange will end up creating a
Request
and receive an appropriate Response
. Here is the actual entrypoint to the server:
And here’s how to implement one of our handlers:
I’ve also included JSON serializers and deserializers in the Github repository.
Testing
How do we actually end-to-end test our Bitcoin exchange?
The first step is to print one of your request values to get its corresponding JSON:
The second step is to use a shell script to send that JSON to the server:
At this point, you can use any combination of the 5 endpoints to change or inspect the exchange’s state.
Conclusion
We showed how you can implement a simple order book in Haskell, which can be the basis of a full-blown Bitcoin exchange. Future articles may cover:
- Writing the order book in C for efficiency, and using that in the larger Haskell program.
- Writing a program that watches for actual Bitcoin to be sent, so money can enter our exchange.
- Using unit tests to validate the order book implementation.
- Adding multiple currency pairs to the exchange.
- Adding authentication, so users won’t have unrestricted access to the exchange.
Comments
Post a Comment