My in-progress Elixir-based Bitcoin node is woefully lacking on the test front. This is especially problematic considering how finicky the Bitcoin protocol parsing and serialization process can be.

But how can we test this functionality without going through the mind-numbing process of manually constructing each packet under test and asserting that it parses and serializes as expected?

Thankfully, Wireshark’s support of the Bitcoin protocol turns this into a simple task. Let’s dig into how we can use Wireshark to generate binary fixtures for each of our Bitcoin packets under test, and explore how we can test against them using Elixir.

Generating Our Fixtures

Wireshark supports the Bitcoin protocol out of the box. That makes the process of generating test fixtures incredibly simple. To create a binary fixture for a given Bitcoin packet, we just need to follow these three steps:

Step one: Fire up Wireshark, start capturing on your network interface, and set bitcoin as your display filter:

Filtering for bitcoin packets.

Step two: Start bitcoind, and watch the packets roll in:

Bitcoin packets on the wire.

Step three: Notice that Wireshark teases out the Bitcoin-specific portion of every matching TCP packet it receives. Each packet can be exported by right clicking on the “Bitcoin protocol” breakdown, and choosing “Export Packet Bytes.”

High level packet information.

The bytes we’re exporting represent the entire packet, as it comes in over the wire.

Parsing Our Fixtures

Now that we’ve saved a handful of packets we’d like to test against, we can start the process of incorporating them into our test suite.

Let’s assume that we’ve saved all of our exported packets into a test/fixtures folder within our project. Let’s also assume that we want to start by testing our “version” packet (the most interesting packet we’re able to parse, so far).

Let’s make a new VersionTest test module and lay down some boilerplate:


defmodule BitcoinNetwork.Protocol.VersionTest do
  use ExUnit.Case

  alias BitcoinNetwork.Protocol
  alias BitcoinNetwork.Protocol.{Message, Version}
end

Next, we’ll add our test:


test "parses a version payload" do
end

The first thing we’ll need to do is load the data from our exported version packet binary:


assert {:ok, packet} = File.read("test/fixtures/version.bin")

We use Elixir’s File.read/1 to read the contents of our version.bin file, and assert that we’ll receive an :ok tuple containing the binary contents of our file in our new packet assignment.

Next, we’ll parse the binary, just like we do within our Node with a call to Message.parse/1:


assert {:ok, message, <<>>} = Message.parse(packet)

Once again, we assert that we’ll receive an :ok tuple with our resulting message. Because the data we exported from Wireshark relates specifically to our version packet, we expect the list of remaining, unparsed binary data to be empty (<<>>).

Now that we’ve parsed the message, we can compare the resulting Version struct found in message.parsed_payload with a pre-defined, expected version struct and assert that they’re equal:


assert message.parsed_payload == version

But where does version come from? How can we know the contents of our version.bin packet without manually parsing it ourselves, byte by byte?

Interpreting Our Fixtures

Once again, Wireshark comes to the rescue. In addition to letting us export our Bitcoin packets as raw binaries, Wireshark also lets us inspect the parsed contents of each of our Bitcoin packets.

If we go back to our version packet in our Wireshark capture file, we can open up the “Bitcoin protocol” section and see a complete breakdown of not only the high level message metadata, but also the specific information sent along in the version message:

Filtering for bitcoin packets.

We can use this information to construct our pre-defined version struct at the top of our test:


version = %Version{
  version: 70015,
  services: 13,
  timestamp: 1_528_146_756,
  recv_ip: <<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 160, 16, 233, 215>>,
  recv_port: 18333,
  recv_services: 9,
  from_ip: <<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0>>,
  from_port: 0,
  from_services: 13,
  nonce: 15_116_783_876_185_394_608,
  user_agent: "/Satoshi:0.14.2/",
  start_height: 1_322_730
}

And with that, we have a solid test of our version parsing functionality.

Testing Serialization

We can test the serialization of our version packet much like we tested the parsing functionality.

Let’s start off by adding a new test to our VersionTest module:


test "serializes a version struct" do
end

Once again, we’ll start off by using File.read/1 to load our binary fixture, and using Message.parse/1 to parse the resulting binary:


assert {:ok, packet} = File.read("test/fixtures/version.bin")
assert {:ok, message, <<>>} = Message.parse(packet)

Rather than comparing the message.parsed_payload to some pre-defined Version struct, we’ll instead serialize it with a call to Protocol.serialize/1 and compare the newly serialized version against the message’s payload binary:


assert Protocol.serialize(message.parsed_payload) == message.payload

And that’s it!

If our version serialization code is working correctly, it should return a binary identical to the version portion of the packet exported from Wireshark.

Final Thoughts

I’d like to give a huge shout out to Lucid Simple’s article on “Binary Fixtures with Wireshark”. It was a huge inspiration for me and a very well written article. I highly recommend you check it out if you’d like a more in-depth exploration of using Wireshark-generated binary fixtures.

For what it’s worth, this kind of testing has already resulted in a positive return on investment. Shortly after implementing these tests, I noticed that my version struct was incorrectly serializing messages, resulting in some strange behavior I’d been noticing with my node. Using the tests as a guide, I was able to quickly fix my implementation.

Three cheers for testing!