Fuzz Testing Bitcoin: How Developers Find Bugs Without Knowing Exactly Where to Look
A software feature can work as expected, pass its tests, and still contain a bug no one has encountered.
Developers write tests to check known requirements and verify that software handles specific inputs correctly. But what happens when a program receives an unusual combination of values, malformed data, or an input that exposes an overlooked assumption?
This is where fuzz testing becomes useful. Fuzz testing involves repeatedly feeding generated or modified inputs into software to look for crashes, errors, and other unexpected behaviour. It helps developers investigate cases they may not have thought to test themselves.
To understand how this works in practice, we spoke with Frankline Omondi, a Btrust grantee whose work includes fuzz testing. He described the process as a form of mining for software bugs: continually exploring inputs, observing how the software responds, and investigating failures.
What Is Fuzz Testing?
Conventional tests usually begin with a defined scenario. A developer might write a unit test to check that a function returns the expected result for a particular input, or a functional test to verify that several components work together correctly.
These tests are essential, but their effectiveness depends partly on which cases developers anticipate.
Fuzz testing takes a different approach. A tool called a fuzzer automatically generates or modifies inputs and repeatedly passes them to a particular part of the software.
Some fuzzers generate inputs from scratch. Others start with a collection of sample inputs, known as a corpus, and produce variations by changing values, removing bytes, or combining pieces of existing data.
Coverage-guided fuzzers also track which parts of the code each input reaches. When an input exercises previously unexplored code, the fuzzer can retain it as a starting point for further variations.
Rather than relying entirely on developers to predict useful test cases, this process helps explore combinations they may not have considered. However, fuzzing still depends on developers choosing what to test and defining how failures will be detected.
How Does a Fuzzer Find a Bug?
Developers begin by writing a fuzz target: an entry point that receives generated inputs and uses them to exercise a particular component.
A target might pass data to a parser, process a network message, or perform a sequence of operations on a data structure. The fuzzer then runs that target repeatedly with different inputs.
Coverage information can guide this process towards unexplored code, but reaching more code is not the same as detecting a bug. Developers also need ways to recognise when something has gone wrong.
A crash is one signal. Sanitizers provide others by detecting particular classes of errors while the program runs. For example:
- AddressSanitizer can detect memory errors such as out-of-bounds access and use-after-free.
- UndefinedBehaviorSanitizer can detect certain forms of undefined behaviour in C and C++ programs.
Developers can also include assertions in a fuzz target. These check properties that should always hold. For example, a target might check that serialising and then deserialising a valid object preserves its contents. If an input breaks that property, the test reports a failure even if the program would not otherwise crash.
When the fuzzer detects a failure, it can save the input that triggered it. Developers can then reproduce the behaviour and investigate its cause.
Tools such as libFuzzer, AFL++ and honggfuzz support this kind of automated testing, with different capabilities and configurations.
Why Passing Conventional Tests Is Not Enough
Fuzz testing does not replace unit tests, functional tests, or code review. It complements them.
Consider a developer making a small change to an established feature. The unit tests pass, and functional tests confirm that the feature still behaves as expected. Yet an unusual combination of inputs might expose an unintended interaction that those tests do not cover.
Frankline highlighted this as one reason fuzz testing matters: a change can satisfy its existing tests while introducing a problem that appears only under less obvious conditions.
The distinction is not that conventional tests are manual or run only once. They can be automated and run continuously. The difference is primarily how test cases are selected.
Conventional tests typically check defined scenarios and expected outcomes. Fuzzing generates and explores variations, potentially exercising paths that the existing test suite does not reach.
Neither approach is exhaustive. A fuzzer can also miss problems, particularly if its target does not exercise the relevant behaviour or lacks a check that would detect the error. Running it longer can improve its chances of finding failures, but time alone does not guarantee effective testing.
How Fuzz Testing Is Used in Bitcoin Core
Bitcoin Core has a dedicated fuzz testing framework with targets covering different components of the codebase. One area where fuzz testing is particularly useful is the peer-to-peer (P2P) networking layer.
Bitcoin nodes exchange messages containing information about transactions, blocks, and other network activity. Because this data comes from other participants, nodes must be able to handle malformed, unexpected, or deliberately adversarial inputs.
Frankline highlighted this layer as an important area for testing. Fuzzing message-handling code helps developers investigate how it responds to inputs that may be difficult to anticipate individually.
For example, Bitcoin Core’s process_message fuzz target exercises message-processing logic with generated inputs. Other targets focus on components such as parsing, serialisation, and data structures.
Each target provides a controlled way to examine a particular part of the software. It does not reproduce every condition on the live network, but it can expose failures in the code responsible for handling those conditions.
Fuzz testing can also support the review of proposed changes. Developers can run relevant targets against modified code and investigate whether generated inputs reveal errors missed during review or by other tests.
The aim is to find and address weaknesses before they reach users.
What Happens When a Fuzzer Finds a Problem?
Finding a failure is the beginning of an investigation, not the end.
Developers first reproduce the problem using the saved input. They may then reduce that input to a smaller example that still triggers the failure, making it easier to isolate the underlying cause.
The investigation might reveal a memory error, an incorrect assumption, or a sequence of operations the code handles badly. It may also uncover a problem in the fuzz target itself. A failure needs to be understood before developers can determine its significance or decide on a fix.
Once the cause is clear, developers can make the necessary changes and add regression tests to help prevent the problem from returning.
The triggering input can also be retained in the corpus so future fuzzing runs continue to exercise that behaviour. Where practical, a dedicated regression test makes the expected outcome explicit.
If the finding could have security implications, it should be handled through the project’s security reporting process rather than immediately disclosed in a public issue.
The value of fuzzing therefore extends beyond discovering a failure. It produces evidence developers can use to understand the software, improve it, and test future changes.
Why Continuous Testing Matters for Bitcoin
Bitcoin Core contains many interacting components. A change to one part of the software can affect assumptions elsewhere, including in code that has worked reliably for years.
Testing cannot therefore be treated as a one-off exercise. Passing a test suite or completing a successful fuzzing run does not establish that a component will remain free of problems as the code evolves.
Continuous fuzzing gives developers repeated opportunities to explore behaviour and detect failures introduced by later changes. As Frankline noted, maintaining complex software requires more than a single round of testing.
Projects such as Google's OSS-Fuzz and Brink’s Fuzzor, among others, illustrate how fuzzing can operate at scale, combining automated testing infrastructure, fuzzing engines, and sanitizers to help open-source projects identify bugs and vulnerabilities.
For Bitcoin software, this work is especially relevant to components that process data from untrusted sources. Depending on their nature, failures could affect node availability, reliability, or security.
Fuzz testing helps investigate some of these risks. Its effectiveness depends on the quality of the targets, the checks they contain, the inputs they explore, and the follow-up work developers do when a failure appears.
Finding the Bugs Developers Cannot Predict
No single testing method can guarantee that software is free of bugs. Unit tests, functional tests, code review, and fuzz testing examine it from different angles.
What makes fuzz testing valuable is its ability to challenge assumptions at scale. Developers know how a function is intended to behave, but they cannot always predict every input it might receive or every path it might take. Fuzzers help explore those possibilities and turn unexpected failures into reproducible cases.
As Bitcoin’s software ecosystem evolves, security is becoming increasingly important. Fuzz testing is one part of the ongoing work required to protect the software people rely on.
The goal is not simply to make a program fail. It is to discover how it can fail, understand why, and use that knowledge to make it more resilient.