Hi Everyone!
In this post I’d like to explain how I approached the Coldcard entropy vulnerability, given that it was the first time I worked with most of the technologies behind this type of research and given that I did not own a physical Coldcard.
before starting, I’d like to make the scope of the research clear.
- Research performed: August 4–6, 2026
- Incident figures last checked: August 24, 2026
This was an independent attempt to reproduce the technical mechanism that had already been publicly disclosed. I did not discover the original vulnerability, I did not recover a live wallet, I did not move any funds and I did not test the model against physical hardware. The final live scan processed 54,680,000 values and produced zero matches.
The purpose of the work was to understand whether I could follow the complete chain, starting from a firmware build mistake and arriving at a Bitcoin address that could be compared with public blockchain data.
The code and numbers in this post describe a research prototype, not a production recovery tool and not proof that a specific Coldcard generated a specific seed. No victim addresses, mnemonics or private keys are published. Do not use this research against funds or devices you do not own or have explicit permission to test.
What happened to Coldcard?
before explaining what I built, it’s useful to clarify the following point, what was the Coldcard entropy vulnerability?
A Coldcard is a hardware signing device for Bitcoin. Its purpose is to create and keep the wallet secret away from an internet-connected computer, then use that secret to authorize transactions. The 12 or 24 words written down during setup are a human-readable backup of the entropy from which the wallet’s keys will be derived.
A hardware wallet needs entropy to create the secret from which the mnemonic and every wallet key will be derived. The important distinction is between a TRNG and a PRNG:
- a TRNG obtains entropy from a physical process inside the hardware
- a PRNG receives an initial state and deterministically expands it into a longer sequence
A PRNG can produce bytes that look completely random, but it cannot create entropy that was not present in its initial state. If the initial state can be enumerated, all future output can also be enumerated.
Coldcard was designed to use its STM32 hardware random number generator for this operation. During a migration to libngu in March 2021, seed generation changed from the previous hardware-specific interface to ngu.random.bytes().
The problem was not that the hardware TRNG broke and the device intentionally selected a weaker fallback at runtime. The hardware implementation was still present. A build and link integration mistake caused the rng_get() symbol used by the seed-generation path to resolve to MicroPython’s software Yasmarang implementation instead.
This distinction is important because reading only the hardware RNG implementation, or confirming that it was present inside the firmware, was not enough. The question that mattered was, which implementation did the wallet-generation call actually reach?
Coinkite describes the same distinction in its technical backgrounder: this was inherited platform behavior selected through the build and link process, not a runtime hardware failure.
The call chain I followed was essentially:
make_new_wallet()
↓
random.bytes(32)
↓
libngu my_random_bytes()
↓
CHIP_TRNG_32()
↓
rng_get()
At the last step, the program reached a deterministic software generator instead of the physical source expected by the code above it.
What motivated me to reproduce it?
When I started the research on August 4, 2026, the public number being repeated was 1,816 BTC taken from more than 5,200 addresses, worth approximately $116 million at that moment.
The first large wave was especially interesting from a technical point of view: 1,082.65 BTC was taken from 1,196 addresses in 41 minutes. This suggested that the expensive part of the work had probably been done in advance and that the on-chain transactions were only the final phase.
These numbers were the motivation. If a mistake at the boundary between three source repositories could reduce the protection of thousands of apparently normal hardware-wallet seeds, I wanted to understand the complete path and not only the vulnerable function.
The incident numbers changed after I had already started. Galaxy’s later high-confidence assessment, published on August 14, was 1,778.84 BTC, or $112.7 million at its reference price, from more than 8,600 addresses. Galaxy had been in direct contact with 190 victims and had identified three named waves plus at least 33 other attacker footprints. If suspected but unconfirmed activity is included, the estimate becomes 2,417.35 BTC, or $153 million. The updated figures are available in Galaxy’s investigation and in this breakdown of the reported waves.
why did the later confirmed BTC number become lower while the address count became higher?
because the first 1,816 BTC figure included activity that was still being treated as a possible fourth wave. Later reporting separated high-confidence attribution from suspected episodes. For this reason, any dollar or victim number in an article about this incident needs a date and a confidence label. BTC is also the better primary measurement because its dollar value changes continuously.
My starting point
This was my first time approaching the complete set of technologies required for research of this type.
The vulnerability touched several layers that are normally discussed separately:
- STM32 firmware and MicroPython
- C preprocessor configuration and linker symbol resolution
- a non-cryptographic PRNG and its internal state
- BIP-39 mnemonic creation
- PBKDF2-HMAC-SHA512
- BIP-32 hierarchical key derivation
- secp256k1 public keys and Bitcoin address formats
- Bitcoin Core’s UTXO set
- CUDA for parallel computation
At the beginning, I thought the main work would be implementing Yasmarang and running a 32-bit loop. In practice, implementing the PRNG step was one of the simplest parts. The difficult part was reproducing the state of the generator at the exact moment the firmware asks for the bytes used by the wallet.
The research was divided into 5 macro phases:
- understand the firmware and linker failure
- reproduce the random-byte generation
- model everything that consumes randomness before seed creation
- build the BIP-39, BIP-32 and UTXO matching pipeline
- validate the implementation and run a partial scan
Phase 1 — Understand the firmware and linker failure
The first step was following the code from wallet creation down to the symbol that was expected to return hardware randomness.
The firmware snapshot used in my repository is commit a85ceb1, built on March 17, 2021 and labelled 4.0.0b4. This is an important limitation: it is close to the introduction of the regression, but it is not a source checkout for every affected production firmware. The first affected public release was 4.0.1.
Inside this snapshot, the new-wallet function does the following:
seed = random.bytes(32)
seed = ngu.hash.sha256s(seed)
The result is 32 bytes of entropy, which BIP-39 encodes as a 24-word mnemonic.
libngu obtains the bytes in C. Its STM32 branch declares an external function named rng_get() and maps CHIP_TRNG_32() to it. It also tries to prevent a build without the hardware RNG using this check:
#ifndef MICROPY_HW_ENABLE_RNG
#error "get a HW TRNG plz"
#endif
The problem is that #ifndef asks whether the macro exists, not whether its value is non-zero. Coldcard defined the macro with value zero. The macro therefore existed, the error did not fire and the build continued.
MicroPython still supplied an implementation of rng_get() when that option was zero, but it was the Yasmarang software implementation. The function signature was correct, so the linker could satisfy the reference without explaining that it had selected the wrong source of randomness.
essentially, the upper part of the code said “give me one value from the chip TRNG,” but the symbol at the end of the chain answered with the output of a deterministic PRNG.
This was the first important lesson of the research: checking that secure code exists is different from checking that the security-sensitive caller can actually reach it.
Phase 2 — Reproduce the random-byte generation
Once the call chain was understood, the second phase was reproducing the bytes returned to the wallet.
Yasmarang has four state variables:
pad — 32 bits
n — 32 bits
d — 32 bits
dat — 8 bits
Each call performs additions, rotations and XOR operations, returns one 32-bit value and mutates the state. Given the same initial state, it will always return the same sequence.
The MicroPython generator initializes its state using values including part of the STM32 unique identifier, the SysTick counter and RTC registers. The main variable I enumerated in the prototype was the combined 32-bit pad value.
This does not mean that I proved the full entropy of every affected Coldcard was exactly 32 bits. It means the scanner covered one 32-bit core state dimension under a specific Mk3-style model. Coinkite’s preliminary estimate is approximately 40 effective bits for Mk2 and Mk3 and approximately 72 bits for Mk4, Mk5 and Q under its assumptions. Later models also include secure-element entropy and were outside the model I actually validated.
The second Yasmarang instance
libngu did not directly return the value obtained from rng_get(). It XORed it with the output of a second Yasmarang instance initialized with public constants:
pad = 0x0a8ce26f
n = 69
d = 233
dat = 0
why doesn’t this solve the problem?
because a deterministic stream initialized from public constants contributes no unknown entropy. Combining one reproducible stream with another reproducible stream still produces a reproducible stream.
The first model I wrote was wrong
My first documentation simplified the process as one low byte from each PRNG call, followed by SHA256d and then either a 12- or 24-word mnemonic.
After reading the actual C implementation, this had to change.
libngu works with one 32-bit value and copies up to four bytes from it into the destination buffer. On the little-endian STM32 target, this means one generator call contributes four bytes in little-endian order. Producing 32 raw bytes therefore takes eight calls, not 32.
The firmware source snapshot I modeled also applies one SHA-256 through sha256s, not SHA256d, and always produces 24 words in the new-wallet path.
The corrected chain for this target became:
8 combined 32-bit outputs
↓
32 raw bytes in little-endian order
↓
single SHA-256
↓
256 bits passed to BIP-39
↓
24-word mnemonic
Hashing is useful for conditioning the output, but it cannot add missing entropy. If two candidates produce two known raw inputs, the corresponding hashes are also known.
Phase 3 — Model what happens before seed creation
At this point I could generate a mnemonic from a candidate state, but this was not enough.
The generator is also used before the user reaches the new-wallet action. Every earlier call advances its state, so an otherwise correct implementation produces the wrong seed if it starts from the right state at the wrong point in time.
The boot path introduced several consumers:
- randomization of non-volatile storage slots
- blank-slot initialization
- rejection and redraw behavior inside random selection
- keypad scan-order shuffling
- user-interface navigation before the new wallet is created
The first large deterministic block was the initialization of three blank storage slots. The model performs a 32-element shuffle and then 3 × 16 calls that each request 256 random bytes. At four bytes per generator call, this accounts for 3,072 generator advances, in addition to the shuffle.
The shuffle itself was more complicated than I expected. A call that asks for a value within a range is not always one PRNG advance. The implementation masks an output and redraws when the result is outside the valid range. This means the exact number of mixer advances can vary with the candidate state.
Keypresses were not the correct unit
My next model treated every keypress as one keypad shuffle.
during testing of the firmware UI code, I found that the useful unit was closer to a keypad scan session. Slow and fast input can result in a different number of scan sessions even when the user enters the same logical keys. The final GPU prototype therefore swept a range of additional session hypotheses instead of relying on one fixed keypress count.
Running the keypad state machine with different virtual press timings produced this output:
$ python3 scan_sim.py
mempad.py: SAMPLE_FREQ=60 NUM_SAMPLES=3 Q_CHECK_RATE=5; idle cutoff in _finish_scan = 250ms
Entering PIN "1234" -- 4 physical presses, realistic 150ms hold:
hold=150ms gap= 50ms -> 1 scan session(s) = 1 shuffle(s) [ok '1234']
hold=150ms gap= 100ms -> 1 scan session(s) = 1 shuffle(s) [ok '1234']
hold=150ms gap= 150ms -> 1 scan session(s) = 1 shuffle(s) [ok '1234']
hold=150ms gap= 200ms -> 2 scan session(s) = 2 shuffle(s) [DROPPED '124']
hold=150ms gap= 225ms -> 4 scan session(s) = 4 shuffle(s) [DROPPED '1']
hold=150ms gap= 250ms -> 4 scan session(s) = 4 shuffle(s) [DROPPED '1']
hold=150ms gap= 275ms -> 4 scan session(s) = 4 shuffle(s) [ok '1234']
hold=150ms gap= 300ms -> 4 scan session(s) = 4 shuffle(s) [ok '1234']
hold=150ms gap= 400ms -> 4 scan session(s) = 4 shuffle(s) [ok '1234']
hold=150ms gap= 800ms -> 4 scan session(s) = 4 shuffle(s) [ok '1234']
Same, but a fast typist (60ms holds):
hold= 60ms gap= 50ms -> 1 scan session(s) = 1 shuffle(s) [DROPPED '12']
hold= 60ms gap= 150ms -> 2 scan session(s) = 2 shuffle(s) [DROPPED '124']
hold= 60ms gap= 250ms -> 4 scan session(s) = 4 shuffle(s) [ok '1234']
hold= 60ms gap= 400ms -> 4 scan session(s) = 4 shuffle(s) [ok '1234']
Story scroll 8/8/8/8/y (5 presses, 150ms hold):
hold=150ms gap= 100ms -> 1 scan session(s) = 1 shuffle(s) [ok '8888y']
hold=150ms gap= 300ms -> 5 scan session(s) = 5 shuffle(s) [ok '8888y']
hold=150ms gap= 600ms -> 5 scan session(s) = 5 shuffle(s) [ok '8888y']
Single isolated press:
hold=150ms gap= 800ms -> 1 scan session(s) = 1 shuffle(s) [ok '1']
The important rows are those marked ok, where the complete logical input reached the firmware queue. The same four-key PIN resulted in either one or four scan sessions depending on timing. Rows marked DROPPED are boundary cases where the next virtual press arrived while the state machine was transitioning, so I did not treat them as valid complete-input cases. These values came from the emulator’s virtual clock and are not timing measurements taken from physical hardware.
For each 32-bit pad value, the final scanner tested the inclusive range from 5 through 30 additional sessions, or 26 variants.
This is an important limitation and also the most useful part of the research process. There was no single magic “boot skip” that I could read from a post and place in the code. The result depended on firmware control flow, storage state, rejection sampling and user interaction.
Emulation without a Coldcard
I did not own a physical Coldcard, so I created a CPython harness around the public firmware modules and replaced the hardware-specific pieces with shims.
The harness allowed me to:
- run firmware UI modules under CPython
- inject scripted keypad input
- count PRNG advances by call site
- preserve MicroPython-specific queue behavior
- compare the Python Yasmarang implementation with a small compiled C known-answer test
To understand the ordering more clearly, I also traced one isolated physical press through the emulated keypad state machine:
$ python3 trace_one.py
t= 0.00 ms _wait_any() <- rearmed; queue now []
t= 10.00 ms --- physical press of "1" ---
t= 10.00 ms _start_scan() -> shuffle -> 3x ngu.random.uniform
t= 60.00 ms _key_event('1') -> queued
t= 160.00 ms --- physical release ---
t= 210.00 ms _key_event('') -> queued
t= 415.00 ms _wait_any() <- rearmed; queue now ['1', '']
last _key_event at t=210.00, _wait_any at t=415.00 -> gap 205.0 ms
_wait_any runs AFTER both the keycode and the release are queued: True
The important detail is the event order, not the absolute millisecond values. _start_scan() performs the scan-order shuffle when the scan session begins; the keypress and release events are queued later, and _wait_any() rearms the matrix only after both. The timestamps are produced by the virtual clock and should not be interpreted as measurements of a real Coldcard.
This was useful for understanding ordering and control flow, but it was not equivalent to a hardware trace. It did not reproduce real interrupt timing, the physical keypad matrix, the exact SysTick value, flash history or the RTC state of a victim device.
For that reason, throughout the post I use “model” and “hypothesis” when describing these values. Without a seed captured from a known affected device, there was no independent end-to-end reference proving that a candidate state generated the same mnemonic as real hardware.
Phase 4 — Build the search pipeline
Once I had a candidate entropy model, I needed to connect it to the Bitcoin side.
The pipeline was divided into these components:
candidate pad and session hypothesis
↓
boot and Yasmarang simulation
↓
SHA-256 and 24-word BIP-39 encoding
↓
PBKDF2-HMAC-SHA512 on the GPU
↓
BIP-32 derivation on the CPU
↓
address material compared with the UTXO table
Why PBKDF2 went to the GPU
BIP-39 converts the mnemonic into a 64-byte seed using PBKDF2-HMAC-SHA512 with 2,048 iterations. Every candidate is independent, which makes this the natural part to parallelize.
I implemented a fused CUDA path that performs the firmware-state simulation, SHA-256, BIP-39 encoding and PBKDF2 for a batch of candidate values. The CPU then performs BIP-32 derivation and UTXO lookups.
The first documentation estimated 200,000 to 400,000 PBKDF2 operations per second and a 3- to 6-hour pass over 2³² candidates on a GTX 1080 Ti. Those were planning estimates for the expensive primitive, not a measured end-to-end result from the final scanner. The finished pipeline tested 26 state variants per pad and also performed multiple key derivations and table lookups, so “PBKDF2 operations per second” and “complete pad values per second” were not the same measurement.
I did not preserve a complete benchmark log, so I cannot honestly present the original 3- to 6-hour estimate as something the final tool achieved.
Building the UTXO table
To check whether a derived address corresponded to an unspent output, I exported the Bitcoin UTXO set to CSV and converted supported address types into a fixed-size memory-mapped hash table.
The artifacts produced were:
| Artifact | Exact value |
|---|---|
| UTXO CSV size | 22,050,760,914 bytes |
| Last numbered CSV row | 164,241,311 |
| Extracted supported keys | 104,378,037 |
| Hash-table slots | 160,581,595 |
| Binary table size | 3,372,213,519 bytes |
| Hash seed | 42 |
The difference between the CSV row count and extracted keys is 59,863,274. This includes entries that the importer did not represent, for example unsupported output types or entries without a supported address representation. The 104,378,037 value is also the number extracted before duplicate insertion is skipped, so it should not be described as 104 million unique wallets.
I also collected a smaller list labelled as the first wave. It contains 504 unique addresses, not all 1,196 addresses later attributed to the first named wave. An attempted download of a larger list failed and the resulting file contains only 404: Not Found.
This is another reason not to describe the local victim list as complete.
Address-path coverage
The intended scan configuration covered the first 20 external addresses for BIP-44, BIP-49 and BIP-84 accounts.
After reviewing the implementation, I found that not all of this coverage should be treated as valid:
- BIP-44 legacy matching is implemented
- BIP-84 native SegWit matching is implemented
- BIP-49 lookup compares the public-key hash against a P2SH entry instead of hashing the wrapped witness redeem script
- BIP-86 is accepted by the command-line parser but falls through to P2WPKH behavior instead of performing Taproot key tweaking and Bech32m address generation
Consequently, I only treat the BIP-44 and BIP-84 portions as meaningful. The prototype also checks only the external chain and a limited address depth. It does not prove coverage of every address a Coldcard wallet could have used.
Phase 5 — Validate and run the scanner
I used synthetic fixtures before attempting a scan against the real UTXO table.
The optimized test dataset contained seven deliberately generated candidate pads. Checking both BIP-44 and BIP-84 produced 14 expected hits over the first 10,000 pads.
This validated that the components agreed with each other:
- candidate generation
- BIP-39 conversion
- PBKDF2
- BIP-32 derivation
- address creation
- UTXO insertion and lookup
but it did not independently validate the firmware model, because the test wallets were generated by the same model later used to recover them. A stronger validation would require a mnemonic and device-state trace captured from a physical Coldcard running a precisely identified affected firmware.
The partial live scan
The preserved checkpoint contains:
current_pad=54680000
candidates_processed=54680000
hits=0
The exact coverage was therefore:
54,680,000 / 4,294,967,296 = 1.273117959%
With 26 session variants for every pad, the nominal number of seed-state variants evaluated by that version of the pipeline was:
54,680,000 × 26 = 1,421,680,000
The live result file is empty. The paper-wallet result file and the enriched-hit file are also empty. No live mnemonic, private key or funded address was recovered.
The result-file creation time and final checkpoint modification time are separated by approximately 6 hours, 51 minutes and 56 seconds. If those timestamps are used as the run boundary, the observed rate was approximately 2,212 complete pad values per second. At that rate, a complete 2³² pad pass with the same 26 variants would project to approximately 22.47 days.
This timing is inferred from filesystem artifacts because the exact command and runtime log were not preserved. It should not be presented as a formal benchmark.
The tests also drifted during development
The model changed quickly, but the original Yasmarang unit tests were not updated with it. A clean build of the current test target fails because the tests still call the removed yasmarang_precompute_const_stream() and yasmarang_generate_entropy() APIs, while the current implementation exposes coldcard_generate_entropy().
The older compiled test artifacts show that previous versions passed their own synthetic cases, but they do not validate the final GPU model.
This is one of the limitations I would address first if I continued the work: create one versioned set of known-answer vectors, generated independently from a real affected device, and require the Python emulator, CPU implementation and CUDA kernel to produce the same result.
The limitations of the research
The main limitations are:
No physical hardware
I did not own a Coldcard and did not obtain a trace from one. Everything was based on public source code, downloaded firmware artifacts and emulation. Real interrupt timing, keypad behavior, flash history, UID values, RTC state and exact boot conditions were not observed.
One historical source snapshot
The source tree used by the emulator is 4.0.0b4. The repository also contains 4.1.3 and fixed 4.2.0 firmware binaries, but the work does not include a demonstrated binary-level reconstruction of their exact control flow. Code behavior changed between releases, including whether one or two SHA-256 operations were used.
A partial entropy model
The scanner enumerated a 32-bit pad and a range of UI-session hypotheses. It did not cover every possible UID, SysTick, RTC and call-history combination implied by the public analyses. It also did not implement the later-model secure-element reseed construction.
No independent seed vector
The synthetic fixtures prove internal consistency, not correspondence with a real affected wallet. Without a physical known-answer vector, a zero-hit result cannot distinguish between “no funded candidate was in this portion of the space” and “the model is still missing an important state transition.”
Partial scan
Only 1.273117959% of the 32-bit pad range was processed. It would be incorrect to call this a complete search or use the zero-hit result to estimate the number of vulnerable wallets.
Incomplete address coverage
Only BIP-44 and BIP-84 matching can be treated as meaningful in the current implementation. The external chain, account zero and a limited index range do not cover passphrases, additional accounts, change addresses, multisig descriptors, non-standard derivation paths or all supported output types.
The hit-reporting path was incomplete
The GPU loop tests 26 session hypotheses, but the hit structure records only the 32-bit pad. It does not record which session hypothesis produced the matching seed. The mnemonic is then regenerated through the simpler CPU function, which does not reproduce that GPU session path. This means a hypothetical match could preserve the derived private key used for the lookup while reporting a mnemonic that did not correspond to it. Because the live scan produced zero hits, this failure was not triggered, but the reporting path would need to be corrected and independently tested before any result could be trusted.
Dataset limitations
The explicit target list contained only 504 addresses and the larger download failed. The global UTXO table was broader, but its importer represented only selected address types and did not preserve value, transaction or provenance information in each lookup key.
Test and measurement limitations
The final test suite is stale, no complete GPU benchmark log was preserved and the repository itself has no commit history. This makes it difficult to prove which exact source version, arguments and binary produced every artifact.
What I learned
The most important thing I learned is that the vulnerable PRNG was only the beginning of the problem.
Reimplementing ten lines of integer operations was easy. Reconstructing how build configuration selected a symbol, how firmware consumed randomness during boot, how rejection sampling changed the number of advances, how user input affected the state and how a mnemonic became multiple address types was the real research.
The other lessons were:
- a hash can redistribute weak entropy but cannot create new entropy
- a 24-word phrase can look perfectly normal while coming from a small search space
- the presence of secure code in a firmware image does not prove that sensitive callers reach it
- end-to-end known-answer vectors are more useful than isolated unit tests for this type of bug
- estimates for one cryptographic primitive are not the same as end-to-end scanner throughput
- negative results need to be reported with the same precision as positive ones
- preserving commands, logs, versions and hashes is part of the research, not administrative work after it
Defensive implications
The failure also suggests concrete checks for hardware-wallet and embedded-firmware teams.
- verify the implementation reached by every security-sensitive symbol in the final linked artifact, not only that the intended source file compiled
- reject builds that contain a forbidden software RNG implementation or expose an ambiguous entropy symbol
- test the seed-generation call path on physical release hardware with instrumentation that identifies every contributing entropy source
- fail closed when a required hardware source is unavailable instead of returning output that only looks random
- treat hashing and deterministic mixing as conditioning steps, never as replacements for a minimum amount of unpredictable input
- preserve build manifests, linker maps, firmware hashes and hardware-validation records for every release
- maintain cross-implementation tests so the firmware, emulator and any recovery tooling agree on all deterministic steps
Coinkite’s hotfix follows the first two principles by excluding the fallback object and adding a build-time check for the expected rng_get() symbol. The broader lesson is that an entropy design needs a testable end-to-end requirement: not simply “the firmware contains a TRNG driver,” but “this exact seed-generation path obtains enough entropy from these verified physical sources.”
Current safety note
Affected users should follow the current instructions published by the manufacturer on the Coldcard security status page. Updating firmware fixes future seed generation but does not retroactively add entropy to a mnemonic created by affected firmware. The official guidance should be consulted directly because fixed-release recommendations and migration instructions can change.
Do not paste a mnemonic, passphrase or private key into an online checker. Do not send sensitive wallet material to someone offering to test it. A research tool that can reconstruct a wallet candidate must treat its output as a live secret even when it was created for defensive purposes.
Conclusion
this is how I approached the Coldcard entropy exploit without owning the physical hardware.
I started from what looked like a 32-bit brute-force problem and ended up spending most of the time on firmware state, linker behavior, boot-time consumers, input sessions and validation. The prototype reached the complete chain from a candidate PRNG state to a Bitcoin UTXO lookup, but the experiment stopped after 54,680,000 pad values, 1,421,680,000 nominal state variants and zero live hits.
For this reason, I consider the project a partial reproduction and a useful study of the failure, not a successful compromise of Coldcard wallets.
If I continue the research, the next step will not be a larger scan. It will be obtaining an authorized physical test device with a known affected firmware, producing an independent seed-generation trace and turning that trace into a versioned known-answer test shared by the Python, C and CUDA implementations.
That is the point at which the model could move from “consistent with the public source code” to “verified against the hardware it is intended to reproduce.”
References
- Coinkite — Technical Deep Dive into the Entropy Issue
- Coldcard — Current Security Status
- Galaxy Research — Coldcard Exploit Abates as Total Losses Climb to at Least 1,700 BTC
- Nader Cserny — Coldcard Money Map
- Wizardsardine — Critical Coldcard Flaw: What Happened, Who Is Affected, and What to Do
- BIP-39 — Mnemonic Code for Generating Deterministic Keys
- BIP-32 — Hierarchical Deterministic Wallets