# Bitcoin 0.1, rebuilt byte for byte: reading a 2009 build machine out of its executable

> Satyajit Ghana — Head of Engineering @ Inkers Technology
> canonical: https://ai.thesatyajit.com/articles/bitcoin-0-1-rebuilt
> date: 2026-10-19
> tags: reverse-engineering, reproducibility, compilers, agentic-coding, security

On 18 October 2026 Tristan (t4t5) posted that he had rebuilt Satoshi Nakamoto's 2009 `bitcoin.exe` "byte for byte", and that "using opus 5.5, i reverse-engineered satoshi's build machine from the binary." The repost that reached me put it as "Debug build with max optimization only on the mining hash." The repository is small: 22 tracked files, about 470 lines of shell, a Dockerfile, a C shim and a text file that lists a toolchain.

This is not a story about money, and I will not treat it as one. It is a reproducible-builds story. A released executable is a function of its source plus everything else on the machine that built it. Bitcoin 0.1 shipped with its source but without the "everything else". The repository claims to have recovered it, and the proof offered is a hash.

<RepoCard repo="t4t5/bitcoin-0.1-rebuilt" />

I read the repository at commit `764d3ed` (a single squashed commit, 18 October 2026). I did not run its build. The build runs a 2006 Windows `gcc.exe` under Wine, and executing Windows binaries was outside what I allowed myself here. What I did instead was check the inputs: the archive, the original executable's structure, and the specific bytes the repository says pin each part of the toolchain.

## What a hash match has to explain

A SHA-256 match is all or nothing. One differing byte anywhere in 6,440,960 gives an unrelated hash. So "rebuilt byte for byte" means every one of these was matched, not approximated:

- **The source.** Not just the `.cpp` files but the exact headers of every library that gets inlined: Boost, wxWidgets, Berkeley DB, OpenSSL.
- **The compiler and its flags, per file.** Optimisation level changes instruction selection, register allocation and inlining. Debug flags change which assert strings exist.
- **The assembler and linker.** Two binutils versions given the same object code can still emit different padding, different import thunk layouts and different section flags.
- **The static libraries.** wxWidgets and Berkeley DB were compiled on Satoshi's machine and linked in. Their code had to be rebuilt with the same toolchain they were built with then, which turns out not to be the one bitcoin itself was built with.
- **The link order.** Objects and archives are laid out in the order the linker sees them. Every later address depends on that.
- **Resources.** Icons, bitmaps and an XML manifest are copied into `.rsrc` verbatim, line endings included.
- **The clock.** A PE file records when it was made, and so do some compiled strings.

The Windows Portable Executable format makes the last point worse than ELF does. The header has a 4-byte `TimeDateStamp`. `windres` stamps every resource directory. The optional header has a `CheckSum` over the whole file, so any other difference moves it too. Microsoft's linker also leaves a "Rich header" in the DOS stub that lists the compiler builds that produced each object, which is a fingerprint of the build machine in its own right. As we will see, this file has none.

## What I measured on the original

The repository vendors the release archives under `original/archives/`. I did not use those copies. I downloaded `bitcoin-0.1.0.tgz` and `bitcoin-0.1.0.rar` fresh from the Satoshi Nakamoto Institute's CDN, the archive the repository names as its source, and hashed them (measured):

| file | MD5 | SHA-256 |
|---|---|---|
| `bitcoin-0.1.0.tgz` | `dca1095f053a0c2dc90b19c92bd1ec00` | `ce9da465…3753d756` |
| `bitcoin-0.1.0.rar` | `91e2dfa2af043eabbb38964cbf368500` | `8b17eb9a…47555b56` |
| `bitcoin/bitcoin.exe` (in the `.tgz`) | | `fbcac071d92e26d82ec917214e334bd43850c0691f113bab1d4741c9bdd30d2d` |

The MD5 and SHA-1 values match both the ones printed on the Institute's code page and the ones in the repository's `original/MANIFEST`. The repository's vendored copies are byte-identical to the Institute's. The executable's hash is the one `build.sh` checks against, and it is 6,440,960 bytes long (measured). I did not extract the `.rar`, which the manifest traces to a copy Hal Finney kept, because I had no RAR tool in the sandbox. So I have checked that archive's hash, but not compared the executable inside it.

I then parsed the executable with `pefile` 2024.8.26 in a Python 3.12 venv. I read it only and never ran it. Measured:

- Machine `0x14c` (i386), 6 sections, `TimeDateStamp` 1231629360, which is 2009-01-10 23:16:00 UTC.
- No COFF symbol table (`PointerToSymbolTable` 0), and characteristics `0x30f`, which include "debug stripped" and "local symbols stripped".
- Linker version field 2.56. Microsoft's linker writes its own version here (Visual C++ 6 writes 6.0). As far as I know, 2.56 is a fixed value GNU ld wrote at the time, not the binutils version (reasoned).
- **No Rich header.** Microsoft's linkers have left one for decades; GNU ld does not. The source tree ships a `makefile.vc` for Visual C++ 6.0 SP6 beside the MinGW `makefile`, and the readme lists both compilers. The binary alone says the MinGW one was used.
- `CheckSum` `0x634606`, and it is valid: `pefile` recomputes the same value. Whatever wrote it last computed a real checksum.
- Sections `.text` 5,761,536 bytes, `.data` 67,072, `.rdata` 537,088, `.bss` none on disk, `.idata` 20,480, `.rsrc` 53,760. With the 1,024 bytes of headers these sum exactly to the file size.

The map below lays those sections out and marks the bytes that matter for the rebuild. Click a mark for its offset and contents. The **clock** filter shows the time-dependent bytes and adds them up.

<PeMap />

## The famous detail is in the makefile

The repost and the replies latch onto "a debug build, but turned on max optimization for one file: the hashing used for mining." That is true, but no reverse engineering was needed to learn it. It is printed in Satoshi's `src/makefile` (measured, from the archive):

```makefile
ifneq "$(BUILD)" "debug"
ifneq "$(BUILD)" "release"
BUILD=debug
endif
endif
...
CFLAGS=-mthreads -O0 -w -Wno-invalid-offsetof -Wformat $(DEBUGFLAGS) $(WXDEFS) $(INCLUDEPATHS)
...
obj/sha.o: sha.cpp		    sha.h
	g++ -c $(CFLAGS) -O3 -o $@ $<
```

Every object is `-O0 -g`. `sha.o` gets `-O3` appended after `CFLAGS`, and for gcc the last `-O` wins. The default build is debug. `sha.cpp` is the SHA-256 that `BlockSHA256` in `main.cpp` calls while mining, so the one file in the mining loop is the one that is optimised. The file's header says it was extracted from Crypto++ 5.5.2 (measured). That is ordinary engineering, not an archaeological find.

What the binary did have to settle is narrower: that Satoshi ran the makefile's **default** debug build rather than `BUILD=release`, and that the result was stripped afterwards. The makefile never strips, and the original has no symbol table. That second fact matters more than it looks, because `strip` rewrites the PE header's `TimeDateStamp`. The link time is gone; the header now records when `strip` ran.

## Reading the machine out of the bytes

Here is the repository's account of Satoshi's machine, with what I could check against the packages myself.

**Compiler: MinGW gcc 3.4.5.** The original's `.rdata` holds two strings that come from libgcc, not from bitcoin: `-LIBGCCW32-EH-3-SJLJ-GTHR-MINGW32` at `0x5e5b68` and `../../gcc-3.4.5/gcc/config/i386/w32-shared-ptr.c` at `0x5e5bbc`. I downloaded `gcc-core-3.4.5-20060117-3.tar.gz` from SourceForge, checked that its SHA-256 matches the repository's manifest (`be73c942…96f631263e`), and found both strings in its `lib/gcc/mingw32/3.4.5/libgcc.a` (measured). The readme in the archive names "MinGW GCC (v3.4.5)" too.

**Runtime: mingw-runtime 3.13.** The archive ships `mingwm10.dll` beside the executable. I hashed it and the copy in `mingw-runtime-3.13.tar.gz`. Both are `1badf3972d9a861a69fafb2c5e3282aecc30007b9d51c199806e542b32d7708f` (measured).

**OpenSSL import library.** Bitcoin links `libeay32.dll` dynamically, so the rebuild needs an import library, not OpenSSL's code. The repository generates a `.def` file from the shipped DLL's export table. I wrote my own 6-line `pefile` script to do the same thing. Its output is identical to the committed `deps/libeay32.def` apart from two comment lines: 2,705 exports, with names and ordinals in the original order (measured).

**Shell: cmd, not MSYS.** Debug builds keep assert strings with the header path. The original contains `/boost/boost/array.hpp` and `/wxWidgets/include/wx/string.h` verbatim (measured, at `0x590cb0` and `0x593118`). Those are the drive-less paths in the makefile's `-I` flags. MSYS would have rewritten them to `C:/msys/...` before gcc saw them. So the makefile ran from a plain `cmd` window. To reproduce that, the build script symlinks `/boost`, `/wxWidgets`, `/DB` and `/OpenSSL` inside the container. Otherwise Wine resolves the drive-less paths as Unix paths, where Windows would have used the current drive.

**Berkeley DB 4.7.25** is in plain text: `Berkeley DB 4.7.25: (May 15, 2008)` (measured). The repository also says Satoshi ran its configure on Windows XP. The tell is a `%lld` check that XP's `msvcrt` fails, which shrinks one structure to 0x14 bytes, and `build-db.sh` patches configure to fail the same way under Wine. I did not verify the structure size (reported).

**wxWidgets 2.8.9, from the installer.** The `RT_MANIFEST` resource is 658 bytes in 22 lines, and every line ends in CRLF (measured). The repository says only the `wxMSW-2.8.9-Setup.exe` installer ships `wx.manifest` with CRLF. The `.tar.gz` and `.zip` use LF, which would be 22 bytes short and shift everything after it (reported). This is the "line endings of a wxWidgets manifest file" in the README.

**Two binutils.** This is the subtle one. The repository says wxWidgets' and Berkeley DB's code has an older `test reg,reg` encoding and 16-byte object padding that only binutils 2.17.50 or older produces. Bitcoin's own objects are padded to 4. Import thunks are packed 8 bytes apart, and `.idata` and `.rsrc` carry 4-byte alignment flags, which only 2.18.50-20080109 produces. So Satoshi upgraded binutils between building the libraries and building bitcoin, and `toolchain.txt` does the same with its `upgrade=` line. I measured the shapes: a run of 499 consecutive `jmp [addr]` thunks at stride 8 starting at `0x445000`, and `.idata`/`.rsrc` flags `0xC0300040` against `.text`'s `0x60500060`. I did not build either binutils to confirm which one makes which shape (reported).

**Boost 1.34.1, headers only.** The repository says later versions add `addressof` wrappers or reorder the `FOREACH` helpers (reported). Debug builds keep every template instantiation as its own function, so header versions show up as code.

<Figure src="https://media.thesatyajit.com/ai/articles/bitcoin-0-1-rebuilt/fig1.jpg?v=b46f6539af7bc9b0" alt="Dark-mode GitHub README headed 'Bitcoin 0.1.0, rebuilt byte for byte', showing the target hash fbcac071… for bitcoin.exe (6,440,960 bytes) and three bullets: it proves the binary matches the source, Bitcoin 0.1 was never meant to be reproducible, and the exe is a record of Satoshi's machine." caption="The top of the repository's README as attached to the announcement: the target hash and why it matters to the author (t4t5/bitcoin-0.1-rebuilt README, as posted on X)." />

## Three clocks

The last piece is time. The repository sets three wall-clock values, all read back out of the original (measured from the file, matching `toolchain.txt`):

| step | embeds | value |
|---|---|---|
| compile wxWidgets' `wincmn.cpp` | `__DATE__` `__TIME__`, "compiled at" | `Nov 28 2008` `08:05:59` (local time) |
| `windres ui.rc` | every resource directory's `TimeDateStamp` | 1231628882 = 2009-01-10 23:08:02 UTC |
| `strip bitcoin.exe` | PE header `TimeDateStamp` | 1231629360 = 2009-01-10 23:16:00 UTC |

The mechanism is `docker/fixedtime.c`, an 88-line `LD_PRELOAD` shim. It answers `clock_gettime(CLOCK_REALTIME)`, `CLOCK_REALTIME_COARSE`, `gettimeofday` and `time` with a fixed second, and only for the Wine process it is loaded into. Monotonic clocks stay real so Wine's timeouts still work. The comments explain why `libfaketime` did not do: Wine 8 reads the coarse realtime clock, and faking the clock in `wineserver` hangs it. So the server is started first, without the shim.

I can account for the README's statement that a build without those times "differs in exactly those 335 bytes". The bytes that carry a clock are 4 (header stamp), 316 (79 resource directories at 4 bytes each, measured), 8 (`08:05:59`) and 11 (`Nov 28 2008`). That is 339, plus the 4-byte checksum that moves with them: 343 (reasoned). Any modern date in that format shares 4 characters with `Nov 28 2008` (two spaces and the "20" of the year), and any time shares the two colons. So at most 337 can differ, and 335 means two more characters happened to coincide in their test (reasoned). The numbers fit. I have not seen their diff.

## What the agent did, and what I cannot check

The post says Opus 5.5 did the reverse engineering. The repository does not show that work. It is one squashed commit, "Remove license note from README", with no history, no notes and no session logs. Nothing in it says which of the conclusions above came from the model and which from the human. I take the post's sentence as the author's account and nothing more.

The shape of the work does suit an agent (reasoned). Each fact in the table above is a hypothesis with a cheap test: pick a package, rebuild, diff the bytes, read what moved. The `.text` section is 5.7 MB of mostly wxWidgets code, and a wrong binutils or Boost version shows up as thousands of scattered differences. Turning those into "the padding is 16, not 4" means reading disassembly, which is tedious for a person and routine for a model with a disassembler. It is the same loop as the agent-driven ports I have covered: [AnyPS5's relinker](/articles/anyps5-static-relinker), [GTA V recompiled for the browser](/articles/gta5-in-the-browser) and [NullMoth's NVIDIA driver for macOS](/articles/nvidia-macos-driver). There, too, the agent's leverage is in iterating against a binary oracle. The tools side is in [REA, Reverse Engineer Anything](/articles/rea-reverse-engineer-anything). For the measure-then-explain habit applied to performance rather than provenance, see [CPU Performance Engineering](/articles/cpu-performance-engineering).

The headline result itself I have to label **reported**. The repository's GitHub Actions workflow rebuilds from scratch on `ubuntu-24.04` on every push and every Monday, and exits non-zero unless the hash matches. GitHub's API was not reachable from my sandbox, so I could not read whether those runs pass. The build needs Docker, 32-bit Wine 8.0 and about 2 GB of disk. The README quotes about 6 minutes on a 12-core machine (reported). Anyone with that setup can settle it with `./build.sh`, which ends in `MATCH` or `MISMATCH`.

## Which binary this is

One correction comes with the rebuild. The archive called `bitcoin-0.1.0` has `static const int VERSION = 101;` in `serialize.h`, which is 0.1.1 by Satoshi's numbering, and its `bitcoin.exe` carries a 10 January 2009 mtime in the tarball (both measured). Satoshi announced v0.1 on 8 January. The repository calls this the 0.1.1-era binary and says no verified copy of the launch-day package is known to survive (reported). A reply to the announcement says this was pointed out in May 2025. The hash match proves something about this executable, not about the very first one.

## Why a byte-exact rebuild matters

Reproducible builds are the practice of making a build a pure function: same source, same declared toolchain, same bytes out. Today it is done by design. Compilers honour `SOURCE_DATE_EPOCH` instead of the wall clock, linkers have flags to zero timestamps, archives are written in sorted order, and distributions rebuild packages independently and compare hashes. Bitcoin Core's own releases have been built deterministically for years. This repository does the harder, backwards version. Nobody declared a toolchain in 2009, so it had to be inferred from the output and then pinned: every package in `downloads/MANIFEST` has a SHA-256, the Debian snapshot behind the Docker image is fixed, and the base image is pinned by digest.

What a match proves is specific. The 2009 executable contains nothing that is not explained by the published source plus a list of public 2006 to 2008 packages. No hidden patch, no extra object, no different `sha.cpp`. What it does not prove is that those packages are themselves clean. That is Ken Thompson's "trusting trust" problem, and a hash match moves the trust onto the toolchain archives rather than removing it. Nor does it say anything about the launch-day binary.

The cost is also visible. Three of the inputs are times copied out of the very file being matched, and that is unavoidable: no source records when it was compiled. Two DLLs are not rebuilt at all. One is an off-the-shelf runtime I matched by hash; the other is only used through its export table. And the build depends on Wine behaving like Windows XP in exactly the places that touch the output. The scripts carry a few small workarounds for that, each commented.

For anyone shipping binaries today, the lesson is the one the repository makes by contrast. Satoshi's build leaked three timestamps, two toolchains and a shell into the file, and it took an agent and a determined person seventeen years later to account for them. A build that records its toolchain and fixes its clock costs a line in a CI file.
