What happened
On 19 August 2026 Bitcoin Core merged pull request 35859, which changes the type of one number in the wallet's encryption code. The description states the problem exactly: "CMasterKey::nDeriveIterations values are deserialized from wallet files as unsigned 32-bit integers, but key derivation narrowed the count to a signed int. A count above INT_MAX became negative in the conversion, and the derivation loop counter then overflowed, which is undefined behavior."
What it changes
Encrypting a Bitcoin Core wallet does not encrypt your private keys with your passphrase. It encrypts a master key with your passphrase, and the master key encrypts the keys. That indirection is why changing a passphrase is instant rather than a re-encryption of every key you own.
Turning a passphrase into an encryption key is the job of a
key derivation function, and the whole
point of one is to be slow, so that guessing costs the guesser real time. Core sets that
cost by measuring your machine. EncryptMasterKey runs the derivation, times it, and
scales the count to a target_time of 100 milliseconds, taking the weighted average of
two runs. There is a floor beneath it, DEFAULT_DERIVE_ITERATIONS = 25000, carrying a
comment that has aged into a fossil: "25000 rounds is just under 0.1 seconds on a 1.86 GHz
Pentium M."
The count then lives in the wallet file, and a file is data. It is stored as an
unsigned 32-bit integer, so any value up to 4,294,967,295 round-trips through it
faithfully. The derivation code took that value as a signed int. Anything above
2,147,483,647 came back negative, and the derivation loop counter then overflowed. That is
undefined behaviour rather than a
wrong answer: the compiler is entitled to assume signed overflow cannot happen and to
generate code on that assumption.
The fix keeps the count unsigned along the whole derivation path so it matches the type
it was serialised as, and
crypter.h now
declares SetKeyFromPassphrase and BytesToKeySHA512AES with unsigned int rounds.
Three smaller changes travel with it, and they are the ones that generalise: calibration
failures are now checked, the calculated count is validated before conversion with
if (target_iterations < 1 || target_iterations > std::numeric_limits<unsigned int>::max()),
the output master key is left "unchanged until derivation and encryption succeed", and
the crypter's fallible methods are marked [[nodiscard]] so that ignoring a failed
derivation stops compiling.
What it does not change
The derivation itself is untouched. What changed is the type of a counter, not the function that turns a passphrase into a key, so no passphrase became easier or harder to guess. The pull request describes no exploit and names no affected wallet: it reports a type mismatch and the undefined behaviour that follows from it, which is a different thing from a working attack. Reaching the bad path at all takes a wallet file carrying a count that the 100 millisecond calibration on a real machine does not produce.
The design around it is untouched too, including the part that surprises people: the cost of your wallet encryption is a measurement of the computer you happened to encrypt it on. Encrypt on a slow laptop and the count is low, permanently, until you set the passphrase again on something faster. The floor is still 25000 rounds, still explained in the source by a comment about a Pentium M.
And wallet encryption goes on doing only what it ever did. It protects the file at rest, against someone who copies it. It does nothing while the wallet is unlocked, nothing against a compromised machine watching you type, and it is not a backup: a passphrase guards the file, and the seed phrase is what restores the coins if the file is gone.
Context
This lands in a month that has already made key handling the story. Coldcard's rebuild of its entropy generation was a failure in how a seed was produced. This is smaller and further along the same path: not how the randomness was made, but how the number guarding it was typed.
The two share a shape worth noticing. In both, the security property was correct on paper and the defect was in a value's representation, one that no test happened to exercise because normal use never produces the value. Core's answer here is the boring one, and it generalises past this bug: match the type you serialise, validate before you narrow, and make the compiler refuse to let a caller ignore a failure.
