BTC$104,872.40+2.41% XAU$3,318.12+0.42% SPY$5,917.88-0.18% M2INFINITE+∞%
BLOCK 945,231
CLIENT WORK · CASE STUDY No. 04

The bug in the vault door.

A client's Zcash sat on a Ledger Blue with no seed backup, which meant the physical device was the only key in existence. Recovering it required compiling our own firmware for a wallet the manufacturer discontinued, and then discovering an undocumented bug in that manufacturer's cryptographic library that had been quietly breaking Zcash signatures for years.

Every other engagement in this series ended with a transaction. This one had to end with a compiler.

The setup was the worst version of a familiar problem. A Ledger Blue, the big-touchscreen flagship Ledger shipped in 2016 and abandoned years ago, holding roughly 3.31 ZEC. The device powered on. The PIN was known. And there was no recovery sheet anywhere, which changes the character of the entire job: with a seed backup a dead device is an inconvenience, and without one the device is the money. Every action carries the possibility of destroying the asset you are trying to save.

Ledger Live does not even recognize the Blue anymore. It will not list it, will not install to it, will not acknowledge it exists. The Zcash app that would ordinarily sign the transaction refused to install through every path we tried. And when we finally got a Zcash-capable app running and asked it to sign, the device answered with a five-character error and locked itself: 0x6F02. It did that identically, at the same instruction, dozens of times over two days.

The answer turned out to be three separate walls stacked behind each other, and the last one was a genuine defect in Ledger's own firmware.

The playbook · what it took
  1. Establish the device can be reached at all. Speak to it directly at the APDU level from an isolated Linux host, because the vendor's own software holds the USB interface and starves everything else.
  2. Discover the signature would have been worthless anyway. Zcash binds a network-version identifier into every signature. The newest available app carried one from 2020.
  3. Build custom firmware. Find the one SDK version out of twenty-three that matches this device, get a 2016 codebase to compile on a 2021 toolchain, and load an unsigned app the device will warn you about.
  4. Learn the app you patched isn't the app that matters. On this device the coin apps are 1.8KB shims that delegate into a shared library.
  5. Instrument the firmware to interrogate itself, so the device reports which line of its own code is failing rather than a generic error.
  6. Characterize the bug with a probe that survives its own crashes, turning a one-answer-per-hour debugging loop into fourteen answers in a single second.
  7. Patch around it in 22 places, preserving the signed bytes exactly while moving where the function calls begin.
  8. Verify the signature independently before anyone broadcasts anything. This step caught a failure that eleven other checks missed.

Wall one: the signature would have been invalid before it was ever made

Zcash rotates a value called the consensus branch ID at every network upgrade, and that value is baked into the mathematics of every signature. Sign with the wrong one and the network rejects your transaction outright. It is a deliberate anti-replay mechanism, and it means a signing device that stops being updated eventually stops being able to sign at all.

The newest Zcash app Ledger ever published for this device hardcodes the branch ID for Canopy, which activated in November 2020. Zcash has upgraded four times since. The live network was running NU6.3, activated at block 3,428,143.

app's baked-in branch id 0xE9FF75A6 Canopy, Nov 2020 network requires 0x37A5165B NU6.3 "Ironwood" chain height 3,439,886 (activation was 3,428,143)

This reframed everything. We had spent hours trying to make the stock app work, and the stock app was never going to produce a usable signature. Even a flawless signing flow would have yielded a transaction the network throws away. There was no configuration fix and no runtime override. The only path forward was to compile our own.

A hardware wallet that stops receiving updates does not slowly get worse. On some chains it stops working entirely, on a date nobody tells you.

Wall two: the app we patched was not the app that mattered

Getting custom firmware onto a discontinued device is its own exercise. Ledger publishes twenty-three SDK versions for the Blue, and exactly one of them declares the hardware identifier our device reports. Counter-intuitively it is not the newest. A 2016 codebase also does not compile cleanly on a 2021 compiler, which produced its own detour: at one point we satisfied the linker with a flag that placed the program's memory heap at address zero. It compiled perfectly and crashed the instant the app opened.

Then the strange part. We built an instrumented version of the Zcash app designed to report exactly where it was failing, loaded it, and got a binary that was byte-for-byte identical to the uninstrumented one.

COIN=zcash .text = 1,792 bytes COIN=bitcoin .text = 64,000 bytes Ledger's own catalog: Zcash "bytes: 1800"

On this device, the altcoin apps are not applications. They are roughly 1.8KB configuration shims that set a few coin parameters and then hand control to the Bitcoin app, which contains all the actual transaction logic. Instrumenting Zcash changed nothing because the code we wanted to inspect was never in it. The error was coming from Ledger's own signed Bitcoin app.

Which created a trap. Replacing the Bitcoin app means deleting it first, and the device refuses: it is a shared library and other installed apps depend on it. The command that would list those apps so we could remove them does not exist in firmware this old. We were asked to remove a dependency without being permitted to see what depended on it.

The way through was a single build variable that compiles the coin app standalone, carrying its own copy of the transaction logic and depending on nothing. The Bitcoin app never had to be touched.

Wall three: a real bug in the manufacturer's cryptography

With a bootable, instrumented, correctly-versioned app finally running, we could ask the device a question that had no answer before: which instruction is failing?

Zcash builds a signature by feeding a precise sequence of bytes through a hash function called BLAKE2b. We wrote a diagnostic that replays that exact sequence using nothing but zeros and no key material whatsoever, so it could be run safely as many times as needed. It reported a failure at one specific step: a one-byte update issued immediately after the running total reached exactly 256 bytes.

That number is not a coincidence. BLAKE2b processes data in 128-byte blocks, and 256 is precisely two of them.

The obvious next question was whether the bug depended on the size of that small update, because if so the fix was trivial. Answering it normally would have cost one app reinstall per test, and each test answers a single bit. So we built a second probe that catches the crash inside the firmware itself, letting it sweep fourteen combinations without ever halting. One command, fourteen answers.

e0b8000000 -> 02020202 02010102 02020202 0000 total 0 + 1, 36 bytes 00 00 fine total 128 + 1, 36 bytes 01 01 update throws total 256 + everything 02 .. throws even earlier total 255/250/200 + 1 02 .. throws in the setup

Decoded, the pattern is unambiguous and it is not about size at all:

On this firmware, the hash function throws an error whenever you add data to it while the running total is already an exact multiple of 128 bytes.

It is a buffer-handling defect in Ledger's cryptographic layer. The implementation evidently leaves a completed block sitting in its buffer instead of processing it, and the next call trips a bounds check. Any Zcash transaction whose signature happens to cross that boundary at a call seam fails, and the Zcash signing sequence lands on it every single time: 220 bytes of prefix, plus a 36-byte segment, is exactly 256.

The fix, and the failure that almost slipped through

Since the failure depends on where a call begins rather than how much it carries, the workaround is to never begin one on a boundary. We routed every hash update through a small staging buffer that holds one byte back whenever a flush would land on a multiple of 128, then releases it with the next chunk.

The bytes being signed are bit-for-bit identical. Only the seams between function calls move. The resulting signature is exactly the one Zcash expects.

The first version of that patch rerouted sixteen call sites and missed six others. The device produced a signature that passed every structural check we had, correct format, correct destination, correct amount, correct fee, correct public key, and failed independent verification, because a held-back byte had been silently dropped at one of the missed sites.

That is the most important paragraph in this case study. A signature that looks perfect and is quietly wrong is the worst possible outcome, and the only reason it was caught is that we treat independent verification as mandatory rather than as a formality. We recompute the entire 294-byte input from scratch, in separate code, and check the signature mathematically before anyone touches a broadcast button.

independently recomputed sighash, branch-id sweep: NU6.3 0x37A5165B *** VERIFIES *** NU6.1 0x4DEC4DF0 no NU6 0xC8E71055 no NU5 0xC2D6D0B4 no Canopy 0xE9FF75A6 no Sapling 0x76B809BB no ALL CHECKS PASSED

The sweep is the strongest single line in the whole engagement. The signature validates under exactly one network version, and it is the one the live chain requires. Version two of the patch covered all twenty-two sites, and the transaction confirmed in block 3,439,883.

A WORD FROM THE FIRM

Is something stuck on hardware nobody supports anymore?

Most people with a discontinued wallet in a drawer assume the answer is either "it still works" or "it's gone." There is a third state, and it is the common one: the device works perfectly, the keys are intact, and the world moved in a way that makes the coins unreachable through any normal tool. Stale network upgrades, abandoned apps, formats no current software will open. That is a solvable engineering problem rather than a loss, and it does not get easier by waiting, because the tooling that can still speak to old hardware gets thinner every year. If you are holding something behind a device the manufacturer has walked away from, the right time to find out where you stand is while it still powers on. You keep custody the entire time and the first conversation costs nothing.

By the numbers

Combined engagement · all four case studies · 2026-08-05 to 2026-08-07
50.8
hours wall clock
871.4M
total tokens
6,536
transcript entries
606
shell invocations
11
custom firmware builds
9
app loads to device
22
firmware call sites patched
2
diagnostic APDUs designed
4
chains recovered from
21.0
MB of transcript
3
frontier models used
1
firmware bug found

Token breakdown: 840,067,913 cache read · 29,112,429 cache write · 2,223,932 output. Models: Claude Opus 5 (1,427 turns) and Claude Fable 5 (376 turns) drove the work; GPT-5.6 Sol was consulted across three structured rounds and supplied two decisive corrections we had wrong, the NU6.3 branch identifier and the NVRAM sizing that stopped the custom app from booting. Both were confirmed against primary sources before we acted on them. The other three case studies in this series ran inside the same engagement window and share these totals; the Zcash work consumed the large majority of it.

01

Abandoned firmware has an expiry date you are never told.

Zcash rotates a version identifier into every signature. A wallet frozen in 2020 cannot sign in 2026, no matter how healthy the hardware is.

02

Design diagnostics that survive their own failures.

Catching the crash inside the firmware turned a one-answer-per-reinstall loop into fourteen answers in a single command. That one decision saved days.

03

A negative result is a deliverable.

Proving the bug was not size-dependent eliminated an entire class of fix immediately, at the cost of one test.

04

Verify independently, always, especially when it looks right.

Our first signature passed eleven structural checks and was silently wrong. Only a from-scratch recomputation caught it.

05

A second opinion earns its cost on facts, not opinions.

Two corrections from a different model changed the outcome. We confirmed both against primary sources rather than taking them on trust.

06

Never let a build workaround reach runtime unexamined.

A flag that silenced a linker error placed the heap at address zero. It compiled cleanly and crashed instantly.

WORK WITH US

We read the chain, and when we have to, the firmware.

Carroll Park Capital is bitcoin-native by conviction. But clients rarely hold exactly one thing, and a decade of cycles has left serious people with real value stranded behind old devices, old formats and old assumptions. Sometimes recovering it means reading a block explorer. Sometimes it means reading the manufacturer's source code, building a patched application, and proving mathematically that the result is correct before anything is broadcast. Both are the same job done properly. If that is the kind of help you need, talk to us before you improvise on hardware you cannot replace.

Coins, devices, firmware versions, and tools in this study are named as they were used. Client identifiers, full addresses, and transaction linkage are withheld. Previous in the series: Case Study No. 03 · The wallet that predated the chain.