~/satyajit

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

mdjsonmcp

2026-10-19 · 14 min · reverse-engineering · reproducibility · compilers · agentic-coding · security

A 1:46 narrated explainer, drawn in code. Every number and picture in it is this article's own; the sources are below.

› transcript

Hi, I'm Keel! In 2009 Satoshi shipped a Windows program with its source. This year someone rebuilt it, byte for byte. One wrong byte gives a different hash. So the compiler, the linkers, the libraries, the shell and even the clock had to be read back out of the old file. First the libraries. wxWidgets and Berkeley DB were built in November, with an older binutils. Then binutils was upgraded. Bitcoin's own files were compiled as a debug build, with full optimisation only on the hashing file. Windres packed the icons and the manifest, and stamped every resource folder with the time it ran. Last, strip removed the symbols and wrote its own time into the header. That file is what shipped. The header says GNU ld, not Microsoft's linker. There is no Rich header at all. In the code, import stubs sit eight bytes apart. The repository ties that to the newer binutils. The strings name gcc three point four point five, and keep include paths that only a plain command window would leave alone. The resources carry seventy nine time stamps, and a manifest with Windows line endings. 335 bytes differ if the three clock readings are left real. The target is the hash of the released file, six point four megabytes. The rebuild prints match only when it is reached. A build that pins its toolchain and fixes its clock is reproducible by design. This one had to be reproduced backwards. Source, tools and clock. Get all three, and the hash follows. Every source is in the full article. I'm Keel. Bye!

Why read this

Notabletop 60%

The 2009 bitcoin.exe taken apart byte range by byte range: which bytes pin the compiler, binutils, shell and clock, checked against the old packages.

  • Original analysis
  • Runs on a laptop CPU
  • A lasting reference

Security & safetyMITPractitioner teardown

How this was scored
Is it new?
1 of 3: An incremental tweak
Can I trust it?
2 of 3: Measures key facts from files, code or configs
Can I run it?
2 of 3: Open code or weights with real limits
Will I understand it?
2 of 3: Mechanism from first principles with figures
Can I act on it?
2 of 3: A concrete recipe, numbers or comparison
Will it last?
2 of 3: A reference for a year or more
Does it affect many?
1 of 3: A specialist community
Only here?
2 of 3: A teardown or measurement few others did

Score 62 of 100, ranked 276 of 577 rated articles. Each question is answered 0–3 by hand, and a 3 is rare. How articles are scored

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.

t4t5/bitcoin-0.1-rebuilt@764d3ed · snapshot 2026-10-19
tracked files
22
license
MIT
branch
main
tests
none found
source
18.5 kB
commit date
2026-10-18
source by language
Shell14.0 kB(8)C2.3 kB(1)Dockerfile1.7 kB(1)Python0.5 kB(1)

by size of tracked source at this commit, file counts in brackets; docs, data and vendored trees excluded

local clone, 2026-10-19 at 764d3ed — branch, commit, commitDate, fileCount, hasTests, languages, license, licenseFile, shallow

shallow clone: counts describe the pinned tree, not the history

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 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):

fileMD5SHA-256
bitcoin-0.1.0.tgzdca1095f053a0c2dc90b19c92bd1ec00ce9da465…3753d756
bitcoin-0.1.0.rar91e2dfa2af043eabbb38964cbf3685008b17eb9a…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:

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.

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):

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.

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.
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):

stepembedsvalue
compile wxWidgets' wincmn.cpp__DATE__ __TIME__, "compiled at"Nov 28 2008 08:05:59 (local time)
windres ui.rcevery resource directory's TimeDateStamp1231628882 = 2009-01-10 23:08:02 UTC
strip bitcoin.exePE header TimeDateStamp1231629360 = 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, GTA V recompiled for the browser and NullMoth's NVIDIA driver for macOS. There, too, the agent's leverage is in iterating against a binary oracle. The tools side is in REA, Reverse Engineer Anything. For the measure-then-explain habit applied to performance rather than provenance, see 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.

Cite this article

For attribution, please use the following reference or BibTeX:

Satyajit Ghana, "Bitcoin 0.1, rebuilt byte for byte: reading a 2009 build machine out of its executable", ai.thesatyajit.com, October 2026.

bibtex
@misc{ghana2026bitcoin01rebuilt,
  author = {Satyajit Ghana},
  title  = {Bitcoin 0.1, rebuilt byte for byte: reading a 2009 build machine out of its executable},
  url    = {https://ai.thesatyajit.com/articles/bitcoin-0-1-rebuilt},
  year   = {2026}
}
share