Vulnerabilities in the Cashu ECash Protocol

In July 2025 I discovered vulnerabilities in the Cashu protocol and some Cashu wallets, and reported them to select Cashu developers. I sent an early draft of this article to them, and we worked together to discuss and address the vulnerability. We agreed on a long term and a short term fix, both of which I believe successfully mitigate any exploits. I was paid a $500 bug bounty for finding it and reporting it responsibly.

This article describes what I originally found, and why it needed to be patched. I also discuss the patches implemented by Cashu wallet development teams.

Prerequisite Knowledge

Already familiar with Ecash? Click here to skip to the fun parts.

Notation

Just so we’re all on the same page:

Notation Meaning
The base-point of the secp256k1 curve.
The order of the secp256k1 curve. There are possible valid non-zero points on the curve, plus the ‘infinity’ point (AKA zero).
Sampling randomly from the set of integers modulo . Note that we exclude zero when sampling.
Concatenation of the byte arrays and .

Ecash

Chaumian Ecash implementations backed by Bitcoin are a growing privacy-focused subsection of the Bitcoin usage landscape, and with good reason: Reviving a well-studied technology and repurposing it for Bitcoin offers a new set of of usability improvements and trade-offs, making Bitcoin-powered payment systems fit into new use cases.

An Ecash mint issues Ecash notes in various denominations, which can be redeemed later at the mint for some fungible commodity or service (such as Bitcoin). This is conceptually very similar to the physical banks of ye olden days, which accepted deposits of some fungible asset like silver or gold, and issued physical cash (paper notes or metal coins) of various denominations in return, which could be used to reclaim the equivalent amount of the physical asset.

In the case of a Bitcoin-backed Ecash mint, the mint accepts Bitcoin deposits, and issues Ecash notes which can be redeemed for Bitcoin at a future time, for as long as the mint remains solvent and operating.

The basic principles of an Ecash mint are:

  • Authenticity - The Ecash mint can be certain the Ecash notes it creates cannot be forged by others, so that when a depositor redeems a note, the mint is safe against fraudulent withdrawals.
  • Fungibility - Depositors can be certain the Ecash notes they receive are anonymous and fungible. Instead of the mint giving a depositor a bearer token (which would have to be recognizable by the mint later when redeemed), the depositor cooperates with the mint to blind the Ecash notes they receive through a clever cryptographic protocol called blind signatures.

How it works

To fully understand the vulnerability, we must first understand the inner mechanics of Ecash blind signatures.

Imagine a mint, with a secret key and public key .

Alice trusts the mint operators and knows . She decides to make a $1 deposit into the Ecash mint. She expects an equivalent amount of Ecash in return.

Let be a hash function which maps some arbitrary input data to a point on the secp256k1 curve, in such a way that the discrete log of is unknowable. Here is an example of one such a hash function.

  1. Alice samples some random scalar

  2. Alice picks a random secret and hashes it into a point

  3. Alice gives the point to the mint.

  4. The mint blindly signs Alice’s blinded point and returns to Alice. This point is called the promise.

  5. Alice unblinds the promise into a proof .

The pair of values is a bearer token which Alice can give back to the mint at a time of her choosing. But because only Alice knows and , only she knows the token , and so nobody can link that token to her deposit - at least, not mathematically.

To verify the authenticity of this token later at redemption time, the mint can check:


The only other way for someone to have constructed would have been to compute given the discrete log such that . However, the hash-to-curve function Alice used ensures that no such can be knowable, and so the only way Alice could know is if the mint itself created using its secret key .

The above is the essence of Ecash, but there are some gotchas to clean up:

  • The cryptography alone doesn’t protect against replay attacks or double-spending. Alice could resubmit , and so the mint must remember which values of or have already been redeemed.
  • Only the mint can verify its own Ecash notes. For Alice to pay someone with an Ecash note, the recipient must be able to swap out the note for a fresh one by contacting the mint directly. The recipient hasn’t been paid until they successfully swap Alice’s Ecash note for a fresh one which only they know.
  • So far we’ve assumed the mint only has a single key pair and . In reality the mint needs a way to issue and distinguish notes of different denominations ($1, $5, $50, etc) and Ecash mint implementations usually do this by having multiple keys: one per denomination. In the Cashu protocol, a mint groups these keys into a “Key Set” with a unique hash identifier. Imagine this like having a different printing template for $1 paper cash notes compared to $5 notes.

The Vulnerability

The vulnerability, which all exploits described in this article depend upon, lies in the Cashu specification document NUT-13: Deterministic Secrets. This document describes a deterministic backup standard for Cashu wallets. Let’s briefly summarize how NUT-13 works.

NUT-13

The idea behind NUT-13 is to give Cashu wallets a standardized way to generate any secret preimage and blinding factor deterministically, so they can be recovered later from a static backup key. The wallet can then use NUT-09 to recover the blinded signature from the mint, and repeat the unblinding again to get a valid ecash proof .

To achieve this, NUT-13 uses BIP39 seed phrases and BIP32 key derivation, inspired by classic hierarchical deterministic (HD) Bitcoin wallet standards. A NUT-13 compliant wallet is supposed to generate a 12-word BIP39 seed phrase when first launched. The user is typically encouraged to save this seed phrase somewhere secure.

The wallet software hashes the mnemonic into a seed, in the manner described in BIP39. The seed is then hashed into a BIP32 master key, as described in BIP32. When receiving or minting ecash from a mint, the wallet derives any secret preimage and blinding factor deterministically from the master key.

To prevent reuse of preimages or blinding factors, the wallet is expected to manage a stateful counter for each unique keyset. The wallet must increment the counter for each new ecash proof minted from that keyset ID. Remember this. It will be important later.

Concretely, NUT-13 specifies that wallets must derive and from the master key with a specific key path derived from two parameters, keyset_id_int and counter_k:

1
2
secret_derivation_path = m/129372'/0'/{keyset_id_int}'/{counter_k}'/0
r_derivation_path = m/129372'/0'/{keyset_id_int}'/{counter_k}'/1
  • counter_k is just an integer, statefully managed by the wallet on a per-keyset basis.
  • keyset_id_int is a reduced integer representation of the relevant keyset ID, which itself is 16 hexadecimal characters. This is computed as:
1
keyset_id_int = parse_int(keyset_id_hex, base=16) % (2 ** 31 - 1)

why not just use the keyset ID directly as a 64-bit integer?

Because BIP32 derivation path elements must be 32-bit integers, and their most significant bit indicates whether hardened derivation is required. NUT-13 reduces the keyset ID modulo to ensure the result does not overflow a uint32 when is added for hardening.

The wallet then derives two BIP32 child keys at these paths, and uses the child private keys as the secret and blinding factor .

The Flaw

The problem is thus:

Wallets track counters by keyset ID, but secrets are derived only from the reduced 31-bit keyset ID integer representation.

The Cashu spec does not require clients to validate keyset IDs are derived correctly, so a malicious mint can easily choose a keyset ID whose 31 bit integer representation collides with that of another mint’s keyset.

If a NUT-13 wallet receives ecash proofs from two such colliding keysets, the wallet will reuse the same set of preimages and blinding factors for outputs on both keysets.

Example

At the time of writing, the Minibits mint’s current active keyset has an 8-byte hex ID 00500550f0494146, which in base-10 integer form is .

To pick a keyset ID in the same 31-bit residue class as Minibits’ keyset ID , I simply generate a random positive integer , and then compute my new keyset ID as

This satisfies . Thus, any NUT-13 wallets will derive the same preimages and blinding factors when minting ecash proofs from keyset as they do for keyset .

Keyset ID Verification

According to NUT-02, Keyset IDs are supposed to be derived from a hash of the public keys constituting the keyset. However, in my research I found no Cashu clients which actually verify the keyset ID is derived correctly, so mints are pretty much free to choose whatever keyset ID they want.

Yet, even if clients did verify the keyset ID is correct, this still doesn’t fix the problem.

There are only (about 2 billion) possible keyset ID integer representations. That’s a puny search space for modern computers. With a simple 100-line rust program I brute-force searched using my below-average CPU, and within a few hours I found 4 completely valid keysets whose keyset ID integer residues collided with those of popular Cashu mints.

The Exploit

Reusing a secret preimage and blinding factor across two different mints does not inherently compromise any ecash. An attacker running a malicious mint must conduct a carefully targeted attack to compromise proofs from a target mint, one key at a time.

To fall victim to this attack, the user (or his wallet) must first attempt to swap and then spend ecash proofs from the malicious mint. Unfortunately, some Cashu wallets have automated background tasks which do exactly this, sometimes without any user interaction at all.

Method

To execute an attack, I first select a target mint and a target pubkey from one of that mint’s keysets, whose corresponding secret key is , unknown to me. My attack is intended to steal ecash proofs issued by , probably with the end goal of melting (withdrawing) them over the Lightning Network.

I construct a keyset consisting of a single public key of denomination 1 sat. I construct the keyset such that for some secret scalar known only to me. The keyset ID of is manipulated (either analytically or by brute-force) so that its 31-bit residue collides with that of my target mint’s keyset ID.

I spin up a custom mint server which I control, with a domain name, TLS certificates, etc. Make it look authentic, but don’t advertise it. My mint publishes the keyset as the only active keyset on its /v1/keys and /v1/keysets endpoints.

Airdropping

I select some target users, possibly using Nostr metadata events to determine which wallet my victims are using, and which mints they use. The Nostr integrations baked into most Cashu wallets makes mining this data surprisingly easy.

I send them each a “poisonous airdrop” Cashu token, consisting of Cashu proofs sourced from my mint, possibly issued under a different keyset than . The data in these proofs themselves don’t matter - what matters is whether the victim users will attempt to claim the proofs.

Swapping

If a victim user swaps the airdropped proofs for fresh proofs, the user’s wallet will derive secrets and blinding factors from the BIP32 path m/129372'/0'/{keyset_id_int}'/{counter_k}'/{0,1}. The keyset_id_int will match that of the target mint’s keyset, but because my malicious keyset has a distinct ID, the user’s wallet will initialize a brand new state counter.

For every new blinded output index the victim requests from my mint, they will derive secret , challenge , and blinding factor . When they send the swap request, the user’s wallet reveals a blinded message . These values will all be identical to those which the user would’ve (and may have already) used when transacting with my target mint for the same counter value .

Example: I send a victim 1024 sats in a single proof from my mint, using a normal but inactive keyset. The user swaps the proof with my mint. Because my mint’s only active keyset is which only contains a single 1-sat denomination key, the user’s wallet will request 1024 output proofs from that 1-sat key. The user’s wallet reveals , where .

Restoring

Before responding to the victim’s swap request, my mint contacts the POST /v1/restore endpoint of my target mint, as defined in NUT-09. This endpoint essentially acts as an input/output record for every previous blind signature the target mint has authored.

My mint now possesses a set of blinded messages received from the victim which may have been used on the target mint as well. I use the target mint’s NUT-09 /v1/restore endpoint to test all these blinded messages. If the mint has previously signed one of the blinded messages , the target mint returns:

Note that the /v1/restore endpoint returns any signatures made across the entire keyset. A blinded signature is only relevant to our attack if the signing key used matches the target pubkey , and not some other key in the target mint’s keyset. We can filter out irrelevant signatures by checking the amount denomination in the response data matches the denomination of .

I have now “recovered” (stolen) any available blinded signatures from the target mint, and I can use them to build a response for the victim’s still-in-progress swap request.

Swap Response

For each blinded signature we found, the malicious mint multiplies it by my secret , and returns it to the user for output :

For each blinded message for which we did not find a blinded signature on the target mint, we return and store in a database. It will be useful later, I promise.

Unblinding

Upon receiving all from our malicious mint, the victim wallet will perform the usual ecash unblinding algorithm using the reused blinding factors and our mint’s malicious pubkey .

These will be stored in the user’s wallet as if they were valid proofs. Usually at this point, the user receives UI confirmation that the Cashu token they received was valid, and their wallet balance visibly increases.

Spending

The user may attempt to spend some of these newly acquired proofs, such as by melting and sending over the lightning network. The user may also send proofs directly to someone else as a Cashu token, and the receiver will then swap the proofs to confirm their validity just as the user first did.

In either case, the proof secret and unblinded signature will be sent to my malicious mint.

Stealing

When we receive a proof issued by our malicious swap protocol, we first check the target mint if the proof for challenge has been spent. If so, we ignore this proof - there is nothing to steal.

If the proof is unspent, then we take one of two paths to try to steal it:

  1. may have been constructed from a blind signature recovered from the target mint.

In this case, my mint receives:

We can compute the target mint’s signature on valid under :

Note that due to the blinding done by the victim, we cannot verify mathematically if . The only way we can check is by attempting to swap the ecash proof at the target mint. We do exactly this, and only proceed to path 2 if swapping fails.

  1. Alternatively, was constructed without a blind signature from the target mint.

Then in this case, our mint has been given:

This lets us isolate the term which contains the secret blinding factor, which we still don’t know.

By itself this is not enough to steal any ecash, but we can combine this with the archive of blinded messages received by our malicious mint.

Before doing that though, we should hit the target mint’s /v1/restore endpoint again, passing any blinded messages for which we didn’t already find a blind signature back in the Restoring phase of the attack. We maintain an up-to-date mapping of , mapping blind messages to their corresponding blind signatures “recovered” from the target mint.

Now we can iterate through each archived blind message/signature tuple and compute a potentially valid unblinded signature :

If the blinding term we extracted matches the archived blind signature , then , and:

Again we cannot verify this mathematically from the victim’s input alone due to blinding, so we must check each candidate proof by attempting to swap it on the target mint. If we find a valid signature , we can stop iterating and erase .

If we find no match, then we know must be a secret which the target mint has not signed yet. We put the victim’s proof into a background queue which occasionally re-checks it against any new blind signatures recovered from the target mint.

Final Response

Our final response to the victim’s swap/melt request is not very consequential. While we’ve already gotten everything we need to steal some proofs from the victim, we are limited to stealing at most proofs if the user only ever spends of the proofs from our malicious key set .

This attack makes it impossible to verify whether the victim’s proofs are “authentic” or not, so the reasonable approach would be to simply hard-code the /v1/swap endpoint to auto-succeed, and hardcode the /v1/melt/* endpoints to return a success status with change outputs, without actually allowing users to pull money out of the malicious mint. The hope is that the victim retries or otherwise keeps interacting with our mint, and in so doing exposes more and more of their proofs from the target mint.

Analysis

A discussion of the properties of this attack: drawbacks, practicality, mitigations, etc.

Affected Software

Though some were more vulnerable than others, any Cashu wallet which uses NUT-13 seeds was at risk of attack. Unfortunately, this means most Cashu wallets were vulnerable. I certainly identified that Minibits, Cashu.me, and Nutstash were vulnerable.

For the attack to work, a victim wallet must do two things:

  1. Swap my airdropped proofs out for new proofs.
  2. Spend the new proofs in melt or swap operation.

Upon receiving a token from an unfamiliar mint, some Cashu wallets may show a prompt asking if the user would like to add the mint and accept the token. If the user clicks yes, they will have completed step 1. If the user then tries to spend those tokens in any way, they will fall victim to step 2.

In other cases like Minibits, Cashu wallets receiving a new token will automatically add my mint to the user’s trusted mint list, and swap the airdropped proofs out for new ones. This ticks off step 1, but not step 2. For that, the user must take manual action.

In the worst cases, some wallets may have a “transfer to trusted mint by default” option which does both step 1 and step 2 without user interaction. These wallets are the most vulnerable as they can be attacked without requiring any action from the user, aside from unlocking the wallet.

Practicality

This attack is complex, but surprisingly practical. The only major hitch which hampers a large-scale attack is that the malicious mint can only really attack one target key at a time. This could be improved - or worsened, depending on your perspective - for the special case of victims whose wallets auto-swap our airdropped proofs.

We can wait until the victim is online, and then break our airdrop up into discrete steps. The first airdrop might target the upstream mint key . Our malicious mint advertises a keyset containing the key . We send a 1000 sat token to the victim, and the victim wallet auto-swaps it with 1000 outputs issued from . We then rotate our keyset to target , setting . Then we send a second 1000-sat token which the victim auto-swaps into proofs from . This may only take a few seconds to execute.

After all airdrops are complete, the user has a wallet filled with 2000 proofs from our two malicious keysets. If the victim were to try to spend all 2000 sats at once, they would reveal to us the first 1000 proofs they were issued from both and .

We could scale this attack up to cover more target keys if desired.

Visibility

From the user’s perspective, this is what an attack would look like:

  1. (optional) I receive an ecash token, possibly with some social engineering message like “We’re starting a new mint, and we’re giving out free sats! Withdraw if you like, it’s your money now!”. I paste or scan the token into my wallet.
    • I may need to manually approve adding a new trusted mint.
    • This step may be skipped in cases where wallets have auto-receive enabled.
  2. I see UI confirmation of a newly received ecash transaction under the new mint.
  3. I try to withdraw my newly airdropped ecash over lightning.
  4. The lightning withdrawal says it succeeded, but the invoice wasn’t actually paid. Weird. Maybe try again?
  5. Well this mint definitely doesn’t work. Block it and go on with my day.
  6. Some time later, I try to pay a lightning invoice from my balance on another mint, but the transaction fails with a “proof already spent” error. Where did my money go?

Mitigations

After speaking with the Cashu developer community and debating different options, we arrived at two fixes: A short-term backwards-compatible fix, and a long-term protocol-level fix.

Long Term Fix

The long term fix is the easier one to understand. In a perfect world all Cashu users would migrate to it immediately.

It’s very simple. Just ditch BIP32 completely - BIP32 was meant for a very different purpose - and instead compute secrets and using a single hash or HMAC, invoked on the full keyset ID and counter.

1
2
3
hash = hmac_sha512(seed, keyset_id + counter.to_bytes())
x = hash[:32]
r = hash[32:]

This approach cryptographically compartmentalizes deterministic secrets scoped for different keysets, provided that the wallet manages the stateful counter correctly and verifies keyset IDs. It fixes the core flaw, which is that secrets derived via NUT-13 for distinct keyset IDs may collide.

When I initially contacted the Cashu developer community about this, they mentioned that improving security of keyset IDs at the specification level was already ongoing work, and they were actively working to transition to longer 256-bit keyset IDs, dubbed “keyset ID v2”. We agreed this could be a good opportunity to insert this long-term protocol-level fix into the Cashu specification itself.

Short Term Fix

The short term fix is more complicated, because it must be backwards-compatible with the existing protocol so as not to break interaction between existing mints and wallets. This fix should only be used until wallets and mints have updated to support v2 keyset IDs and the more long-term secure deterministic secret derivation scheme.

Cashu wallet developers have been advised to add code to their applications which guards against the attack scenario where two keyset IDs have colliding 31-bit residues. This more or less means a wallet must constantly check every keyset ID it encounters to see if any have colliding residues. If a wallet finds a new keyset ID with a residue which collides with one in its cache, the wallet should prompt the user to confirm which mint they trust more. Proofs issued by the less-trusted mint should be marked as hazardous, possibly unspendable.

  • Note that it’s not enough to only compare against active keyset IDs, because an attacker could target the inactive keysets of legitimate mints.
  • We also cannot just focus on keysets for which the user currently holds valid proofs, because the attacker could proactively trick a victim into revealing secrets they haven’t used on the target mint yet (but may use in the future). As soon as those secrets are used on the target mint to create blinded proofs, the attacker could then steal those proofs and unblind them.
  • Trust-on-first-use (TOFU) is not a good policy here, because it’s feasible than an attacker could swoop in early with some kind of timing attack to fool wallets into becoming their more-trusted mint. The user needs to be informed that something is wrong so they can recognize and rectify the situation.
  • Note that some wallets (Minibits) track counters not just by keyset ID but also by mint, which creates another opportunity for this attack to re-emerge. All cashu wallets should ensure they are compliant with the updated version of the NUT-13 spec.

Conclusion

As of publishing time, I have not personally gone through every Cashu wallet to verify these bugs have been patched - I had a very busy Autumn working on optimizing post-quantum cryptography - But the Cashu developers have assured me the short term fix has been effectuated in every major Cashu wallet, and the long term keyset ID v2 protocol update is well on its way with implementations forthcoming.

Along the way I hope readers take home a few lessons about security engineering in general:

  • Look closely at apps which perform automated tasks using sensitive bearer secrets. Avoid auto-trusting anything outside direct user input (and even then).
  • Deterministic secrets are fickle. Pay attention to how the derivation mechanism works, but also how it is used. There could be mistaken assumptions.
  • Be careful when using “SHOULD” in a cryptographic specification. Figure out when “SHOULD” needs to be “MUST”.
  • Watch out for injections - Anytime a large domain is pigeonholed into a smaller space.

Big thanks to the Cashu devs for bearing the bulk of the work of actually fixing this thing. While the initial research was challenging, there is little I find more prosaically daunting than corralling teams of open source devs to fix an obscure vulnerability, and they saved me from attempting that myself.