Limiting Peers with DynamicSupervisor Options

Written by Pete Corey on Jun 18, 2018.

Last month I posted an article about using Elixir’s DynamicSupervisor behavior to recursively connect our Elixir-based node to peers throughout Bitcoin’s peer-to-peer network.

The last part of that article talked about how we could limit the exponential growth of our set of connected peers by setting a hard cap on the number of processes supervised by our dynamic Node.Supervisor process.

We went through the rigmarole of building this child process cap ourselves, but it was pointed out to me that we could have used DynamicSupervisor’s built in :max_children option to accomplish the same thing!

Our Hand-Rolled Solution

When we implemented our own restriction on the number of peers we allow our node to connect to, we did it within the BitcoinNetwork.connect_to_node/2 function:


def connect_to_node(ip, port) do
  if count_peers() < Application.get_env(:bitcoin_network, :max_peers) do
    DynamicSupervisor.start_child(BitcoinNetwork.Node.Supervisor, %{
      id: BitcoinNetwork.Node,
      start: {BitcoinNetwork.Node, :start_link, [{ip, port}]},
      restart: :transient
    })
  else
    {:error, :max_peers}
  end
end

The count_peers/0 helper function simply calls out to DynamicSupervisor.count_children/1 to count the number of processes being supervised by our dynamic Node.Supervisor:


BitcoinNetwork.Node.Supervisor
|> DynamicSupervisor.count_children()
|> Map.get(:active)

If the number of active peers is less than our specified number of :max_peers, we allow the connection. Otherwise, we return an :error tuple.

Elixir’s Solution

If we read through the DynamicSupervisor documentation, we’ll find that we can pass a :max_children option to DynamicSupervisor.start_link/2. Digging through Elixir’s source, we can see that, when present, the :max_children option does literally exactly what we did in our hand-rolled solution:


if dynamic < max_children do
  handle_start_child(child, %{state | dynamic: dynamic + 1})
else
  {:reply, {:error, :max_children}, state}
end

If dynamic, the number of processes currently being supervised by the supervisor, is less than the specified max_children, add the child. Otherwise, return an :error tuple.

Refactoring

Refactoring our original solution to make use of the :max_children option largely consists of removing our original solution. We’ll start by gutting the guard in our BitcoinNetwork.connect_to_node/2 function:


def connect_to_node(ip, port) do
  DynamicSupervisor.start_child(BitcoinNetwork.Node.Supervisor, %{
    id: BitcoinNetwork.Node,
    start: {BitcoinNetwork.Node, :start_link, [{ip, port}]},
    restart: :transient
  })
end

This means we can also remove our count_peers/0 helper function.

Now we simply need to add the :max_children option to our dynamic supervisor when it starts up:


{:ok, pid} =
  Supervisor.start_link(
    [
      {DynamicSupervisor,
        name: BitcoinNetwork.Node.Supervisor,
        strategy: :one_for_one,
        max_children: Application.get_env(:bitcoin_network, :max_peers)}
    ],
    strategy: :one_for_one
  )

That’s all there is to it!

Our limited set of peers.

Spinning up our Bitcoin node with a low value for :max_peers shows that our Node.Supervisor is honoring our limit.

Final Thoughts

My final thoughts are that I should really spend more time reading through the Elixir and Erlang documentation. There’s quite a few gems hidden in plain sight that would do me quite a bit of good to know about.

I’d also like to thank the Redditor who pointed the :max_children option out to me. Thanks, ParticularHabit!

Generating Test Fixtures with Wireshark

Written by Pete Corey on Jun 11, 2018.

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!

Be Careful Using With in Tests

Written by Pete Corey on Jun 4, 2018.

Last week I struck a chord in the Elixir community when I tweeted about a trap I fell into while writing a seemingly simple test using Elixir’s with special form. Based on the reaction to that tweet, I thought it’d be a good idea to explore where I went wrong and how I could have prevented it.

The Test

The test in question was fairly simple. Let’s imagine it looked something like this:


test "foo equals bar" do
  with {:ok, foo} <- do_foo(),
       {:ok, bar} <- do_bar() do
    assert foo == bar
  end
end

We’re using with to destructure the results of our calls to do_foo/0 and do_bar/0 function calls. Next, we’re asserting that foo should equal bar.

If do_foo/0 or do_bar/0 return anything other than an :ok tuple, we’d expect our pattern match to fail, causing our test to fail. On running our test, we see that it passes. Our do_foo/0 and do_bar/0 functions must be working as expected!

The False Positive

Unfortunately, we’re operating under a faulty assumption. In reality, our do_foo/0 and do_bar/1 functions actually look like this:


def do_foo, do: {:ok, 1}
def do_bar, do: {:error, :asdf}

Our do_bar/0 is returning an :error tuple, not the :ok tuple our test is expecting, but our test is still passing. What’s going on here?

It’s easy to forget (at least for me, apparently) that when a with expression fails a pattern match, it doesn’t throw an error. Instead, it immediately returns the unmatched value. So in our test, our with expression is returning the unmatched {:error, :asdf} tuple without ever executing its do block and skipping our assertion entirely.

Because our assertion is never given a chance to fail, our test passes!

The Fix

The fix for this broken test is simple once we recognize what the problem is. We’re expecting our assignments to throw errors if they fail to match. One surefire way to accomplish that is to use assignments rather than a with expression.


test "foo equals bar" do
  {:ok, foo} = do_foo()
  {:ok, bar} = do_bar()
  assert foo == bar
end

Now, the :error tuple returned by our do_bar/0 function will fail to match with our :ok tuple, and the test will fail. Not only that, but we’ve also managed to simplify our test in the process of fixing it.

Success!

The Better Fix

After posting the above fix in response to my original tweet, Michał Muskała replied with a fantastic tip to improve the error messaging of the failing test.

Michał's pro tip.

Currently, our test failure looks like this:


** (MatchError) no match of right hand side value: {:error, :asdf}
code: {:ok, bar} = do_bar()

If we add assertions to our pattern matching assignments, we set ourselves up to receive better error messages:


test "foo still equals bar" do
  assert {:ok, foo} = do_foo()
  assert {:ok, bar} = do_bar()
  assert foo == bar
end

Now our failing test reads like this:


match (=) failed
code:  assert {:ok, bar} = do_bar()
right: {:error, :asdf}

While we’re still given all of the same information about the failure, it’s presented in a way that’s easier to read and internalize, leading to a quicker understanding of how and why our test is failing.

I’ll be sure to incorporate that tip into my tests from now on. Thanks Michał!