├── .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 |
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 | 
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
--------------------------------------------------------------------------------