Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
name: CI

on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]

env:
CARGO_TERM_COLOR: always

jobs:
test:
name: Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- name: Build
run: cargo build --verbose
- name: Run tests
run: cargo test --verbose

clippy:
name: Clippy
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy
- name: Clippy check
run: cargo clippy -- -D warnings

fmt:
name: Format
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt
- name: Format check
run: cargo fmt --check
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@ edition = "2021"
[dependencies]
bitcoincore-rpc = "0.19"
bitcoin = "0.32"
clap = { version = "4", features = ["derive"] }
clap = { version = "4", features = ["derive", "env"] }
anyhow = "1"
serde_json = "1.0.149"
19 changes: 17 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@

A Bitcoin CLI tool that detects dust attack UTXOs in your wallet and sweeps them safely using PSBTs ([BIP174](https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki)).

Built in Rust!
---

## Demo

Example of scanning for dust UTXOs and performing a dry-run sweep:

[![asciicast](https://asciinema.org/a/911545.svg)](https://asciinema.org/a/911545)

## What is a Dust Attack?

Expand Down Expand Up @@ -146,6 +150,17 @@ cHNidP8BA...
Send: bitcoin-cli sendrawtransaction <hex>
```

### Sweep using OP_RETURN (burn to miner fees)
```bash
dust-cleaner --rpc-user <user> --rpc-pass <pass> sweep --method op-return
```

Example output:
```
🔥 Method: op-return — dust burned to miner fees
Output: OP_RETURN ("ash" — ashes to ashes, dust to dust)
```

### Custom threshold

```bash
Expand Down
141 changes: 141 additions & 0 deletions docs/demo.cast
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
{"version": 2, "width": 82, "height": 44, "timestamp": 1775798717, "env": {"SHELL": "/bin/bash", "TERM": "xterm-256color"}}
[0.820534, "o", "\u001b[?2004h\u001b]0;jolah1@DESKTOP-N4M1LE0: ~/dev/dust-cleaner\u0007\u001b[01;32mjolah1@DESKTOP-N4M1LE0\u001b[00m:\u001b[01;34m~/dev/dust-cleaner\u001b[00m$ "]
[2.795947, "o", "c"]
[2.980767, "o", "a"]
[3.139852, "o", "r"]
[3.392408, "o", "g"]
[3.835027, "o", "o"]
[4.186488, "o", " "]
[5.40972, "o", "r"]
[5.701888, "o", "u"]
[6.069876, "o", "n"]
[6.946351, "o", " "]
[7.654731, "o", "-"]
[7.837404, "o", "-"]
[8.257093, "o", " "]
[9.03623, "o", "-"]
[9.230662, "o", "-"]
[9.907083, "o", "h"]
[10.337625, "o", "e"]
[10.51073, "o", "l"]
[10.830549, "o", "p"]
[12.792466, "o", "\r\n\u001b[?2004l\r"]
[12.967165, "o", "\u001b[1m\u001b[92m Finished\u001b[0m `dev` profile [unoptimized + debuginfo] target(s) in 0.10s\r\n"]
[12.973381, "o", "\u001b[1m\u001b[92m Running\u001b[0m `target/debug/dust-cleaner --help`\r\n"]
[12.981323, "o", "Detect and sweep dust attack UTXOs from your Bitcoin wallet\r\n\r\n\u001b[1m\u001b[4mUsage:\u001b[0m \u001b[1mdust-cleaner\u001b[0m [OPTIONS] \u001b[1m--rpc-user\u001b[0m <RPC_USER> \u001b[1m--rpc-pass\u001b[0m <RPC_PASS> <COMMAND>\r\n\r\n\u001b[1m\u001b[4mCommands:\u001b[0m\r\n \u001b[1mscan\u001b[0m Scan wallet for dust UTXOs\r\n \u001b[1msweep\u001b[0m Create a PSBT sweeping all dust UTXOs\r\n \u001b[1mhelp\u001b[0m Print this message or the help of the given subcommand(s)\r\n\r\n\u001b[1m\u001b[4mOptions:\u001b[0m\r\n \u001b[1m--rpc-url\u001b[0m <RPC_URL> Bitcoin Core RPC URL [default: http://127.0.0.1:18443]\r\n \u001b[1m--rpc-user\u001b[0m <RPC_USER> \r\n \u001b[1m--rpc-pass\u001b[0m <RPC_PASS> \r\n \u001b[1m--threshold\u001b[0m <THRESHOLD> Custom dust threshold in sats. If omitted, uses per-script-type thresholds\r\n \u001b[1m-h\u001b[0m, \u001b[1m--help\u001b[0m Print help\r\n"]
[12.982709, "o", "\u001b[?2004h\u001b]0;jolah1@DESKTOP-N4M1LE0: ~/dev/dust-cleaner\u0007\u001b[01;32mjolah1@DESKTOP-N4M1LE0\u001b[00m:\u001b[01;34m~/dev/dust-cleaner\u001b[00m$ "]
[15.274456, "o", "c"]
[15.510141, "o", "a"]
[15.687383, "o", "r"]
[15.980204, "o", "g"]
[16.508834, "o", "o"]
[17.879149, "o", " "]
[18.482939, "o", "r"]
[18.748675, "o", "u"]
[19.060231, "o", "n"]
[19.412957, "o", " "]
[22.445408, "o", "-"]
[22.642001, "o", "-"]
[22.924427, "o", " "]
[23.237142, "o", "-"]
[23.458555, "o", "-"]
[25.72683, "o", "p"]
[26.257663, "o", "\b\u001b[K"]
[26.766145, "o", "r"]
[27.368655, "o", "p"]
[27.735021, "o", "c"]
[30.695653, "o", "-"]
[31.182338, "o", "p"]
[33.338241, "o", "\b\u001b[K"]
[33.578218, "o", "\b\u001b[K"]
[34.803893, "o", "-"]
[35.324833, "o", "u"]
[35.888364, "o", "s"]
[36.083225, "o", "e"]
[36.398795, "o", "r"]
[37.953145, "o", " "]
[38.34731, "o", "u"]
[38.676875, "o", "s"]
[38.937002, "o", "e"]
[39.227316, "o", "r"]
[39.696359, "o", " "]
[40.23407, "o", "-"]
[40.448878, "o", "-"]
[41.333096, "o", "r"]
[41.636531, "o", "p"]
[42.040403, "o", "c"]
[43.378474, "o", "-"]
[43.876399, "o", "p"]
[44.168749, "o", "a"]
[44.48426, "o", "s"]
[44.710614, "o", "s \r"]
[44.947755, "o", " "]
[45.496174, "o", "p"]
[45.79453, "o", "a"]
[46.116484, "o", "s"]
[46.332291, "o", "s"]
[46.501898, "o", " "]
[47.26367, "o", "-"]
[47.459406, "o", "-"]
[48.382401, "o", "t"]
[48.699388, "o", "h"]
[49.072755, "o", "r"]
[49.441081, "o", "e"]
[50.802838, "o", "s"]
[50.933947, "o", "h"]
[51.624798, "o", "o"]
[51.896608, "o", "l"]
[52.192583, "o", "d"]
[52.876343, "o", " "]
[53.534242, "o", "1"]
[53.955866, "o", "0"]
[54.145991, "o", "0"]
[54.384159, "o", "0"]
[55.356373, "o", " "]
[55.933721, "o", "s"]
[56.281976, "o", "c"]
[56.899056, "o", "a"]
[57.043523, "o", "n"]
[61.326621, "o", "\r\n\u001b[?2004l\r"]
[61.51143, "o", "\u001b[1m\u001b[92m Finished\u001b[0m `dev` profile [unoptimized + debuginfo] target(s) in 0.09s\r\n"]
[61.519195, "o", "\u001b[1m\u001b[92m Running\u001b[0m `target/debug/dust-cleaner --rpc-user user --rpc-pass pass --threshold 1000 scan`\r\n"]
[61.575349, "o", "Found 9 total UTXOs (threshold: 1000 sats (custom))\r\n\r\n"]
[61.576409, "o", "⚠️ DUST UTXOs (3 found):\r\n 500 sats | 84e32362d15b14d069934ef2fe7a5d7864323d59f6936ef361bce812c2a8c10e:0 | P2WPKH | bcrt1q4x2qz6a7xrau48hflygphv75vmwx60v6xg9dqz\r\n"]
[61.576935, "o", " 300 sats | 1b6cceaba9b1555ad385f3273e3a726a7f3e4a7d1091128d8628caa38d8514cd:0 | P2WPKH | bcrt1q62sx9egww58us7up2f5w3lf2vspeyrjqwupr2q\r\n 800 sats | 1a51b81e315379f2b036f5bdaf4862edfe3b3527c7596c475918e7f8b97ba6a1:0 | P2WPKH | bcrt1qyk3sa24pnfrex8gneldjdwe2n8ed0yhj83uy8s\r\n\r\n✅ CLEAN UTXOs (6 found):\r\n 4999973630 sats | 84e32362d15b14d069934ef2fe7a5d7864323d59f6936ef361bce812c2a8c10e:1\r\n 5000000000 sats | f50406678677be7e407d55ad7eda96df662237555cc5da5b26cc740838eca7ec:0\r\n 4999999860 sats | 81452410ca2bb3de54916a51a0c1b00f1c723bcef470d812895a01f33abb6500:0\r\n 5000000000 sats | e16c27cbd32cbbeb7f1e3d03fd26c77ae4f2c42f8bb122b77dc9d61cdddc8bcd:0\r\n 4999994210 sats | 1a51b81e315379f2b036f5bdaf4862edfe3b3527c7596c475918e7f8b97ba6a1:1\r\n 5000000000 sats | 8789c76955d1107de5731d618424372b5c6467254ae960a214938c16a6852740:0\r\n\r\n─────────────────────────────────────────\r\n📊 Summary\r\n Total UTXOs: 9"]
[61.577538, "o", "\r\n Dust UTXOs: 3 (1600 sats)\r\n Clean UTXOs: 6 (29999967700 sats)\r\n Threshold: 1000 sats (custom)\r\n\r\n 💡 Run 'sweep' to consolidate dust into a single UTXO\r\n─────────────────────────────────────────\r\n"]
[61.578418, "o", "\u001b[?2004h\u001b]0;jolah1@DESKTOP-N4M1LE0: ~/dev/dust-cleaner\u0007\u001b[01;32mjolah1@DESKTOP-N4M1LE0\u001b[00m:\u001b[01;34m~/dev/dust-cleaner\u001b[00m$ "]
[70.912916, "o", "c"]
[71.007195, "o", "l"]
[71.611786, "o", "e"]
[73.418187, "o", "a"]
[73.593535, "o", "r"]
[75.662852, "o", "\r\n\u001b[?2004l\r"]
[75.667244, "o", "\u001b[H\u001b[2J\u001b[3J"]
[75.66789, "o", "\u001b[?2004h\u001b]0;jolah1@DESKTOP-N4M1LE0: ~/dev/dust-cleaner\u0007\u001b[01;32mjolah1@DESKTOP-N4M1LE0\u001b[00m:\u001b[01;34m~/dev/dust-cleaner\u001b[00m$ "]
[87.016649, "o", "\u001b[7mcargo run -- --rpc-user user --rpc-pass\u001b[27m\u001b[7m \u001b[27m\u001b[7mpass --threshold 1000 sweep --dry-run\u001b[27m"]
[87.973369, "o", "\u0007\u001b[A\r\u001b]0;jolah1@DESKTOP-N4M1LE0: ~/dev/dust-cleaner\u0007\u001b[01;32mjolah1@DESKTOP-N4M1LE0\u001b[00m:\u001b[01;34m~/dev/dust-cleaner\u001b[00m$ cargo run -- --rpc-user user --rpc-pass pass --threshold 1000 sweep --dry-run"]
[91.891958, "o", "\r\n\u001b[?2004l\r"]
[92.04665, "o", "\u001b[1m\u001b[92m Finished\u001b[0m `dev` profile [unoptimized + debuginfo] target(s) in 0.07s\r\n"]
[92.053677, "o", "\u001b[1m\u001b[92m Running\u001b[0m `target/debug/dust-cleaner --rpc-user user --rpc-pass pass --threshold 1000 sweep --dry-run`\r\n"]
[92.070096, "o", "Found 3 dust UTXOs to sweep:\r\n 500 sats | 84e32362d15b14d069934ef2fe7a5d7864323d59f6936ef361bce812c2a8c10e:0\r\n 300 sats | 1b6cceaba9b1555ad385f3273e3a726a7f3e4a7d1091128d8628caa38d8514cd:0\r\n 800 sats | 1a51b81e315379f2b036f5bdaf4862edfe3b3527c7596c475918e7f8b97ba6a1:0\r\n\r\n🔍 Dry Run — no PSBT created\r\n\r\n Method: Consolidate\r\n Dust inputs: 3\r\n Total dust: 1600 sats\r\n Funder UTXO: 5000000000 sats\r\n Estimated fee: 626 sats\r\n Estimated output: 5000000974 sats\r\n\r\n Run without --dry-run to create the PSBT.\r\n"]
[92.071323, "o", "\u001b[?2004h\u001b]0;jolah1@DESKTOP-N4M1LE0: ~/dev/dust-cleaner\u0007\u001b[01;32mjolah1@DESKTOP-N4M1LE0\u001b[00m:\u001b[01;34m~/dev/dust-cleaner\u001b[00m$ "]
[116.248237, "o", "\u001b[7mcargo run -- --rpc-user user --rpc-pass\u001b[27m\u001b[7m \u001b[27m\u001b[7mpass --threshold 1000 sweep --method op-return --dry-run\u001b[27m"]
[118.796131, "o", "\u0007\u001b[A\b\b\b\b\b\b\b\b\b\b\b\b\b\bcargo run -- --rpc-user user --rpc-pass pass --threshold 1000 sweep --method op-return --dry-run"]
[119.62632, "o", "\r\n"]
[119.626848, "o", "\u001b[?2004l\r"]
[119.78859, "o", "\u001b[1m\u001b[92m Finished\u001b[0m `dev` profile [unoptimized + debuginfo] target(s) in 0.07s\r\n"]
[119.794165, "o", "\u001b[1m\u001b[92m Running\u001b[0m `target/debug/dust-cleaner --rpc-user user --rpc-pass pass --threshold 1000 sweep --method op-return --dry-run`\r\n"]
[119.815471, "o", "Found 3 dust UTXOs to sweep:\r\n 500 sats | 84e32362d15b14d069934ef2fe7a5d7864323d59f6936ef361bce812c2a8c10e:0\r\n 300 sats | 1b6cceaba9b1555ad385f3273e3a726a7f3e4a7d1091128d8628caa38d8514cd:0\r\n 800 sats | 1a51b81e315379f2b036f5bdaf4862edfe3b3527c7596c475918e7f8b97ba6a1:0\r\n\r\n🔍 Dry Run — no PSBT created\r\n\r\n Method: OpReturn\r\n Dust inputs: 3\r\n Total dust: 1600 sats\r\n Funder UTXO: 5000000000 sats\r\n Estimated fee: 626 sats\r\n Estimated output: 5000000974 sats\r\n\r\n Run without --dry-run to create the PSBT.\r\n"]
[119.81687, "o", "\u001b[?2004h\u001b]0;jolah1@DESKTOP-N4M1LE0: ~/dev/dust-cleaner\u0007\u001b[01;32mjolah1@DESKTOP-N4M1LE0\u001b[00m:\u001b[01;34m~/dev/dust-cleaner\u001b[00m$ "]
[138.60205, "o", "\u001b[7mcargo run -- --rpc-user user --rpc-pass\u001b[27m\u001b[7m \u001b[27m\u001b[7mpass --threshold 1000 sweep --method op-return\u001b[27m"]
[139.284238, "o", "\u0007\u001b[A\b\b\b\bcargo run -- --rpc-user user --rpc-pass pass --threshold 1000 sweep --method op-return"]
[142.210555, "o", "\r\n"]
[142.21145, "o", "\u001b[?2004l\r"]
[142.36781, "o", "\u001b[1m\u001b[92m Finished\u001b[0m `dev` profile [unoptimized + debuginfo] target(s) in 0.07s\r\n"]
[142.373133, "o", "\u001b[1m\u001b[92m Running\u001b[0m `target/debug/dust-cleaner --rpc-user user --rpc-pass pass --threshold 1000 sweep --method op-return`\r\n"]
[142.406946, "o", "Found 3 dust UTXOs to sweep:\r\n 500 sats | 84e32362d15b14d069934ef2fe7a5d7864323d59f6936ef361bce812c2a8c10e:0\r\n 300 sats | 1b6cceaba9b1555ad385f3273e3a726a7f3e4a7d1091128d8628caa38d8514cd:0\r\n 800 sats | 1a51b81e315379f2b036f5bdaf4862edfe3b3527c7596c475918e7f8b97ba6a1:0\r\n\r\n🔥 Method: op-return — dust burned to miner fees\r\n Output: OP_RETURN (\"ash\" — ashes to ashes, dust to dust)\r\n\r\n ℹ️ Using clean UTXO to fund fees: 5000000000 sats\r\n"]
[142.54227, "o", "\r\n📊 Sweep Summary:\r\n Dust inputs: 3\r\n Total dust: 1600 sats\r\n\r\n🧹 Sweep PSBT (base64):\r\ncHNidP8BAPoCAAAABEAnhaYWjJMUomDpSiVnZFwrNySEYR1z5X0Q0VVpx4mHAAAAAAD9////DsGowhLovGHzbpP2WT0yZHhdev7yTpNp0BRb0WIj44QAAAAAAP3////NFIWNo8ooho0SkRB9Sj5/anI6PifzhdNaVbGpq85sGwAAAAAA/f///6Gme7n45xhZR2xZxyc1O/7tYkivvfU2sPJ5UzEeuFEaAAAAAAD9////AwAAAAAAAAAABWoDYXNoQAYAAAAAAAAWABSttCLfgG4/NURqfAHEpBsd1fcygQTkBSoBAAAAFgAUQJeNT5jyl6LaNiWtzN2MWndlLLYAAAAAAAEAgwIAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/////AlUA/////wIA8gUqAQAAABYAFF6XxDXy7piDz1AhPspCGSm9yBVQAAAAAAAAAAAmaiSqIant4vYcP3HR3v0/qZnfo2lTdVxpBol5mWK0i+vYNpdOjPkAAAAAAQEfAPIFKgEAAAAWABRel8Q18u6Yg89QIT7KQhkpvcgVUCIGAoENJdbT6mMTuD2hDkCwv1V4S5QlOyEdjjW5Ppn9OjIqGJi7BkpUAACAAQAAgAAAAIAAAAAAAAAAAAABAHECAAAAAe4a44e+jKVcfWdeXARqGank2Mtorlv5e/5x4ZMeK5Q4AAAAAAD9////AvQBAAAAAAAAFgAUqZQBa74w+8qe6fkQG7PUZtxtPZr+igUqAQAAABYAFDfwRlg6Y/RLC+tMJGvwLMmNTq8AaQAAAAEBH/QBAAAAAAAAFgAUqZQBa74w+8qe6fkQG7PUZtxtPZoiBgMJaNSmn50loDjR98nh/gMrTvFXaGo7NPI2Xl19VMnY6xiYuwZKVAAAgAEAAIAAAACAAAAAABkAAAAAAQBSAgAAAAHsI1uFz5DnI/y7jeQT9iTIKt0jaOpEnApHws6hSNGO8AIAAAAA/f///wEsAQAAAAAAABYAFNKgYuUOdQ/Ie4FSaOj9KmQDkg5AaQAAAAEBHywBAAAAAAAAFgAU0qBi5Q51D8h7gVJo6P0qZAOSDkAiBgLzk3PR1gOWaC0X7bvc/nHr3+5wc5+zPvgPYa1F6FzImhiYuwZKVAAAgAEAAIAAAACAAAAAABgAAAAAAQBxAgAAAAHsI1uFz5DnI/y7jeQT9iTIKt0jaOpEnApHws6hSNGO8AEAAAAA/f///wIgAwAAAAAAABYAFCWjDqqhmkeTHRPP2ya7Kpny15LyYtsFKgEAAAAWABTLA2JCoOsM139b+ft5JSupavi5H2kAAAABAR8gAwAAAAAAABYAFCWjDqqhmkeTHRPP2ya7Kpny15LyIgYDSYFWXz0zDtQ8nb819SBLfP+t/N6mCOsWq2X0ROrHHtkYmLsGSlQAAIABAACAAAAAgAAAAAAaAAAAAAAiAgNjLZZToK8U19OxlBXF1gJK6lJaPQ7gG6/izvhnnt4m6RiYuwZKVAAAgAEAAIAAAACAAQAAABIAAAAAIgICS/cUTihGMLrbjuES/wYIQR9uEn88RIEQ3tD9NdHVr08YmLsGSlQAAIABAACAAAAAgAAAAAAbAAAAAA==\r\n\r\n💡 Next steps:\r\n Inspect: bitcoin-cli -rpcport=18443 -rpcuser=user -rpcpassword=pass decodepsbt <psbt>\r\n Sign: bitcoin-cli -rpcport=18443 -rpcuser=user -rpcpassword=pass walletprocesspsbt <psbt>\r\n Send: bitcoin-cli -rpcport=18443 -rpcuser=user -rpcpassword=pass sendrawtransaction <hex>\r\n"]
[142.54348, "o", "\u001b[?2004h\u001b]0;jolah1@DESKTOP-N4M1LE0: ~/dev/dust-cleaner\u0007\u001b[01;32mjolah1@DESKTOP-N4M1LE0\u001b[00m:\u001b[01;34m~/dev/dust-cleaner\u001b[00m$ "]
[150.79263, "r", "170x44"]
[150.79337, "o", "\r\u001b[K\r\u001b]0;jolah1@DESKTOP-N4M1LE0: ~/dev/dust-cleaner\u0007\u001b[01;32mjolah1@DESKTOP-N4M1LE0\u001b[00m:\u001b[01;34m~/dev/dust-cleaner\u001b[00m$ "]
[171.879902, "o", "\u001b[?2004l\r\r\nexit\r\n"]
Loading
Loading