From c23808f0547dc17bafc6be53c84a575811893995 Mon Sep 17 00:00:00 2001 From: Kristaps Kaupe Date: Sat, 11 Jan 2020 11:23:31 +0200 Subject: [PATCH] Privacy improvements for direct_send() * Shuffle order of tx inputs and outputs * Set locktime for best anonset (Core, Electrum) --- jmclient/jmclient/__init__.py | 2 +- jmclient/jmclient/blockchaininterface.py | 4 ++++ jmclient/jmclient/maker.py | 8 ++------ jmclient/jmclient/taker.py | 8 ++------ jmclient/jmclient/taker_utils.py | 8 +++++--- jmclient/jmclient/wallet.py | 11 +++++++++++ 6 files changed, 25 insertions(+), 16 deletions(-) diff --git a/jmclient/jmclient/__init__.py b/jmclient/jmclient/__init__.py index a5c9104..9397197 100644 --- a/jmclient/jmclient/__init__.py +++ b/jmclient/jmclient/__init__.py @@ -17,7 +17,7 @@ from .taker import Taker, P2EPTaker from .wallet import (Mnemonic, estimate_tx_fee, WalletError, BaseWallet, ImportWalletMixin, BIP39WalletMixin, BIP32Wallet, BIP49Wallet, LegacyWallet, SegwitWallet, SegwitLegacyWallet, UTXOManager, - WALLET_IMPLEMENTATIONS) + WALLET_IMPLEMENTATIONS, compute_tx_locktime) from .storage import (Argon2Hash, Storage, StorageError, RetryableStorageError, StoragePasswordError, VolatileStorage) from .cryptoengine import BTCEngine, BTC_P2PKH, BTC_P2SH_P2WPKH, EngineError diff --git a/jmclient/jmclient/blockchaininterface.py b/jmclient/jmclient/blockchaininterface.py index d401d3a..4b3b285 100644 --- a/jmclient/jmclient/blockchaininterface.py +++ b/jmclient/jmclient/blockchaininterface.py @@ -388,6 +388,10 @@ class BitcoinCoreInterface(BlockchainInterface): return 10000 return int(Decimal(1e8) * Decimal(estimate)) + def get_current_block_height(self): + return self.rpc("getblockchaininfo", [])["blocks"] + + class RegtestBitcoinCoreMixin(): """ This Mixin provides helper functions that are used in Interface classes diff --git a/jmclient/jmclient/maker.py b/jmclient/jmclient/maker.py index 43ebc0d..3dcae54 100644 --- a/jmclient/jmclient/maker.py +++ b/jmclient/jmclient/maker.py @@ -13,7 +13,7 @@ from binascii import unhexlify from jmbitcoin import SerializationError, SerializationTruncationError import jmbitcoin as btc -from jmclient.wallet import estimate_tx_fee +from jmclient.wallet import estimate_tx_fee, compute_tx_locktime from jmclient.wallet_service import WalletService from jmclient.configure import jm_single from jmbase.support import get_log, EXIT_SUCCESS, EXIT_FAILURE @@ -605,11 +605,7 @@ class P2EPMaker(Maker): "value": new_change_amount}) new_ins = [x[1] for x in utxo.values()] new_ins.extend(my_utxos.keys()) - # set locktime for best anonset (Core, Electrum) - most recent block. - # this call should never fail so no catch here. - currentblock = jm_single().bc_interface.rpc( - "getblockchaininfo", [])["blocks"] - new_tx = btc.make_shuffled_tx(new_ins, new_outs, False, 2, currentblock) + new_tx = btc.make_shuffled_tx(new_ins, new_outs, False, 2, compute_tx_locktime()) new_tx_deser = btc.deserialize(new_tx) # sign our inputs before transfer diff --git a/jmclient/jmclient/taker.py b/jmclient/jmclient/taker.py index 3ba5907..c8c95fc 100644 --- a/jmclient/jmclient/taker.py +++ b/jmclient/jmclient/taker.py @@ -16,7 +16,7 @@ from jmclient.configure import jm_single, validate_address from jmbase.support import get_log from jmclient.support import (calc_cj_fee, weighted_order_choose, choose_orders, choose_sweep_orders) -from jmclient.wallet import estimate_tx_fee +from jmclient.wallet import estimate_tx_fee, compute_tx_locktime from jmclient.podle import generate_podle, get_podle_commitments, PoDLE from jmclient.wallet_service import WalletService from .output import generate_podle_error_string @@ -1007,14 +1007,10 @@ class P2EPTaker(Taker): if self.my_change_addr is not None: self.outputs.append({'address': self.my_change_addr, 'value': my_change_value}) - # set locktime for best anonset (Core, Electrum) - most recent block. - # this call should never fail so no catch here. - currentblock = jm_single().bc_interface.rpc( - "getblockchaininfo", [])["blocks"] # As for JM coinjoins, the `None` key is used for our own inputs # to the transaction; this preparatory version contains only those. tx = btc.make_shuffled_tx(self.utxos[None], self.outputs, - False, 2, currentblock) + False, 2, compute_tx_locktime()) jlog.info('Created proposed fallback tx\n' + pprint.pformat( btc.deserialize(tx))) # We now sign as a courtesy, because if we disappear the recipient diff --git a/jmclient/jmclient/taker_utils.py b/jmclient/jmclient/taker_utils.py index 17a05a5..94ffe53 100644 --- a/jmclient/jmclient/taker_utils.py +++ b/jmclient/jmclient/taker_utils.py @@ -11,8 +11,9 @@ from jmbase import get_log, jmprint from .configure import jm_single, validate_address from .schedule import human_readable_schedule_entry, tweak_tumble_schedule,\ schedule_to_text -from .wallet import BaseWallet, estimate_tx_fee -from jmbitcoin import deserialize, mktx, serialize, txhash, amount_to_str +from .wallet import BaseWallet, estimate_tx_fee, compute_tx_locktime +from jmbitcoin import deserialize, make_shuffled_tx, serialize, txhash,\ + amount_to_str from jmbase.support import EXIT_SUCCESS log = get_log() @@ -78,7 +79,8 @@ def direct_send(wallet_service, amount, mixdepth, destaddr, answeryes=False, log.info("Using a fee of : " + amount_to_str(fee_est) + ".") if amount != 0: log.info("Using a change value of: " + amount_to_str(changeval) + ".") - txsigned = sign_tx(wallet_service, mktx(list(utxos.keys()), outs), utxos) + txsigned = sign_tx(wallet_service, make_shuffled_tx( + list(utxos.keys()), outs, False, 2, compute_tx_locktime()), utxos) log.info("Got signed transaction:\n") log.info(pformat(txsigned)) tx = serialize(txsigned) diff --git a/jmclient/jmclient/wallet.py b/jmclient/jmclient/wallet.py index f54eef4..04ce77f 100644 --- a/jmclient/jmclient/wallet.py +++ b/jmclient/jmclient/wallet.py @@ -7,6 +7,7 @@ import warnings import functools import collections import numbers +import random from binascii import hexlify, unhexlify from datetime import datetime from copy import deepcopy @@ -92,6 +93,16 @@ def estimate_tx_fee(ins, outs, txtype='p2pkh'): raise NotImplementedError("Txtype: " + txtype + " not implemented.") +def compute_tx_locktime(): + # set locktime for best anonset (Core, Electrum) + # most recent block or some time back in random cases + locktime = jm_single().bc_interface.get_current_block_height() + if random.randint(0, 9) == 0: + # P2EP requires locktime > 0 + locktime = max(1, locktime - random.randint(0, 99)) + return locktime + + #FIXME: move this to a utilities file? def deprecated(func): @functools.wraps(func)