├── .gitignore ├── .prettierignore ├── .prettierrc ├── INSTRUCTIONS.md ├── README.md ├── example_flow_of_funds_process.md ├── main.ts ├── moneroc.out ├── package.json ├── src └── utils │ ├── balance.ts │ ├── calc.ts │ ├── churn.ts │ ├── delay.ts │ ├── random.ts │ ├── rpc.ts │ └── types.ts ├── testing ├── README.md └── index.ts ├── tsconfig.json ├── writeup.md └── xmr_churner.png /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | testign/ 3 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | testing 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": true, 4 | "printWidth": 80 5 | } 6 | -------------------------------------------------------------------------------- /INSTRUCTIONS.md: -------------------------------------------------------------------------------- 1 | # Instructions 2 | 3 | The only way `moneroc` will work is if you have your funds to distribute and churn in Account 0. 4 | 5 | - Make sure you are running your [OWN monero node](https://www.getmonero.org/resources/user-guides/vps_run_node.html) 6 | - Make a new wallet via monero-wallet-rpc 7 | - Transfer (all) the amount of funds you want to distribute and churn into Account 0 (primary/first index) into that new wallet you just created. 8 | - Once you have your monero node daemon set up, connect your wallet to the daemon and get the monero-wallet-rpc URL 9 | - Install [`moneroc`](https://github.com/antichainalysis/xmr-churner/blob/main/README.md#usage) 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | moneroc - XMR Churner 3 |
4 |

5 | 6 |
7 |

The proper way to churn Monero

8 |
9 | 10 | # Description 11 | `moneroc` is a tool designed for automating the process of distributing funds between multiple Monero accounts and **churning** them (i.e., transferring funds between accounts multiple times) in the same wallet using monero-wallet-rpc. It interacts with the Monero RPC server to create new accounts, distribute funds, churn, and shuffle balances. The goal is to enhance the **privacy and transaction obfuscation**, making it more difficult to trace the origin and flow of funds. `moneroc` accomplishes this by leveraging **Monero's ring signature privacy** and simulating the **natural fund movements** with randomized delays between transactions. 12 | 13 | `moneroc` is designed for: 14 | * **Obfuscating the source of funds** by introducing multiple decoy addresses that get mixed into future transactions, utilizing **ring signatures** to obscure transaction origins. 15 | * **Simulating real-world transaction patterns** by adding unpredictable delays and repeating churn operations over several hours, preventing any identifying patterns in fund transfers. 16 | 17 | **NOTE: `moneroc` is not offering or guaranting full complete untraceability of your Monero, simply put it is just a tool designed to help Monero users who participate in churning [to do it properly](https://github.com/antichainalysis/xmr-churner/blob/main/writeup.md) (read up carefully before you use). Monero is a tool just like `moneroc` and tools are meant to be used correctly. There is no silver bullet for privacy, people can slip up and make mistakes—just be smart, cautious, and aware/vigilant.** 18 | 19 | I have tried and will continue to try to make this tool as user-friendly as possible so even the most non-technical/average person is able to use it easily and successfully. 20 | 21 | # Features 22 | * **Dynamic Range Calculation**: `moneroc` automatically calculates the range of atomic units (AMU) to distribute based on the total balance of the account (main account 0). 23 | * **Account Creation & Transfer**: `moneroc` automatically creates new monero accounts and distributes a random amount of XMR within a specific range between them. 24 | * **Churn Automation**: Configurable churn range to randomly perform churns between accounts for a set number of times to enhance privacy. 25 | * **Monitors Account Balance**: `moneroc` ensures that sufficient unlocked funds are available before proceeding with any transactions. 26 | * **Delays**: Random (30-60 minutes delay range) delays between each operation because after each churn, 8-10+ hours pass, and decoy addresses start to appear, making it difficult for anyone monitoring the process to detect patterns, trace, or link activities. 27 | 28 | # Requirements 29 | * Your OWN ([local - will explain why later on why local node is needed](https://github.com/antichainalysis/xmr-churner/blob/main/writeup.md#introduction-to-moneroc)) Monero node. 30 | * monero-wallet-rpc completely setup with the JSON-RPC interface enabled (default is `http://127.0.0.1:18082/json_rpc`) 31 | * bun - https://bun.sh/ for running the tool. 32 | 33 | # Installation 34 | All `moneroc` requires to install is bun.sh, execute the following: 35 | ``` 36 | git clone https://github.com/antichainalysis/xmr-churner.git 37 | cd xmr-churner 38 | curl -fsSL https://bun.sh/install | bash 39 | bun install 40 | ``` 41 | 42 | # Usage 43 | PLEASE READ [THIS](https://github.com/antichainalysis/xmr-churner/blob/main/INSTRUCTIONS.md) BEFORE YOU INSTALL AND RUN `moneroc`. 44 | 45 | To run `moneroc`, execute the following command: 46 | ``` 47 | bun run main.ts --rpc-url --churnRange 48 | ``` 49 | * `--rpc-url` or `-rpc`: The RPC URL of your Monero node (defaults to `http://127.0.0.1:18082/json_rpc`). 50 | * `--churnRange` or `-cr`: A comma-separated range (e.g., 3,7) that specifies how many times to perform the churn operation. The tool will randomly select a number within this range (defaults to `3,7`). 51 | 52 | Example: 53 | ``` 54 | bun run main.ts --rpc-url http://127.0.0.1:18082/json_rpc --churnRange 3,7 55 | ``` 56 | 57 | # [How It Works](https://github.com/antichainalysis/xmr-churner/blob/main/xmr_churner.png) 58 | 59 | Read [this](https://github.com/antichainalysis/xmr-churner/blob/main/example_flow_of_funds_process.md) for a tldr/faster, better, simpler, and clearer understanding of `moneroc`. 60 | 61 | [log](https://github.com/antichainalysis/xmr-churner/blob/main/moneroc.out) 62 | 63 | **1. Check Account Balance** 64 | * `moneroc` first checks the balance and unlocked balance of the wallet. 65 | * If the balance is below the upper limit of `maxRange`, `moneroc` breaks the loop and stops distributing funds. 66 | * If the unlocked balance is insufficient, it waits for the funds to become available by polling the balance every 5 seconds 67 | 68 | **2. Create new Account** 69 | * Once sufficient funds are avaiable, `moneroc` creates a new Monero account and retrieves the account's address and index 70 | 71 | **3. Fund Distribution** 72 | * A random amount of XMR (within the `amuRange` range) is selected and transferred to the newly created account using the Monero `transfer` method via a RPC call. 73 | 74 | **4. Wait for Unlocked Funds** 75 | * If the unlocked balance is not enough, `moneroc` waits until the funds are unlocked. 76 | 77 | **5. Random Delays Between Operations** 78 | * After each transfer, `moneroc` introduces a random delay between 30 and 60 minutes to mimic more natual, human-like behaviour. This prevents `moneroc` from acting in a predictable manner and makes it harder for any observer to track/trace the flow of funds. 79 | 80 | **6. Churning Process** 81 | * After all funds have been distributed across the accounts created in the same wallet, `moneroc` starts the churning process: 82 | * It retrieves a list of all accounts and their balances. 83 | * Accounts with non-zero balances are selected for churning. 84 | * `moneroc` transfers funds from each account to a newly created account, iterating over each account as specific by the `churnRange` 85 | 86 | **7. Churning Iterations** 87 | * `moneroc` performs the churn operation for each selected account a random number of times (`iters`), defined within the `churnRange`. Each churn operation involves transferring all available funds from an account to a new one. 88 | -------------------------------------------------------------------------------- /example_flow_of_funds_process.md: -------------------------------------------------------------------------------- 1 | # Breakdown of the `moneroc` Process 2 | 3 | So, lets just say we have 100 XMR in Account 0 which we will distribute in intervals of 10 XMR: 4 | 5 | 1. **Account Fund Distribution:** 6 | * Account 0 has 100 XMR. 7 | * It will send 10 XMR to each of the newly created accounts (10 new accounts) one by one with a `delayRange` in between each distribution. 8 | 9 | Distribution Process: 10 | ``` 11 | Account 0 - 100 XMR 12 | 13 | Account 0 -> Account 1: 10 XMR 14 | wait 30-60 minutes 15 | Account 0 -> Account 2: 10 XMR 16 | wait 30-60 minutes 17 | Account 0 -> Account 3: 10 XMR 18 | wait 30-60 minutes 19 | Account 0 -> Account 4: 10 XMR 20 | wait 30-60 minutes 21 | Account 0 -> Account 5: 10 XMR 22 | wait 30-60 minutes 23 | Account 0 -> Account 6: 10 XMR 24 | wait 30-60 minutes 25 | Account 0 -> Account 7: 10 XMR 26 | wait 30-60 minutes 27 | Account 0 -> Account 8: 10 XMR 28 | wait 30-60 minutes 29 | Account 0 -> Account 9: 10 XMR 30 | wait 30-60 minutes 31 | Account 0 -> Account 10: 10 XMR 32 | ``` 33 | 34 | 2. **Churning Process**: 35 | * After distribution, accounts with non-zero balances (Account 1 to Account 10 in this context/example) are selected for churning. 36 | * Each account moves its funds to a newly created account (Account 11 to Account 20 in this context/example), with random delays between each transaction. 37 | 38 | **NOTE: This is just 1 churn, there are more churns** 39 | 40 | Churning Process: 41 | ``` 42 | Account 1 -> Account 11 43 | wait 30-60 minutes 44 | Account 2 -> Account 12 45 | wait 30-60 minutes 46 | Account 3 -> Account 13 47 | wait 30-60 minutes 48 | Account 4 -> Account 14 49 | wait 30-60 minutes 50 | Account 5 -> Account 15 51 | wait 30-60 minutes 52 | Account 6 -> Account 16 53 | wait 30-60 minutes 54 | Account 7 -> Account 17 55 | wait 30-60 minutes 56 | Account 8 -> Account 18 57 | wait 30-60 minutes 58 | Account 9 -> Account 19 59 | wait 30-60 minutes 60 | Account 10 -> Account 20 61 | ``` 62 | 63 | # Conceptual Visual Representation 64 | 65 | ```mermaid 66 | classDiagram 67 | class Churning1 { 68 | The main churning process. 69 | +getAccounts() 70 | +churn() 71 | +randomDelay() 72 | } 73 | class Churning2 { 74 | The main churning process. 75 | +getAccounts() 76 | +churn() 77 | +randomDelay() 78 | } 79 | class Churning3 { 80 | The main churning process. 81 | +getAccounts() 82 | +churn() 83 | +randomDelay() 84 | } 85 | class Churning4 { 86 | The main churning process. 87 | +getAccounts() 88 | +churn() 89 | +randomDelay() 90 | } 91 | class Churning5 { 92 | The main churning process. 93 | +getAccounts() 94 | +churn() 95 | +randomDelay() 96 | } 97 | class Churning6 { 98 | The main churning process. 99 | +getAccounts() 100 | +churn() 101 | +randomDelay() 102 | } 103 | class Churning7 { 104 | The main churning process. 105 | +getAccounts() 106 | +churn() 107 | +randomDelay() 108 | } 109 | class Churning8 { 110 | The main churning process. 111 | +getAccounts() 112 | +churn() 113 | +randomDelay() 114 | } 115 | class Churning9 { 116 | The main churning process. 117 | +getAccounts() 118 | +churn() 119 | +randomDelay() 120 | } 121 | class Churning10 { 122 | The main churning process. 123 | +getAccounts() 124 | +churn() 125 | +randomDelay() 126 | } 127 | 128 | class Account0 { 129 | XMR = 100 130 | +getBalance() 131 | +waitForBalance() 132 | +createAccount() 133 | +transfer() 134 | } 135 | 136 | class Account1 { 137 | XMR = 10 138 | } 139 | 140 | class Account2 { 141 | XMR = 10 142 | } 143 | 144 | class Account3 { 145 | XMR = 10 146 | } 147 | 148 | class Account4 { 149 | XMR = 10 150 | } 151 | 152 | class Account5 { 153 | XMR = 10 154 | } 155 | 156 | class Account6 { 157 | XMR = 10 158 | } 159 | 160 | class Account7 { 161 | XMR = 10 162 | } 163 | 164 | class Account8 { 165 | XMR = 10 166 | } 167 | 168 | class Account9 { 169 | XMR = 10 170 | } 171 | 172 | class Account10 { 173 | XMR = 10 174 | } 175 | 176 | class Account11 { 177 | XMR = 10 178 | } 179 | 180 | class Account12 { 181 | XMR = 10 182 | } 183 | 184 | class Account13 { 185 | XMR = 10 186 | } 187 | 188 | class Account14 { 189 | XMR = 10 190 | } 191 | 192 | class Account15 { 193 | XMR = 10 194 | } 195 | 196 | class Account16 { 197 | XMR = 10 198 | } 199 | 200 | class Account17 { 201 | XMR = 10 202 | } 203 | 204 | class Account18 { 205 | XMR = 10 206 | } 207 | 208 | class Account19 { 209 | XMR = 10 210 | } 211 | 212 | class Account20 { 213 | XMR = 10 214 | } 215 | 216 | Account0 --> Account1 : distributes XMR 217 | Account0 --> Account2 : distributes XMR 218 | Account0 --> Account3 : distributes XMR 219 | Account0 --> Account4 : distributes XMR 220 | Account0 --> Account5 : distributes XMR 221 | Account0 --> Account6 : distributes XMR 222 | Account0 --> Account7 : distributes XMR 223 | Account0 --> Account8 : distributes XMR 224 | Account0 --> Account9 : distributes XMR 225 | Account0 --> Account10 : distributes XMR 226 | 227 | Account1 --> Churning1 : undergoes 228 | Account2 --> Churning2 : undergoes 229 | Account3 --> Churning3 : undergoes 230 | Account4 --> Churning4 : undergoes 231 | Account5 --> Churning5 : undergoes 232 | Account6 --> Churning6 : undergoes 233 | Account7 --> Churning7 : undergoes 234 | Account8 --> Churning8 : undergoes 235 | Account9 --> Churning9 : undergoes 236 | Account10 --> Churning10 : undergoes 237 | 238 | Churning1 --> Account11 : transfers to 239 | Churning2 --> Account12 : transfers to 240 | Churning3 --> Account13 : transfers to 241 | Churning4 --> Account14 : transfers to 242 | Churning5 --> Account15 : transfers to 243 | Churning6 --> Account16 : transfers to 244 | Churning7 --> Account17 : transfers to 245 | Churning8 --> Account18 : transfers to 246 | Churning9 --> Account19 : transfers to 247 | Churning 10 --> Account20 : transfers to 248 | ``` 249 | 250 | # Reality 251 | ### When you run moneroc: 252 | ![XMR Churner](https://github.com/antichainalysis/xmr-churner/blob/main/xmr_churner.png?raw=true) 253 | 254 | -------------------------------------------------------------------------------- /main.ts: -------------------------------------------------------------------------------- 1 | import yargs from 'yargs'; 2 | import { hideBin } from 'yargs/helpers'; 3 | 4 | import { getBalance, waitForBalance } from './src/utils/balance'; 5 | import { churn } from './src/utils/churn'; 6 | import { randomDelay } from './src/utils/delay'; 7 | import { createAccount, transfer, getAccounts } from './src/utils/rpc'; 8 | import { calculateAmuRange } from './src/utils/calc'; 9 | import { randomRange } from './src/utils/random'; 10 | 11 | interface Args { 12 | 'rpc-url': string; 13 | churnRange: number[]; 14 | } 15 | 16 | const argv = yargs(hideBin(process.argv)) 17 | .option('rpc-url', { 18 | alias: 'rpc', 19 | type: 'string', 20 | description: 'The RPC URL to connect to', 21 | default: 'http://127.0.0.1:18082/json_rpc', 22 | }) 23 | .option('churnRange', { 24 | alias: 'cr', 25 | type: 'array', 26 | description: 27 | 'Random number of times to perform churn by given range - Churn range, e.g., --churnRange, -cr 3,7', 28 | default: [3, 7], // inclusive, exclusive 29 | coerce: (arg) => (Array.isArray(arg) ? arg : arg.split(',').map(Number)), // check if it's an array or string 30 | }) 31 | .help().argv as Args; 32 | 33 | const rpcUrl = argv['rpc-url']; 34 | const churnRange: [number, number] = 35 | argv.churnRange.length === 2 ? (argv.churnRange as [number, number]) : [3, 7]; 36 | const delayRange: [number, number] = [30 * 60 * 1000, 60 * 60 * 1000]; 37 | 38 | async function main() { 39 | const { balance: totalBalance } = await getBalance(rpcUrl, 0); 40 | 41 | console.log('Total balance in account 0:', totalBalance / 1e12); 42 | 43 | const [minRange, maxRange] = calculateAmuRange(totalBalance / 1e12).map( 44 | (value) => value * 1e12, 45 | ); 46 | 47 | console.log('Calculated range:', [minRange / 1e12, maxRange / 1e12]); 48 | 49 | while (true) { 50 | const { balance, unlockedBalance } = await getBalance(rpcUrl); 51 | 52 | console.log('Balance:', balance / 1e12); 53 | console.log('Unlocked:', unlockedBalance / 1e12); 54 | 55 | if (balance < maxRange) break; 56 | if (unlockedBalance < maxRange) { 57 | console.log('Waiting for funds to unlock...'); 58 | await waitForBalance(rpcUrl, 0, maxRange); 59 | console.log('Funds unlocked'); 60 | } 61 | 62 | const address = await createAccount(rpcUrl); 63 | console.log('Created new account with address:', address); 64 | 65 | const amu = Math.floor(Math.random() * (maxRange - minRange) + minRange); 66 | console.log('Sending', amu / 1e12, 'XMR to', address); 67 | 68 | await transfer(rpcUrl, amu, address); 69 | console.log('Transaction sent to RPC'); 70 | } 71 | 72 | console.log('Finished distributing (out of funds)'); 73 | 74 | await randomDelay(delayRange); 75 | 76 | console.log('Starting churn'); 77 | 78 | const accounts = await getAccounts(rpcUrl); 79 | 80 | const accountsToChurn = accounts 81 | .filter((acc) => acc.balance) 82 | .map((acc) => ({ 83 | ...acc, 84 | iters: randomRange(churnRange), 85 | })); 86 | 87 | for (let i = 0; i < churnRange[1]; i++) { 88 | for (const account of accountsToChurn) { 89 | if (i >= account.iters) continue; 90 | 91 | console.log( 92 | 'Doing churn %d/%d for account index %d', 93 | i + 1, 94 | account.iters, 95 | account.account_index, 96 | ); 97 | 98 | const oldIndex = account.account_index; 99 | 100 | account.account_index = await churn(rpcUrl, oldIndex); 101 | await randomDelay(delayRange); 102 | } 103 | } 104 | 105 | console.log('Finished all churns'); 106 | } 107 | 108 | main(); 109 | -------------------------------------------------------------------------------- /moneroc.out: -------------------------------------------------------------------------------- 1 | root@debian:~/xmr-churner# bun run main.ts --rpc-url http://127.0.0.1:18083/json_rpc --churnRange 3,5 2 | Total balance in account 0: 596.0580623298 3 | Calculated atomic units range: [ 34, 102 ] 4 | Balance: 596.0580623298 5 | Unlocked: 596.0580623298 6 | Created new account with address: 88tdp...JgM6 7 | Sending 90.510942478119 XMR to 88tdp...JgM6 8 | Transaction sent to RPC 9 | Balance: 505.547075331681 10 | Unlocked: 0 11 | Waiting for funds to unlock... 12 | Funds unlocked 13 | Created new account with address: 85wk...omWrf 14 | Sending 57.604040346455 XMR to 85wk...omWrf 15 | Transaction sent to RPC 16 | Balance: 447.943004365226 17 | Unlocked: 0 18 | Waiting for funds to unlock... 19 | Funds unlocked 20 | Created new account with address: 8A4N...ZhTW 21 | Sending 87.148194397831 XMR to 8A4N...ZhTW 22 | Transaction sent to RPC 23 | Balance: 360.794779267395 24 | Unlocked: 0 25 | Waiting for funds to unlock... 26 | Funds unlocked 27 | Created new account with address: 87x3...DZu2 28 | Sending 40.588276871156 XMR to 87x3...DZu2 29 | Transaction sent to RPC 30 | Balance: 320.206471656239 31 | Unlocked: 0 32 | Waiting for funds to unlock... 33 | Funds unlocked 34 | Created new account with address: 8A7E...VADy 35 | Sending 92.699746340546 XMR to 8A7E...VADy 36 | Transaction sent to RPC 37 | Balance: 227.506694635693 38 | Unlocked: 0 39 | Waiting for funds to unlock... 40 | Funds unlocked 41 | Created new account with address: 85Rr...fiPu 42 | Sending 37.928563212171 XMR to 85Rr...fiPu 43 | Transaction sent to RPC 44 | Balance: 189.578100723522 45 | Unlocked: 0 46 | Waiting for funds to unlock... 47 | Funds unlocked 48 | Created new account with address: 82rD...xUtp 49 | Sending 80.702962105713 XMR to 82rD...xUtp 50 | Transaction sent to RPC 51 | Balance: 108.875016297809 52 | Unlocked: 0 53 | Waiting for funds to unlock... 54 | Funds unlocked 55 | Created new account with address: 8BEM...WR7v 56 | Sending 92.866165910428 XMR to 8BEM...WR7v 57 | Transaction sent to RPC 58 | Balance: 16.008819587381 59 | Unlocked: 0 60 | Finished distributing (out of funds) 61 | waiting 40 minutes 62 | Starting churn 63 | Doing churn 1/6 for account index 0 64 | Swept 0 -> 11 65 | waiting 48 minutes 66 | Doing churn 1/4 for account index 1 67 | Swept 1 -> 12 68 | waiting 58 minutes 69 | Doing churn 1/4 for account index 3 70 | Swept 3 -> 13 71 | waiting 47 minutes 72 | Doing churn 1/4 for account index 4 73 | Swept 4 -> 14 74 | waiting 31 minutes 75 | Doing churn 1/6 for account index 5 76 | Swept 5 -> 15 77 | waiting 34 minutes 78 | Doing churn 1/6 for account index 6 79 | Swept 6 -> 16 80 | waiting 43 minutes 81 | Doing churn 1/6 for account index 7 82 | Swept 7 -> 17 83 | waiting 53 minutes 84 | Doing churn 1/6 for account index 8 85 | Swept 8 -> 18 86 | waiting 52 minutes 87 | Doing churn 1/6 for account index 9 88 | Swept 9 -> 19 89 | waiting 42 minutes 90 | Doing churn 1/3 for account index 10 91 | Swept 10 -> 20 92 | waiting 33 minutes 93 | Doing churn 2/6 for account index 11 94 | Swept 11 -> 21 95 | waiting 44 minutes 96 | Doing churn 2/4 for account index 12 97 | Swept 12 -> 22 98 | waiting 48 minutes 99 | Doing churn 2/4 for account index 13 100 | Swept 13 -> 23 101 | waiting 36 minutes 102 | Doing churn 2/4 for account index 14 103 | Swept 14 -> 24 104 | waiting 38 minutes 105 | Doing churn 2/6 for account index 15 106 | Swept 15 -> 25 107 | waiting 42 minutes 108 | Doing churn 2/6 for account index 16 109 | Swept 16 -> 26 110 | waiting 59 minutes 111 | Doing churn 2/6 for account index 17 112 | Swept 17 -> 27 113 | waiting 50 minutes 114 | Doing churn 2/6 for account index 18 115 | Swept 18 -> 28 116 | waiting 37 minutes 117 | Doing churn 2/6 for account index 19 118 | Swept 19 -> 29 119 | waiting 40 minutes 120 | Doing churn 2/3 for account index 20 121 | Swept 20 -> 30 122 | waiting 37 minutes 123 | Doing churn 3/6 for account index 21 124 | Swept 21 -> 31 125 | waiting 53 minutes 126 | Doing churn 3/4 for account index 22 127 | Swept 22 -> 32 128 | waiting 52 minutes 129 | Doing churn 3/4 for account index 23 130 | Swept 23 -> 33 131 | waiting 55 minutes 132 | Doing churn 3/4 for account index 24 133 | Swept 24 -> 34 134 | waiting 44 minutes 135 | Doing churn 3/6 for account index 25 136 | Swept 25 -> 35 137 | waiting 42 minutes 138 | Doing churn 3/6 for account index 26 139 | Swept 26 -> 36 140 | waiting 58 minutes 141 | Doing churn 3/6 for account index 27 142 | Swept 27 -> 37 143 | waiting 59 minutes 144 | Doing churn 3/6 for account index 28 145 | Swept 28 -> 38 146 | waiting 32 minutes 147 | Doing churn 3/6 for account index 29 148 | Swept 29 -> 39 149 | waiting 53 minutes 150 | Doing churn 3/3 for account index 30 151 | Swept 30 -> 40 152 | waiting 39 minutes 153 | Doing churn 4/6 for account index 31 154 | Swept 31 -> 41 155 | waiting 51 minutes 156 | Doing churn 4/4 for account index 32 157 | Swept 32 -> 42 158 | waiting 58 minutes 159 | Doing churn 4/4 for account index 33 160 | Swept 33 -> 43 161 | waiting 44 minutes 162 | Doing churn 4/4 for account index 34 163 | Swept 34 -> 44 164 | waiting 32 minutes 165 | Doing churn 4/6 for account index 35 166 | Swept 35 -> 45 167 | waiting 54 minutes 168 | Doing churn 4/6 for account index 36 169 | Swept 36 -> 46 170 | waiting 58 minutes 171 | Doing churn 4/6 for account index 37 172 | Swept 37 -> 47 173 | waiting 32 minutes 174 | Doing churn 4/6 for account index 38 175 | Swept 38 -> 48 176 | waiting 51 minutes 177 | Doing churn 4/6 for account index 39 178 | Swept 39 -> 49 179 | waiting 39 minutes 180 | Doing churn 5/6 for account index 41 181 | Swept 41 -> 50 182 | waiting 41 minutes 183 | Doing churn 5/6 for account index 45 184 | Swept 45 -> 51 185 | waiting 49 minutes 186 | Doing churn 5/6 for account index 46 187 | Swept 46 -> 52 188 | waiting 31 minutes 189 | Doing churn 5/6 for account index 47 190 | Swept 47 -> 53 191 | waiting 45 minutes 192 | Doing churn 5/6 for account index 48 193 | Swept 48 -> 54 194 | waiting 35 minutes 195 | Doing churn 5/6 for account index 49 196 | Swept 49 -> 55 197 | waiting 43 minutes 198 | Doing churn 6/6 for account index 50 199 | Swept 50 -> 56 200 | waiting 38 minutes 201 | Doing churn 6/6 for account index 51 202 | Swept 51 -> 57 203 | waiting 32 minutes 204 | Doing churn 6/6 for account index 52 205 | Swept 52 -> 58 206 | waiting 38 minutes 207 | Doing churn 6/6 for account index 53 208 | Swept 53 -> 59 209 | waiting 59 minutes 210 | Doing churn 6/6 for account index 54 211 | Swept 54 -> 60 212 | waiting 42 minutes 213 | Doing churn 6/6 for account index 55 214 | Swept 55 -> 61 215 | waiting 55 minutes 216 | Finished all churns 217 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "xmr-churner", 3 | "version": "1.0.0", 4 | "description": "moneroc - The proper way to churn Monero", 5 | "module": "index.ts", 6 | "type": "module", 7 | "keywords": [ 8 | "xmr-churner", 9 | "xmrchurner", 10 | "churnxmr", 11 | "xmrmixer", 12 | "churn", 13 | "monero", 14 | "xmr", 15 | "mixer", 16 | "xmr mixer", 17 | "mix xmr", 18 | "churn xmr" 19 | ], 20 | "author": "antichainalysis", 21 | "devDependencies": { 22 | "@types/bun": "latest", 23 | "@types/yargs": "^17.0.33" 24 | }, 25 | "peerDependencies": { 26 | "typescript": "^5.0.0" 27 | }, 28 | "dependencies": { 29 | "yargs": "^17.7.2", 30 | "yargs-parser": "^21.1.1" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/utils/balance.ts: -------------------------------------------------------------------------------- 1 | import type { BalanceResponse } from './types'; 2 | /** 3 | * Return the wallet's balance. 4 | * @param {string} rpcUrl - The RPC URL to connect to. 5 | * @param {number} [accountIndex=0] - The account index to retrieve the balance for (defaults to 0) 6 | * @returns {Promise<{balance: number, unlockedBalance: number}>} Wallet's balance and unlocked balance. 7 | */ 8 | export async function getBalance( 9 | rpcUrl: string, 10 | accountIndex: number = 0, 11 | ): Promise { 12 | const res = await fetch(rpcUrl, { 13 | headers: { 14 | 'Content-Type': 'application/json', 15 | }, 16 | method: 'POST', 17 | body: JSON.stringify({ 18 | jsonrpc: '2.0', 19 | id: '0', 20 | method: 'get_balance', 21 | params: { 22 | account_index: accountIndex, 23 | }, 24 | }), 25 | }); 26 | const json = (await res.json()) as any; 27 | return { 28 | balance: json.result.balance as number, 29 | unlockedBalance: json.result.unlocked_balance as number, 30 | }; 31 | } 32 | 33 | /** 34 | * @param rpcUrl - The RPC URL to connect to. 35 | * @param accountIndex - The account index to check the balance for (defaults to 0) 36 | * @param maxAmu - The target amount of unlocked balance to wait for. 37 | * @returns {Promise} 38 | */ 39 | export async function waitForBalance( 40 | rpcUrl: string, 41 | accountIndex: number = 0, 42 | maxAmu: number, 43 | ): Promise { 44 | return new Promise((res) => { 45 | const interval = setInterval(async () => { 46 | const { unlockedBalance } = await getBalance(rpcUrl, accountIndex); 47 | 48 | if (unlockedBalance >= maxAmu) { 49 | clearInterval(interval); 50 | res(0); 51 | } 52 | }, 5000); 53 | }); 54 | } 55 | -------------------------------------------------------------------------------- /src/utils/calc.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Calculate a random range for the total balance 3 | * @param {number} totalBalance - The total balance. 4 | * @returns {[number, number]} - Returns array with minimum and maximum range. 5 | */ 6 | export function calculateAmuRange(totalBalance: number): [number, number] { 7 | let minRange: number; 8 | let maxRange: number; 9 | 10 | if (totalBalance < 100) { 11 | minRange = Math.floor(totalBalance * 0.1); // 10% of total as min 12 | maxRange = Math.floor(totalBalance * 0.3); // 30% of total as max 13 | } else if (totalBalance < 1000) { 14 | const percentage = Math.random() * (0.3 - 0.1) + 0.1; // Random 10%-30% range 15 | const rangeSize = totalBalance * percentage; 16 | 17 | minRange = Math.floor(rangeSize * 0.5); // 50% of range size as min 18 | maxRange = Math.floor(rangeSize * 1.5); // 150% of range size as max 19 | 20 | maxRange = Math.min(maxRange, 160); // cap to 160 max 21 | } else { 22 | const percentage = Math.random() * (0.15 - 0.05) + 0.05; // Random 5%-15% range 23 | const rangeSize = totalBalance * percentage; 24 | 25 | minRange = Math.floor(rangeSize * 0.6); // 60% of range size as min 26 | maxRange = Math.floor(rangeSize * 1.2); // 120% of range size as max 27 | 28 | // 4 figs 29 | if (totalBalance >= 1000 && totalBalance < 10000) { 30 | minRange = Math.floor(Math.random() * (150 - 100) + 150); // Random min between 150 and 200 31 | maxRange = Math.floor(Math.random() * (250 - 150) + 150); // Random max between 150 and 250 32 | } 33 | 34 | // 5 figs 35 | if (totalBalance >= 10000 && totalBalance < 100000) { 36 | minRange = Math.floor(Math.random() * (300 - 200) + 200); // Random min between 200 and 250 37 | maxRange = Math.floor(Math.random() * (500 - 300) + 300); // Random max between 300 and 500 38 | } 39 | } 40 | 41 | minRange = Math.max(1, minRange); 42 | maxRange = Math.max(minRange + 1, maxRange); 43 | 44 | return [minRange, maxRange]; 45 | } 46 | -------------------------------------------------------------------------------- /src/utils/churn.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Create a new account and send the unlocked balance to the account. 3 | * @param {string} rpcUrl - The RPC URL to connect to. 4 | * @param {number} accountIndex - The account index to sweep the balance to. 5 | * @returns {Promise} newAccount - The account the funds have been sweeped to. 6 | */ 7 | export async function churn( 8 | rpcUrl: string, 9 | accountIndex: number, 10 | ): Promise { 11 | const res = await fetch(rpcUrl, { 12 | headers: { 13 | 'Content-Type': 'application/json', 14 | }, 15 | method: 'POST', 16 | body: JSON.stringify({ 17 | jsonrpc: '2.0', 18 | id: '0', 19 | method: 'create_account', 20 | params: {}, 21 | }), 22 | }); 23 | const json = (await res.json()) as any; 24 | const address = json.result.address as string; 25 | const newAccount = json.result.account_index as number; 26 | 27 | await fetch(rpcUrl, { 28 | headers: { 29 | 'Content-Type': 'application/json', 30 | }, 31 | method: 'POST', 32 | body: JSON.stringify({ 33 | jsonrpc: '2.0', 34 | id: '0', 35 | method: 'sweep_all', 36 | params: { 37 | address, 38 | account_index: accountIndex, 39 | }, 40 | }), 41 | }); 42 | 43 | console.log('Swept', accountIndex, '->', newAccount); 44 | return newAccount; 45 | } 46 | -------------------------------------------------------------------------------- /src/utils/delay.ts: -------------------------------------------------------------------------------- 1 | import { randomRange } from './random'; 2 | 3 | /** 4 | * Random delay within the given range. 5 | * @param {number[]} delayRange - Minimum and maximum delay in milliseconds 6 | */ 7 | export function randomDelay(delayRange: [number, number]) { 8 | return new Promise((r) => { 9 | const ms = randomRange(delayRange); 10 | 11 | console.log('waiting', Math.floor(ms / (60 * 1000)), 'minutes'); 12 | 13 | setTimeout(r, ms); 14 | }); 15 | } 16 | -------------------------------------------------------------------------------- /src/utils/random.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Generate a random number with range 3 | * @param {number[]} range - The start and end of the range. 4 | * @returns {number} Random number within specified range. 5 | */ 6 | export function randomRange(range: [number, number]): number { 7 | return Math.floor(Math.random() * (range[1] - range[0]) + range[0]); 8 | } 9 | -------------------------------------------------------------------------------- /src/utils/rpc.ts: -------------------------------------------------------------------------------- 1 | import type { SubaddressAccount } from './types'; 2 | 3 | /** 4 | * Creates a new account and returns the account's address 5 | * @param {string} rpcUrl - The RPC URL to connect to. 6 | * @returns {Promise} Account address 7 | */ 8 | export async function createAccount(rpcUrl: string): Promise { 9 | const res = await fetch(rpcUrl, { 10 | headers: { 11 | 'Content-Type': 'application/json', 12 | }, 13 | method: 'POST', 14 | body: JSON.stringify({ 15 | jsonrpc: '2.0', 16 | id: '0', 17 | method: 'create_account', 18 | params: {}, 19 | }), 20 | }); 21 | const json = (await res.json()) as any; 22 | 23 | return json.result.address as string; 24 | } 25 | 26 | /** 27 | * Transfers a specified amount of funds to a given address. 28 | * @param {string} rpcUrl - The RPC URL to connect to. 29 | * @param {number} amu - The amount of transfer. 30 | * @param {address} address - The address to transfer the funds to. 31 | * @returns {Promise} Response of the transfer request. 32 | */ 33 | export async function transfer( 34 | rpcUrl: string, 35 | amu: number, 36 | address: string, 37 | ): Promise { 38 | return fetch(rpcUrl, { 39 | headers: { 40 | 'Content-Type': 'application/json', 41 | }, 42 | method: 'POST', 43 | body: JSON.stringify({ 44 | jsonrpc: '2.0', 45 | id: '0', 46 | method: 'transfer', 47 | params: { 48 | destinations: [ 49 | { 50 | amount: amu, 51 | address, 52 | }, 53 | ], 54 | }, 55 | }), 56 | }); 57 | } 58 | 59 | /** 60 | * Get all accounts for a wallet. 61 | * @param {string} rpcUrl - The RPC URL to connect to. 62 | * @returns {Promise} - Array of subaddress accounts. 63 | */ 64 | export async function getAccounts( 65 | rpcUrl: string, 66 | ): Promise { 67 | const res = await fetch(rpcUrl, { 68 | headers: { 69 | 'Content-Type': 'application/json', 70 | }, 71 | method: 'POST', 72 | body: JSON.stringify({ 73 | jsonrpc: '2.0', 74 | id: '0', 75 | method: 'get_accounts', 76 | params: {}, 77 | }), 78 | }); 79 | const json = (await res.json()) as any; 80 | 81 | return json.result.subaddress_accounts as SubaddressAccount[]; 82 | } 83 | -------------------------------------------------------------------------------- /src/utils/types.ts: -------------------------------------------------------------------------------- 1 | type BalanceResponse = { 2 | balance: number; 3 | unlockedBalance: number; 4 | }; 5 | 6 | // Each subaddress account contains: 7 | // - an index 8 | // - balance 9 | // - base address 10 | // - unlocked balance 11 | type SubaddressAccount = { 12 | account_index: number; 13 | balance: number; 14 | base_address: string; 15 | unlocked_balance: number; 16 | }; 17 | 18 | export type { BalanceResponse, SubaddressAccount }; 19 | -------------------------------------------------------------------------------- /testing/README.md: -------------------------------------------------------------------------------- 1 | # **DO NOT USE** 2 | -------------------------------------------------------------------------------- /testing/index.ts: -------------------------------------------------------------------------------- 1 | const rpcUrl = "http://127.0.0.1:18082/json_rpc"; 2 | 3 | const amuRange: [number, number] = [1e12 * 160, 1e12 * 200]; 4 | const churnRange: [number, number] = [3, 7]; // inclusive, exclusive 5 | const delayRange: [number, number] = [30 * 60 * 1000, 60 * 60 * 1000]; 6 | 7 | function randomRange(range: [number, number]) { 8 | return Math.floor(Math.random() * (range[1] - range[0]) + range[0]); 9 | } 10 | 11 | async function getBalance(accountIndex = 0) { 12 | const res = await fetch(rpcUrl, { 13 | headers: { 14 | "Content-Type": "application/json", 15 | }, 16 | method: "POST", 17 | body: JSON.stringify({ 18 | jsonrpc: "2.0", 19 | id: "0", 20 | method: "get_balance", 21 | params: { 22 | account_index: accountIndex, 23 | }, 24 | }), 25 | }); 26 | const json = (await res.json()) as any; 27 | return { 28 | balance: json.result.balance as number, 29 | unlockedBalance: json.result.unlocked_balance as number, 30 | }; 31 | } 32 | 33 | const waitForBalance = (accountIndex = 0) => 34 | new Promise((res) => { 35 | const interval = setInterval(async () => { 36 | const { unlockedBalance } = await getBalance(accountIndex); 37 | if (unlockedBalance >= amuRange[1]) { 38 | clearInterval(interval); 39 | res(0); 40 | } 41 | }, 5000); 42 | }); 43 | 44 | async function churn(accountIndex: number) { 45 | const res = await fetch(rpcUrl, { 46 | headers: { 47 | "Content-Type": "application/json", 48 | }, 49 | method: "POST", 50 | body: JSON.stringify({ 51 | jsonrpc: "2.0", 52 | id: "0", 53 | method: "create_account", 54 | params: {}, 55 | }), 56 | }); 57 | const json = (await res.json()) as any; 58 | const address = json.result.address as string; 59 | const newAccount = json.result.account_index as number; 60 | 61 | await fetch(rpcUrl, { 62 | headers: { 63 | "Content-Type": "application/json", 64 | }, 65 | method: "POST", 66 | body: JSON.stringify({ 67 | jsonrpc: "2.0", 68 | id: "0", 69 | method: "sweep_all", 70 | params: { 71 | address, 72 | account_index: accountIndex, 73 | }, 74 | }), 75 | }); 76 | 77 | console.log("Swept", accountIndex, "->", newAccount); 78 | 79 | return newAccount; 80 | } 81 | 82 | const randomDelay = () => 83 | new Promise((r) => { 84 | const ms = randomRange(delayRange); 85 | console.log("Waiting", Math.floor(ms / (60 * 1000)), "minutes"); 86 | setTimeout(r, ms); 87 | }); 88 | 89 | while (true) { 90 | const { balance, unlockedBalance } = await getBalance(); 91 | console.log("Balance:", balance); 92 | console.log("Unlocked:", unlockedBalance); 93 | if (balance < amuRange[1]) { 94 | break; 95 | } 96 | if (unlockedBalance < amuRange[1]) { 97 | console.log("Waiting for funds to unlock..."); 98 | await waitForBalance(); 99 | console.log("Funds unlocked"); 100 | } 101 | 102 | const res = await fetch(rpcUrl, { 103 | headers: { 104 | "Content-Type": "application/json", 105 | }, 106 | method: "POST", 107 | body: JSON.stringify({ 108 | jsonrpc: "2.0", 109 | id: "0", 110 | method: "create_account", 111 | params: {}, 112 | }), 113 | }); 114 | const json = (await res.json()) as any; 115 | const address = json.result.address as string; 116 | 117 | console.log("Created new account with address", address); 118 | 119 | const amu = randomRange(amuRange); 120 | 121 | console.log("Sending", amu / 1e12, "XMR to", address); 122 | 123 | await fetch(rpcUrl, { 124 | headers: { 125 | "Content-Type": "application/json", 126 | }, 127 | method: "POST", 128 | body: JSON.stringify({ 129 | jsonrpc: "2.0", 130 | id: "0", 131 | method: "transfer", 132 | params: { 133 | destinations: [ 134 | { 135 | amount: amu, 136 | address, 137 | }, 138 | ], 139 | }, 140 | }), 141 | }); 142 | 143 | console.log("Transaction sent to RPC"); 144 | } 145 | 146 | console.log("Finished distributing (out of funds)"); 147 | 148 | await randomDelay(); 149 | 150 | console.log("Starting churn"); 151 | 152 | const res = await fetch(rpcUrl, { 153 | headers: { 154 | "Content-Type": "application/json", 155 | }, 156 | method: "POST", 157 | body: JSON.stringify({ 158 | jsonrpc: "2.0", 159 | id: "0", 160 | method: "get_accounts", 161 | params: {}, 162 | }), 163 | }); 164 | const json = (await res.json()) as any; 165 | const accounts = json.result.subaddress_accounts as { 166 | account_index: number; 167 | balance: number; 168 | base_address: string; 169 | unlocked_balance: number; 170 | }[]; 171 | 172 | const accountsToChurn = accounts 173 | .filter((acc) => acc.balance) 174 | .map((acc) => ({ 175 | ...acc, 176 | iters: randomRange(churnRange), 177 | })); 178 | 179 | for (let i = 0; i < churnRange[1]; i++) { 180 | for (const account of accountsToChurn) { 181 | if (i >= account.iters) continue; 182 | console.log( 183 | "Doing churn %d/%d for account index %d", 184 | i + 1, 185 | account.iters, 186 | account.account_index 187 | ); 188 | const oldIndex = account.account_index; 189 | account.account_index = await churn(oldIndex); 190 | await randomDelay(); 191 | } 192 | } 193 | 194 | console.log("Finished all churns"); 195 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | // Enable latest features 4 | "lib": ["ESNext"], 5 | "target": "ESNext", 6 | "module": "ESNext", 7 | "moduleDetection": "force", 8 | "jsx": "react-jsx", 9 | "allowJs": true, 10 | 11 | // Bundler mode 12 | "moduleResolution": "bundler", 13 | "allowImportingTsExtensions": true, 14 | "verbatimModuleSyntax": true, 15 | "noEmit": true, 16 | 17 | // Best practices 18 | "strict": true, 19 | "skipLibCheck": true, 20 | "noFallthroughCasesInSwitch": true, 21 | 22 | // Some stricter flags (disabled by default) 23 | "noUnusedLocals": false, 24 | "noUnusedParameters": false, 25 | "noPropertyAccessFromIndexSignature": false 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /writeup.md: -------------------------------------------------------------------------------- 1 | # Monero (XMR) Churning 2 | 3 | ## What is Churning in the context of Monero? 4 | 5 | XMR Churning is essentially the process of moving Monero (XMR) between different addresses or wallets with the goal of enhancing privacy. The idea is that by moving the coins around, you can obfuscate the transaction history, making it harder for anyone to trace the origin or destination of funds. 6 | 7 | Churning is typically used to increase **transaction obfuscation** by breaking links between addresses and making it harder for external observers to trace the flow of funds. It's more about disrupting the transaction trail and creating noise. However, **how** you churn is CRUCIAL in determining whether it improves or harms your privacy. 8 | 9 | We all know XMR churning is very controversial topic, some say churning benefits your privacy and others say it significantly harms your privacy even more. Hence why I wrote `moneroc` tool and this writeup to help explain why XMR churning is beneficial in TODAYS time if done correctly. 10 | 11 | # Why is Churning needed? 12 | 13 | Monero churning is really only needed especially when your funds originate from an exchange/identifiable source/traceable source that WILL report to **Chainalysis** and/or the **IRS**. So, I agree with people who say normal Monero users should not participate in Monero churning. 14 | 15 | When you receive Monero from an exchange or other identifiable sources, **the origin of funds can often be traced back** to the exchange. One of the very common methods used to trace back Monero funds is the **EAE (Eve-Alice-Eve) attack**. 16 | 17 | In the **EAE attack**, the first "Eve" is (Chainalysis/IRS) getting the outgoing TXID(s) from whatever exchange. Then, identify "Alice's" consolidation transactions. The second "Eve" trick is when you bring that Monero from the exchange (the first "Eve") to the same or **ANOTHER exchange**. That's when an external observer (Chainalysis/IRS) gets to see the Monero deposit addresses (yes it's still impossible to identify Monero's stealth deposit addresses using blockchain data but if you are sending Monero that comes directly from an exchange prior, external observers are able to see this still because of their authority...). 18 | 19 | The core issue arises from the fact that **exchanges are a weak point** in Monero's privacy. It's easier for external observers to link transactions originating from exchanges. For a deeper dive into the very technical details of this, see this [article on tracing Monero transactions](https://medium.com/@nbax/tracing-the-wannacry-2-0-monero-transactions-d8c1e5129dc1). 20 | 21 | Even Monero developers acknowledge this in their article on **FCMP++** ([Monero's Official Article](https://www.getmonero.org/2024/04/27/fcmps.html)): 22 | 23 | ``` 24 | Full-Chain Membership Proofs, as a concept, is a replacement for rings within the Monero protocol. While rings have offered sender privacy to Monero since it launched, they're vulnerable to attacks such as the EAE attack, have difficulties upon chain reorganizations, and in general enable statistical analysis (mitigated by distribution of the decoy selection algorithm). 25 | ``` 26 | 27 | One highly notable example is when **Julius "zeekill" Kivimäki**, who was identified because he [inadvertently fell victim to the EAE attack](https://cointelegraph.com/news/finnish-authorities-traced-monero-vastaamo-hack). You'd be surprised by how many people inadvertently fall victim to this attack, even if they churn their Monero after receiving it from an exchange and that is because they INCORRECTLY churn it. 28 | 29 | However, after "Alice" if the person was to churn their Monero after receiving it from the exchange (CORRECTLY) then there would be no chance in being identified. Additionally, waiting a minimum of **12 hours**, and ideally **one to two days,** before exchanging your Monero adds significant privacy benefits (because of **Monero's Ring Signature technology**). 30 | 31 | ### sweep_all: 32 | To my understanding most people churn via the `sweep_all` method which links outputs (i.e., undermines your privacy). The concern with this method is that if you sweep coins from multiple addresses (often containing unspent outputs), it links all those outputs together into one transaction. 33 | 34 | The main issue with `sweep_all` is more about **consolidation**. For example, by consolidating funds from multiple addresses into one, it risks revealing relationships between addresses that were previously independent. 35 | 36 | While it does consolidate the funds, it creates a direct link between multiple addresses, which could make the transaction history easier to trace if someone is able to track the previous addresses. This linking of outputs could potentially reduce privacy, especially if you use the same `sweep_all` methods repeatedly. 37 | 38 | ### self-sending: 39 | 40 | Self-sending is when you send coins back to yourself within the same wallet **account** (e.g., using the same receiving address or generating a new address under the same account). 41 | 42 | Although Monero uses **stealth addresses** to ensure that transactions within the same wallet are unlinkable to external observers, this process **doesn't break any inherent link** between the source and destination addresses **for the wallet owner**. This means that while the transaction appears private to anyone looking at the blockchain, **the relationship between the internal address remains intact** for the wallet holder. 43 | 44 | For external observers, the transaction appears as private and unlinkable as any other Monero transaction. However, for the wallet owner, the relationship between the source and destination remains fully traceable. Additionally, if the funds originate from a traceable source (e.g., an exchange), a self-send does not disrupt the chain of custody or add meaningful privacy, as the exchange TXID can still be linked to the wallet. 45 | 46 | # Introduction to `moneroc` 47 | 48 | `moneroc` is a tool designed to facilitate **proper Monero churning** by utilizing effective strategies that enhance privacy without the pitfalls of methods like `sweep_all` or self-sending. This tool automates the process of distributing funds by creating new accounts and churning XMR between them into other new accounts by carefully selecting random delays to avoid obvious patterns and mimicing a more human-like transaction on the network. 49 | 50 | Click [here](https://github.com/antichainalysis/xmr-churner/blob/main/README.md#how-it-works) to read up on how it works. 51 | 52 | There are still a few key things I'd like to mention/explain: 53 | - The minimum amount of churns recommended is 3 54 | - Minimum (range) amount of time to wait between each operation is at least 30 minutes. Here's why: 55 | * **Avoiding Predictability:** Rapid, automated transactions can easily be identified by observers/adversaries/3rd parties, leading to patterns that could compromise/undermine your privacy. 56 | * It enhances **Monero's Ring Signature Privacy**. Once a full churn loop is completed, with 8 to 10+ hours having passed, the **ring signature** mechanism in Monero becomes much more effective. Monero uses **ring signatures** to mix your transaction with others, making it difficult to trace the source or destination of funds. When funds are moved across multiple acounts over time, the **number of decoy addresses** in the ring increases, which strengthens the privacy of the transaction. 57 | * The **ring size**—such as **Ring 16**—means that there are 16 possible sources for a transaction, including your own address and 15 decoy addresses. As more time passes and more churn occurs, the total number of decoy addresses involved in each transaction increases, making it significantly harder to identify the real sender or receiver. 58 | - It is HIGHLY recommended that you run your OWN monero node and on Tor (read below on why this is needed) 59 | * running monerod with tor: 60 | ``` 61 | monerod --proxy tor,127.0.0.1:9050 --anonymous-inbound tor,127.0.0.1:9050 62 | ``` 63 | 64 | ## Why run Monero daemon on Tor? 65 | Every time `moneroc` broadcasts a transaction (churns), it connects to a Monero **peer-to-peer** network node. If you run `moneroc` without using Tor, all of your transactions will be associated with the same public IP address, which could potentially link them together and undermine your privacy. 66 | 67 | Chainalysis, in a presentation to the IRS a few months ago (August-September 2024), explained how they run a lot of their **OWN poisoned (honeypot) nodes** in attempt to trace Monero transactions (lol). Their nodes are set up in a way to trace and link Monero transactions in an effort to de-anonymize us users... so later on in this writing I will refer to them as __CA nodes__. 68 | 69 | The concern is that if all of your churn transactions come from a single IP address, it could look like they are coming from an exchange or centralized platform. This could be an indicator that you're churning XMR with non-private intentions. 70 | 71 | However, not all nodes are CA so it wouldn't be every single TX (churn). Each chunk gets churned a random number of times and then each output split in size so there's a lot of opportunities for `moneroc` to have connected to CA node(s) and non CA node(s). 72 | 73 | # [Chainalysis Monero Presentation to IRS -- August 2023](https://vimeo.com/1009284260) 74 | 75 | 00:00 - 10:00: 76 | 77 | - Monero had its origins in a whitepaper called cryptonote. Which sought to seek privacy and decentralization. 78 | - Cryptonote was first in a cryptocurrency called "Bytecoin", which was acknowledged as the first privacy coin. Which later on, Monero spawned out of the Bytecoin community because of controversy and it's anonymous development team. 79 | - There are also different privacy coins, such as Zcash and Dash. But the main difference between Monero, and Zcash/Dash is that Monero has privacy on by default. With Zcash it's an opt-in feature. 80 | - Bitcoin is pseudonymous, not a true "privacy coin" since the transactions are public. 81 | - Japan and UAE have outlawed privacy coins, there were also leaked EU draft regulations trying to ban privacy coins. Which was virtually impossible due to how decentralized the network is. 82 | - Ring Confidential Transactions also known as Ring CTs, is one of the features with the Monero protocol. When you send a Monero transaction, instead of making it obvious the funds are coming from your identity, the Monero protocol automatically will draw in other transaction outputs from the Monero blockchain, and it will hide the real output being spent among those decoys. 83 | - The mandatory Ring size used to be 11, meaning 1 real output being spent hidden among 11 decoy outputs, currently the Ring size is 16. (It may increase to 100 in the future. He also says how it will make his job alot harder, since a big part of his job is learning how to remove those decoy outputs). 84 | - Pederson commitment is a cryptographic technique that obscures transaction amounts while ensuring they still balance out. (Allow people to spend their funds without revealing the network of how much their spending. 85 | - Similarly with Bitcoin, it uses the same unspent transaction model, meaning the transactions input consists of previous outputs from the previous outputs on the blockchain. That means, the network will need to stop the user from spending the same coin twice. How is this possible if everything is obscure? The answer is zero knowledge proofs, which is where you want to convince someone that you know some secret information without actually revealing what the secret information is. In Monero, a prover is the initiator of a transaction and they're convincing the verifier (the miner), that they control the private keys for some input in that set, and that the amounts balance without revealing what the actual input is being spent and without revealing the amount. 86 | - Zero knowledge proofs allow funds to be spent without revealing what member of the ring is sending and what the amount of Monero is being spent was. 87 | 88 | 10:00 - 20:00: 89 | 90 | - Stealth addresses are created automatically when you initiate a Monero transaction they serve to further obfuscate identities on the Monero blockchain. You're creating a new stealth address (essentially a burner address and also known as a one-time public keys) everytime you're sending a Monero transaction. 91 | - Another feature of Monero is Bulletproofs, they made the network alot faster and brought down transaction fees. 92 | - The more privacy technology added was Dandelion, which was originally developed for the Bitcoin blockchain but it was later implemented for Monero. What it does it hides IP addresses from being sent along with the transaction. It does this by splitting the transmission of a transaction into two parts. First is the anonymity phase, where the transaction is shared from one node to the next without being shared more widely to the network. At a randomly selected point, a node will choose to start spreading all around to the rest of the network. So when you're receiving it at the later stage, you have no idea whether the IP address you're receiving from, is the same IP address where the transaction came from. Which makes it essentially invisible of what the initiators IP address was. 93 | - He goes on to explain how it was a really big challenge for Chainalysis because of alot of how they do their Monero tracing involved IP observations. Anything after 2020 (When Dandelion was introduced) IP observation became much much weaker. Before it was introduced they were more confident in their IP observation skills (lol). They also name transactions on if it was the "pre-dandelion era" or the "post-dandelion era" 94 | - A Bitcoin address shows the actual spenders, the transacted amounts are visible, the address itself is also public. However, with Monero, the addresses are all stealth addresses, so you can't toss that addresses into a searchbar and find other transactions from that address. THe actual inputs are hidden in ring signature. Also, no amount disclosed. 95 | - However, there are still some breadcrumbs they can use. The payment ID was an optional field (up to Dec 2019). The fees paid with the transaction. Some initiators would use a higher fee structure, in order to have their transactions prioritised by the network, which where Chainalysis would use that to identify behaviours, as some initiators would use the same fees structure, creating a link. It shows: Fees paid, Size of transaction, number of mixins, unlocking time, number of inputs, number of outputs, transaction version and key information order. 96 | - Most users like to use the x1 fee multiplier (the default). However shown on the video theres multiple spikes meaning some of them can be identified if they are consistently using that fee structure. 97 | 98 | 20:00 - 30:00 99 | 100 | - Chainalysis uses honeypot nodes to "bypass" dandelion, as if an initiator were to broadcast a Chainalysis node it would be labeled with "RPC: IP address"; indicating a Chainalysis broadedcasted transaction, meaning Chainalysis can see the initiators IP address, when they connect. 101 | - Chainalysis has a Monero Block Explorer, which shows the most recent blocks that have been mined and added to the Monero blockchain. So those blocks have little to no history behind them, (e.g, no spending of the outputs). 102 | - Older blocks will have much more information than the latest blocks mined. 103 | - Chainalysis investigated a darknet marketplace and specifically the administrators which were believed to be operating outside of Columbia. They had a list of transactions, which the transactions represented the administrator of the darknet marketplace was swapping from Bitcoin to Monero, using the swap service "MorphToken". Each time the administrator swapped, it represented a transaction hash sending the Monero assosciated with the swap. He then uses the Monero Block Explorer to find more information about the transactions, which in this case returned 70 txids (transaction ids). The data involves, the block height, the date, the node, the coutnry, delay to next, features, txid, name. The Monero explorer already identified that it was a MorphToken swap and that the same transaction features were used. 104 | - Chainalysis also has a document full of known, or suspected service, or exchange IP addresses. The list conatains Binance, Exodus, Cumberland Mining, SwapLap, FixedFloat, MorphToken, ChangeNow, CoinSwitch. 105 | - Chainalysis later on finds that the administrator connected to one of their nodes when swapping, as it was labeled with "RPC", meaning that IP observation was fairly simple, as dandelion had no effect. He checks the IP address with ipqualityscore.com, which is a IP address Lookup site, showing if the IP was a VPN/Proxy, which it was. 106 | - From the outputs section, there was a bunch of IP addresses which show that it was potentially deposits from services. In this case the IP address was "116.202.237.82", an IP address assosciated with ChangeNow. Which where he copies the txid, and Chainalysis would return it to LE, describing how this transaction id might be the user depositing funds into ChangeNow. Which where LE would go to ChangeNow and try to get some KYC information. 107 | - Coming back to the administrator, the ring CTs are ruled out as he filtered them with the IP address of MorphToken. 108 | - Looking at the co-spends he sees the same RPC IP address, meaning the same Chainalysis node where the transaction was broadcasted from, tracking the transaction hops, one of the outputs is the spend and the other is the change in balance. Even if it's post-dandelion, Chainalysis still knows that the user connected to one of their nodes and broadcasted a transaction on their node. Going forward one more hop, he sees that in Output 0, he already ruled out all the decoys, and sees that its an RPC ip address, meaning that was the IP address that initiated the transaction, he sees that the IP address seems to be assosciated with Columbia, he checks the IP address on ipqualityscore.com to verify its a clean IP address, which it is. 109 | - After identifying the IP address of the administrator, they leverage the IP address with other data, to find other information about the administrator (off the Monero blockchain.). Then he uses the Chainalysis tool "reactor" to find that the IP address was used in two centralized entities, which can later be subpoenad if theres any records. In this case, there were records showing the identity of the administrator in Columbia. From a list of MorphToken swap TXIDs to the Monero Blockchain Explorer to finding out that the user connected to a Chainalysis node, to then tracing it forward to other Chainalysis tools to find the identity of the darknet marketplace administrator. 110 | -------------------------------------------------------------------------------- /xmr_churner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antichainalysis/xmr-churner/3b88bde173422c91ea00808a7fe59f6b56ae28f0/xmr_churner.png --------------------------------------------------------------------------------