Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Electrum lacks a clear way to handle dust attacks #6960

Open
CodeForcer opened this issue Jan 21, 2021 · 14 comments
Open

Electrum lacks a clear way to handle dust attacks #6960

CodeForcer opened this issue Jan 21, 2021 · 14 comments
Labels
bug 🐞 topic-wallet 👛 related to wallet.py, or maybe address_synchronizer.py/coinchooser.py

Comments

@CodeForcer
Copy link

CodeForcer commented Jan 21, 2021

Recently our hot wallet was dusted with an output from an advertisement transaction. The advertisement transaction remains in the mempool unconfirmed, assumably for a couple days before being dropped.

The hot wallet continues processing withdrawals, which are all unconfirmed on the blockchain, until finally crashing with the mempool too long error.

At this point we are in an "all hands on deck situation".

A developer loaded the hot-wallet private key into the Electrum GUI and attempted a cancellation for the transaction immediately after the dust input arrived (spending the dust input) with a very high transaction fee, almost $200 USD.

This cancelled the 20+ unconfirmed tx's, leaving just the RBF transaction. However, because the dust attack never confirms the RBF never confirms, despite the huge fee. Another RBF is attempted with a $400 USD transaction fee, but this likewise never confirms.

Unfortunately at this point it doesn't seem like Electrum has any effective way to fix the situation. Freezing inputs is only allowed for inputs which are not currently part of a transaction, so the coins tab does not show the dust inputs or otherwise allow you to freeze them.

Also, modifying with RBF doesn't allow you to drop an input, nor does Electrum GUI seem to allow a custom transaction while selecting inputs from transactions you want to replace.

What can we do in these situations to free coins that have been dusted? The ability to freeze inputs only helps you if a dust input has arrived in your wallet, not in a situation where your wallet has already attempted to spend the dust input.

Lastly, is it possible to implement functionality in Electrum to easily stop this kind of attack? For example, automatically disabling the use of dust inputs in new transactions would prevent this from happening.

Thanks

@SomberNight
Copy link
Member

SomberNight commented Jan 21, 2021

Was this dust sent to an address that has had other larger UTXOs at that time?
I don't see why the coinchooser would spend the dust, except if it wanted to spend another larger UTXO and due to the privacy heuristics pulled in all other UTXOs from that address.


workaround.

  • remove the unwanted unconfirmed txs from your wallet:
    (could also use wallet.remove_transaction(txid) but that assumes you never delete txs that have children in the wallet, whereas this handles that too)
    window.history_list.remove_local_tx("fea1476847268cca3f9b6ecfdbf4422601a657c84660262e9696e805872d878f")
    
  • freeze dust UTXO in Coins tab using GUI

I am trying to come up with an automatic solution.


EDIT:

our hot wallet was dusted
The hot wallet continues processing withdrawals

This was the original point of error -- that the dust UTXO was chosen to be spent.
Can you confirm that you used Electrum at this point?
Or did you only try to use Electrum to recover from the situation?

@CodeForcer
Copy link
Author

CodeForcer commented Jan 21, 2021

Yeah, we used Electrum command line to process the withdrawals, so the coin-chooser must have selected the dust inputs for spending. We only trust Electrum for our production systems and also run our own ElectrumX node.

The dust transaction at the time of writing is still unconfirmed:
https://www.blockchain.com/btc/tx/b4a5d5ddcffab99987509049b02d3fa42fd2f46a1602ab6ceee751cc22edac2f

I assume that it will get dropped at some point, but they have chosen a fee to maximise the time it will hang around in the mempool for.

EDIT: Btw, we load a wallet with a single address, rather than a mnemonic. Not sure if that affects the coin selection

SomberNight added a commit that referenced this issue Jan 21, 2021
Main motivation is that I often use wallet.remove_transaction
from the Qt console, and would find this behaviour more intuitive.
Note that previously if one were to call this on a tx with children,
the crash reporter would appear with "wallet.get_history() failed balance sanity-check".

related: #6960 (comment)
@SomberNight SomberNight added bug 🐞 topic-wallet 👛 related to wallet.py, or maybe address_synchronizer.py/coinchooser.py labels Jan 21, 2021
@SomberNight
Copy link
Member

Note that there are two issues here:

  1. the coinchooser should be made smarter not to select these malicious "dust" UTXOs
    (but I don't have a good definition of "dust" yet -- it's not the usual definition as they are above the dust limit)
  2. the GUI should provide a way to recover from this situation after it had already occurred

@yeschacha
Copy link

how about providing an option to user to ignore / auto freeze any UTXOs < some value during sending or at all times.

@SomberNight
Copy link
Member

SomberNight commented Jan 21, 2021

how about providing an option to user to ignore / auto freeze any UTXOs < some value during sending or at all times.

Yes, that is pretty naive but it could work.

Note that we need something that works by default. It should not need explicit enabling by the user.
For example, OP could have already avoided this situation had he opted in to "only spend confirmed coins".


EDIT:
The issue with a fixed threshold (even if configurable, there would be a default) is that you could still be targeted and sent threshold+1 satoshis in a huge low-fee unconfirmed tx, which then could still result in the same situation as in OP.


EDIT2:
related:
WalletWasabi/WalletWasabi#2454
JoinMarket-Org/joinmarket-clientserver#471

@CodeForcer
Copy link
Author

CodeForcer commented Jan 21, 2021

@SomberNight if I select "only spend confirmed coins" then it limits the address to a single withdrawal per block when using a singular address.

This is workable for some applications, but unfortunately not if you expect to be processing potentially hundreds of transactions from an address every hour.

The naive approach you outlined above would be great IMO, because it puts the onus back on the attacker to spend larger amounts of funds if they want to dust you. For an address that processes withdrawals and only has large incoming transactions you can just set the threshold at 0.1 BTC (for example), and rest assured that no-one will dust you.

@SomberNight
Copy link
Member

So are most of your UTXOs usually unconfirmed?
Because I was thinking that another solution would be to give even more priority than we currently do to try to only spend confirmed UTXOs at first; and only spend unconfirmed ones as a fallback.

@ecdsa
Copy link
Member

ecdsa commented Jan 22, 2021

Because I was thinking that another solution would be to give even more priority than we currently do to try to only spend confirmed UTXOs at first; and only spend unconfirmed ones as a fallback.

isn't it already what we do?

EDIT: In coinchooser.py the preference for unconfirmed coins only applies to 'buckets'. If a bucket with unconfirmed coins is required, the coin chooser will try to spend all the coins from that bucket. Maybe that rule should not apply to unconfirmed utxos (that would be slightly worse for privacy).

@SomberNight
Copy link
Member

@ecdsa yes, exactly that is what I was thinking about.

@SomberNight
Copy link
Member

SomberNight commented Jan 22, 2021

Unfortunately neither solution is without faults.

  1. freezing all inputs below a threshold
    • the problem is that a targeted attack could just send threshold+1 sats.
      • Fine, OP might want to set the threshold to 0.1 BTC or some similar ridiculously high amount,
        but by default we could only really set it to something along the lines of 5000 sat.
  2. changing the coinchooser to start preferring confirmed coins on the UTXO-level rather than on the bucket-level;
    or more generally, any kind of distinguishing done based on if a UTXO is confirmed;
    • the problem is that a wallet might already be in a state where it only has unconfirmed UTXOs.
      • this is especially true for automated/scripted systems, where a wallet creates many outgoing txs -- and in fact such systems might be the most vulnerable to this attack (due to lack of human oversight)

So, I believe

  • (2) would work better for the average user, because they typically have confirmed coins and so their coinchooser could just ignore the malicious UTXO,
    • but note that even for the average user, they can intermittently be in a state where they do not have any confirmed UTXOs
  • (1) would work better for automated systems which create lots of txs, as they might not have confirmed UTXOs in the first place which makes (2) useless for them
    • these users, such as OP, might probably want to set a relatively high threshold (compared to what we might reasonably set by default)

@ecdsa
Copy link
Member

ecdsa commented Jan 22, 2021

I do not like the threshold approach. it is fragile, as you pointed out. if it is set too high it becomes useless. And it adds another parameter, that will raise thousands of questions such as "how to set it?", and "why can't I spend my bitcoins?". And the inevitable reddit experts are going to create threads about it.

the problem is that a wallet might already be in a state where it only has unconfirmed UTXOs.

so what? we are not supposed to fix bugs back in time...

@SomberNight
Copy link
Member

SomberNight commented Jan 22, 2021

note: @ecdsa pointed out on IRC that another weakness of the threshold approach is that the problematic UTXO might have a value above threshold but have an unconfirmed chain of parents of large low-fee transactions. So e.g. many 100 KB txs chained together with the last one creating an above threshold UTXO for the victim

@CodeForcer
Copy link
Author

CodeForcer commented Jan 22, 2021

@SomberNight

So are most of your UTXOs usually unconfirmed?

Yes, it's not uncommon for us to push 10+ transactions in a single block, all chaining off previous ones.

One thing I think worth adding to this discussion is that the majority of these incidents aren't "attacks" per se, rather just spammers advertising things.

So while there are workarounds for "attackers" to create threshold+ transactions with unconfirmed chains of parents that are all large low-fee transactions, this is a very rare occurrence, whereas advertising spam is not that rare - probably happening many times a day.

It might be worth considering simple measures to eliminate these spam situations causing grief, as a seperate concern to dealing with a dedicated attacker who is trying to get past your threshold values.

Lastly, I am not an expert here like you guys, but I assume an attacker who creates a chain of low fee transactions, followed by a high fee transaction to breach your wallets threshold, is taking a huge risk. After all, what if a miner decides to confirm all the transactions for some reason? They could have just deposited many thousands of dollars for free in the wallet of their intended victim.

SomberNight added a commit to SomberNight/electrum that referenced this issue Jan 22, 2021
SomberNight added a commit to SomberNight/electrum that referenced this issue Jan 22, 2021
SomberNight added a commit to SomberNight/electrum that referenced this issue Jan 22, 2021
SomberNight added a commit to SomberNight/electrum that referenced this issue Jan 22, 2021
ecdsa pushed a commit that referenced this issue Jan 29, 2021
@SomberNight
Copy link
Member

#6962 was merged, which implements a variant of option (1)
(the difference from (1) is that these UTXOs are only frozen while unconfirmed; and also that change UTXOs created by the wallet are excluded)

You can configure the threshold:

threshold = self.config.get('unconf_utxo_freeze_threshold', 5_000)

ln2max pushed a commit to ln2max/electrum that referenced this issue Oct 24, 2022
Add documentation to the docstring of the appropriate function
explaining how to disable the spesmilo#6960 test for dusting transactions

TODO: it may make sense to document passing this option on the command
line as well, since editing the config file by hand is a recipe for
trouble.

Unfortunately this is not as simple as doing:
```
electrum --unconf_utxo_freeze_threshold=0
```
and I can't find how electrum takes config arguments on the command
line. Maybe someone else knows?
ln2max pushed a commit to ln2max/electrum that referenced this issue Oct 24, 2022
Add documentation to the docstring of the appropriate function
explaining how to disable the spesmilo#6960 test for dusting transactions

TODO: it may make sense to document passing this option on the command
line as well, since editing the config file by hand is a recipe for
trouble.

Unfortunately this is not as simple as doing:
```
electrum --unconf_utxo_freeze_threshold=0
```
and I can't find how electrum takes config arguments on the command
line. Maybe someone else knows?
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug 🐞 topic-wallet 👛 related to wallet.py, or maybe address_synchronizer.py/coinchooser.py
Projects
None yet
Development

No branches or pull requests

5 participants
@ecdsa @SomberNight @CodeForcer @yeschacha and others