← Back to all insights

TL;DR. A wallet auditor flagged Nunchuk's desktop wallet as not reproducible, meaning nobody outside the company could prove the download matched the published source, and we confirmed every figure in that report ourselves. Nunchuk then fixed it within two days, and our own rebuild of the new version matches theirs in 1,584 of 1,587 files, with the only differences being 1,302 embedded clock readings rather than any actual code.

The short version

When you download a bitcoin wallet, you download a program somebody else compiled. The source code being on GitHub does not prove the two are related. Somebody could publish clean code and ship you something different, and you would have no way to tell.

The fix for this is called a reproducible build. If you can compile the published source yourself and end up with a file identical to the one on the download page, then the download provably came from that source. If you cannot, you are trusting the company, not the code.

WalletScrutiny is a project that tries exactly this, wallet by wallet, and publishes the result. On 18 August they published a red verdict for Nunchuk Desktop 2.6.4: not reproducible. That was the ninth such verdict on the same wallet, on a thread that had been running since April 2025.

We did three things. We checked whether the report was correct. It was, in every detail. We then found that Nunchuk had quietly fixed the underlying problem on 19 and 20 August, hours after the most recent report. And then we compiled version 2.6.6 ourselves, on our own machine, and compared it to the official download.

3 of 1,587
files differ between our own build and Nunchuk's official download

Of the three, two are the same file included twice. And the differences inside them are not code. They are 1,302 recorded timestamps: the moment each file happened to be copied onto the machine that built it. Our copy and their copy were made 58 minutes apart, and that is the entire disagreement.

What this means for you. Nunchuk Desktop on Linux is not yet perfectly reproducible, but it is very close, and the remaining gap is bookkeeping rather than anything that could hide malicious code. The version before it, 2.6.5, was checked one day earlier and differed in 157 places. That is a real improvement in a very short time.

Three cautions if you use this wallet. Version 2.6.6 is marked as a pre-release and, unlike every version before it, ships without a signature file, so there is currently nothing to check the download against. It is Linux only, so macOS and Windows users get none of this yet. And 2.6.5, the current normal release, is still one of the versions marked not reproducible.

Everything below is the working: the exact commands, the exact numbers, and four small fixes we tested that would close the gap completely. Skip it unless you want to check us.


The technical detail

1. Is the report itself correct?

Before checking Nunchuk we checked the person checking Nunchuk. Every figure in verification bcf39301… was reproduced here from scratch.

Claim in the reportOur result
nunchuk-linux-v2.6.4.zip is 5d7b4c36…ae26matches
That hash matches the release SHA256SUMSmatches
SHA256SUMS.asc is a good signature by 8C8ECD3F…ED2EF525good signature, key is Ta Tat Tai (Nunchuk binary release signing key)
AppImage inside, 181,107,192 bytes, 87ebfd10…b195byte-exact match

The report is accurate. We found one thing it understates. SHA256SUMS.asc is not a detached signature over SHA256SUMS; it is a clearsigned copy carrying the sums inside itself. The command most people would reach for fails:

$ gpg --verify SHA256SUMS.asc SHA256SUMS
gpg: not a detached signature

You have to extract the signed payload and diff it against the plain file. We did, they are identical, so the chain holds. But anyone following their instincts hits an error and may conclude the signature is broken when it is fine. A detached .sig alongside would cost nothing.

2. Has any Nunchuk desktop build ever been reproducible?

Not according to the record. The Nunchuk desktop page lists nine verifications, all by the same verifier, and every one reads Not reproducible: 1.9.47, 1.9.48, 1.9.49, 2.5.0, 2.6.0, 2.6.2, 2.6.3, 2.6.4 and 2.6.5. Not one of them reads Reproducible when tested. A second 1.9.48 artifact is listed with no verification at all, and five older rows sit behind a "Show 5 more" control that we did not expand, so we cannot speak for those.

The written record runs alongside it in nunchuk-desktop issue #86, opened 10 April 2025 by the same tester (xrviv on GitHub, dannybuntu on WalletScrutiny) and now fifteen comments long. The opening report is not even about hashes: the build could not start, because contrib/libnunchuk pointed at a private GitLab repository that outsiders could not clone at all.

By 2.6.2 the reports had become forensic. The rebuild differed from the release in a stable pattern of 85 entries: 6 files different, 3 libraries only in the release, 76 libraries only in the rebuild. The same 85 came back at 2.6.3 and again at 2.6.4. The 19 August comment then asked the question that mattered, after two earlier ones went unanswered:

What changed in the Linux release packaging between v2.6.4 and v2.6.5? Nothing in reproducible-builds/ changed [...] yet the released artifact changed substantially. Something outside that directory produces the release, and whatever it is, it moved between those two tags.

3. What happened on 19 and 20 August

Version 2.6.6 was published at 05:54 UTC on 20 August 2026. Our first command in this check ran at 06:04 UTC. The release was ten minutes old, which is why we ended up documenting a fix in progress rather than a standing failure.

When (UTC)What
18 Aug 08:222.6.4 report, three open questions
19 Aug 10:302.6.5 report, "which workflow actually produces the release ZIP?"
19 Augfour commits reworking reproducible-builds/, and build-linux.yml is deleted
20 Aug 05:09tag 2.6.6, commit 025aba7, message "Reproducibile build"
20 Aug 05:542.6.6 published

Which workflow produced the release ZIP? At tag 2.6.4, neither of the two public ones published anything to a release. build-linux.yml ran on manual trigger only; it did build a ZIP, but it uploaded only the AppImage as a CI artifact, and its ZIP wraps a folder while the released ZIP holds the AppImage at the root. reproducible_linux.yml ran on tags but also uploaded only the AppImage. So no public workflow put nunchuk-linux-v2.6.4.zip on the releases page. Something outside the repository did. We cannot see what, and that is the point: the rebuild was being compared against a file whose recipe was not published.

Can the release publish the reproducible build? As of tag 2.6.6, yes. reproducible_linux.yml is gone. The new build-linux.yml fires on a tag push, builds inside the documented Docker image via build_linux.sh, and ends with gh release create uploading exactly that ZIP. The released file is now the CI file.

What about the compile-time OAuth values? At 2.6.4, CMakeLists.txt compiled OAUTH_CLIENT_ID, OAUTH_CLIENT_SECRET and OAUTH_REDIRECT_URI into the binary using add_definitions, fed from repository secrets. Whether the release builds actually had non-empty values we cannot tell from outside, and that was the tester's point too: if they were set, no outsider could ever match the binary. At 2.6.6 the mechanism is gone. The three definitions are absent from CMakeLists.txt, absent from the workflow, and the strings are absent from the shipped binary, which now links libQt5NetworkAuth.so.5 dynamically instead. The question is moot rather than answered, which is the better outcome.

You can see the change in the shipped files without reading any code:

2.6.4 released2.6.6 released
bundled libraries62136
bin/nunchuk-qt167,157,224 bytes117,127,872 bytes

The official 2.6.6 bundles 77 libraries that 2.6.4 did not, and has dropped all three that only the old official build carried (libcrypto.so.3, libssl.so.3, libpq.so.5). Those three are exactly the ones the tester reported as present only in the release. The official download has moved to the shape a from-source rebuild produces.

4. A detail nobody asked about: hwi

bin/hwi was one of the six files that always differed. It is not compiled from source; it is downloaded during packaging, so we looked at where from.

At tag 2.6.4, package_linux.sh fetched HWI 3.1.0 from giahuy98/HWI, a personal fork, with the official bitcoin-core/HWI line commented out directly above it, and no checksum.

The hwi actually shipped inside the 2.6.4 release hashes to e8669f2f…d05b4. We downloaded HWI 3.2.0-displayaddress from nogibi/HWI, a different fork at a different version, and it matches exactly. So the published wallet shipped a hardware-wallet interface binary that its own public build script would not have produced.

At 2.6.6 the script pins the nogibi release and verifies its SHA-256 before installing it, and that is the file shipped. That specific gap is closed, though the dependency is still a fork rather than upstream.

5. Our own rebuild of 2.6.6

We followed reproducible-builds/README.md exactly: clone, git checkout 2.6.6, submodules, build the Docker image, run the build. The builder image took about 18 minutes, the application build 16 minutes 48 seconds on 12 cores.

Our AppImage came out at 184,678,904 bytes. The official one is 184,678,904 bytes. Same size to the byte, different SHA-256. So we unpacked both SquashFS trees and compared every file.

FileBytes differingOf
bin/nunchuk-qt3,894117,127,872
lib/libcrypto.so233,417,040
lib/libcrypto.so.1.123identical copy of the above
the other 1,584 files0

3,940 bytes out of 184,678,904, or 0.0021 percent. The file lists match exactly: same 1,587 files, same paths, no extras on either side.

libcrypto.so: 23 bytes

Nineteen sit inside the 20-byte GNU build ID, which is a hash over the rest of the file and therefore changes whenever anything else does. The other four are here:

ours:     built on: Thu Aug 20 06:20:27 2026 UTC
official: built on: Thu Aug 20 03:47:21 2026 UTC

OpenSSL stamps its own compile time into the library. Four digits of a clock, and nothing else in three and a half megabytes.

nunchuk-qt: 3,894 bytes

Twenty are the build ID. The other 3,874 are all inside .rodata, and they decode as 64-bit millisecond timestamps. There are 1,300 of them, and every single one is offset from the official build by exactly the same 3,477 seconds.

off=79,719,888   ours=2026-08-20 06:07:52.934   official=2026-08-20 05:09:55.086
off=79,719,954   ours=2026-08-20 06:07:52.934   official=2026-08-20 05:09:55.086
off=79,719,976   ours=2026-08-20 06:07:52.708   official=2026-08-20 05:09:54.970
delta(seconds) -> count: {3477: 1300}

That is Qt's resource compiler. When rcc embeds a QML file or an icon into the binary, it records that file's last-modified time next to it. git clone sets those times to whenever you cloned. So 1,300 assets carry the moment the repository was checked out, and the two builds differ by the 58 minutes between their checkouts.

Not one byte of compiled code differs, and the ELF section layout is identical. The two binaries are the same program disagreeing about what time it was.

6. Four fixes, each tested

We ran each of these inside Nunchuk's own builder image rather than reasoning about them.

1. Set SOURCE_DATE_EPOCH in build_linux.sh. Qt 5.15.2's rcc honours it. Same resource, two different file mtimes:

without SOURCE_DATE_EPOCH:  786b4df4…   2b229e45…   different
with SOURCE_DATE_EPOCH:     69277d4f…   69277d4f…   identical

That removes all 1,300 timestamps. Nunchuk already does this on Windows, where windows.sh reads export SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct). The Linux path never got the line.

2. Set it in Dockerfile.linux too, before OpenSSL is compiled. OpenSSL 1.1.1g reads it directly, in util/mkbuildinf.pl line 15:

my $date = gmtime($ENV{'SOURCE_DATE_EPOCH'} || time()) . " UTC";

That removes the built on: line.

3. Pass two options through to mksquashfs. appimagetool ignores SOURCE_DATE_EPOCH and stamps a fresh build time into the SquashFS regardless, even when the input files have identical mtimes. Two runs three seconds apart:

appimagetool ...                              2bdd2d6f…   f12f50e1…   different
appimagetool --mksquashfs-opt -mkfs-time \
             --mksquashfs-opt 0 \
             --mksquashfs-opt -all-time \
             --mksquashfs-opt 0 ...           c453166f…   c453166f…   identical

4. Pin the AppImage's own mtime before zipping it. The ZIP records it, so without this the archive changes every time even when its contents do not:

7z with a fresh mtime:   3a70b78d…   61b6c72e…   different
7z with a pinned mtime:  3ca298f8…   3ca298f8…   identical

Point 3 matters most for the documentation. README.md currently tells you to diff your ZIP against the official one, and that test cannot pass today no matter what anyone does, because it is measuring the clock rather than the build.

7. What is still not right

Credit

dannybuntu at WalletScrutiny filed patient, precise, reproducible reports for sixteen months, mostly into silence, and the last one moved a wallet company inside two days. Nunchuk, once they engaged, did not paper over it: they removed the workflow that was producing the unverifiable file, made the tag build the release build, took the compile-time secret mechanism out, and put a checksum on the one dependency they were downloading blind. That is the right response and it is rarer than it should be.

The four fixes above are public and reproducible, so anyone who wants them, Nunchuk included, can take them straight from here.


This is an independent check. We are not affiliated with Nunchuk or WalletScrutiny and we do not sell Nunchuk products. Everything above was run on our own machine on 20 August 2026 against the public releases and the public repository, and every hash here can be reproduced with the commands in Nunchuk's own reproducible-builds/README.md. If we got something wrong we would rather hear it than not.