Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions test/functional/rpc_coinjoin.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
# file COPYING or http://www.opensource.org/licenses/mit-license.php.

import random
from decimal import Decimal

from test_framework.test_framework import BitcoinTestFramework
from test_framework.blocktools import COINBASE_MATURITY
Expand All @@ -20,6 +21,7 @@
)

# See coinjoin/options.h
COINJOIN_RANDOM_ROUNDS = 3
COINJOIN_ROUNDS_DEFAULT = 4
COINJOIN_ROUNDS_MAX = 16
COINJOIN_ROUNDS_MIN = 2
Expand Down Expand Up @@ -60,6 +62,7 @@ def run_test(self):
w3 = node.get_wallet_rpc('w3')
self.generatetoaddress(node, COINBASE_MATURITY + 1, w3.getnewaddress())
self.test_use_cj_option(w3)
self.test_use_cj_success(w3)
w3.unloadwallet()

if not self.options.descriptors:
Expand Down Expand Up @@ -125,6 +128,19 @@ def test_newkeypool_stops_mixing(self, node):
node.newkeypool()
assert_equal(node.getcoinjoininfo()['running'], False)

def simulate_mixing(self, node, inputs, denom, rounds):
# A same-denomination, fee-free self-spend advances each output by one
# round. Zero-fee transactions are not relayed, so mine them directly.
for _ in range(rounds):
outputs = [{node.getnewaddress(): denom} for _ in inputs]
raw_tx = node.createrawtransaction(inputs, outputs)
signed_tx = node.signrawtransactionwithwallet(raw_tx)
assert_equal(signed_tx['complete'], True)
txid = node.decoderawtransaction(signed_tx['hex'])['txid']
self.generateblock(self.nodes[0], output=node.getnewaddress(), transactions=[signed_tx['hex']])
inputs = [{'txid': txid, 'vout': n} for n in range(len(inputs))]
return inputs

def test_use_cj_option(self, node):
self.log.info('"use_cj" option should spend fully mixed coins only')
addr = node.getnewaddress()
Expand All @@ -151,6 +167,51 @@ def test_use_cj_option(self, node):
# The same input is spendable once "use_cj" is not requested
assert_equal(node.sendall(recipients=[addr], options={'inputs': [input_ref]})['complete'], True)

def test_use_cj_success(self, node):
self.log.info('"use_cj" option should spend fully mixed coins and mark the transaction as CoinJoin')
# Node-global setting, restored at the end of this subtest
node.setcoinjoinrounds(COINJOIN_ROUNDS_MIN)

# Fund two denominated outputs. The funding transaction pays a fee and
# has non-denominated change, so both outputs start at 0 rounds.
denom = Decimal('0.00100001')
funding_txid = node.send(outputs=[{node.getnewaddress(): denom}, {node.getnewaddress(): denom}])['txid']
self.generate(self.nodes[0], 1)
funding_tx = node.gettransaction(txid=funding_txid, verbose=True)['decoded']
inputs = [{'txid': funding_txid, 'vout': out['n']} for out in funding_tx['vout'] if out['value'] == denom]
assert_equal(len(inputs), 2)

# COINJOIN_ROUNDS_MIN + COINJOIN_RANDOM_ROUNDS rounds make IsFullyMixed()
# deterministic regardless of the wallet's salt.
rounds = COINJOIN_ROUNDS_MIN + COINJOIN_RANDOM_ROUNDS
inputs = self.simulate_mixing(node, inputs, denom, rounds)

# Both coins report full rounds and count towards the anonymized balance
mixed_utxos = [(u['txid'], u['vout']) for u in node.listunspent()
if u['coinjoin_rounds'] == COINJOIN_ROUNDS_MIN + COINJOIN_RANDOM_ROUNDS]
assert_equal(sorted(mixed_utxos), sorted((i['txid'], i['vout']) for i in inputs))
assert_equal(node.getbalances()['mine']['coinjoin'], 2 * denom)

# "send" accepts a fully mixed preset input; the transaction is marked as
# CoinJoin and pays the remainder as fee instead of creating change
res = node.send(outputs={node.getnewaddress(): Decimal('0.0009')}, options={'inputs': [inputs[0]], 'use_cj': True})
assert_equal(res['complete'], True)
tx = node.gettransaction(txid=res['txid'], verbose=True)
assert_equal(tx['DS'], '1')
assert_equal([(txin['txid'], txin['vout']) for txin in tx['decoded']['vin']],
[(inputs[0]['txid'], inputs[0]['vout'])])
assert_equal(len(tx['decoded']['vout']), 1)

# "sendall" with automatic selection sweeps the remaining fully mixed coin
res = node.sendall(recipients=[node.getnewaddress()], options={'use_cj': True})
assert_equal(res['complete'], True)
tx = node.gettransaction(txid=res['txid'], verbose=True)
assert_equal(tx['DS'], '1')
assert_equal([(txin['txid'], txin['vout']) for txin in tx['decoded']['vin']],
[(inputs[1]['txid'], inputs[1]['vout'])])

node.setcoinjoinrounds(COINJOIN_ROUNDS_DEFAULT)

def test_setcoinjoinamount(self, node):
self.log.info('"setcoinjoinamount" should update mixing target')
# Test normal and large values
Expand Down
Loading