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.
- license
- MIT
- branch
- main
- tests
- none found
- source
- 18.5 kB
- commit date
- 2026-10-18
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 source. Not just the
.cppfiles 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
.rsrcverbatim, 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,TimeDateStamp1231629360, which is 2009-01-10 23:16:00 UTC. - No COFF symbol table (
PointerToSymbolTable0), and characteristics0x30f, 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.vcfor Visual C++ 6.0 SP6 beside the MinGWmakefile, and the readme lists both compilers. The binary alone says the MinGW one was used. CheckSum0x634606, and it is valid:pefilerecomputes the same value. Whatever wrote it last computed a real checksum.- Sections
.text5,761,536 bytes,.data67,072,.rdata537,088,.bssnone on disk,.idata20,480,.rsrc53,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.
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.

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, 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.