Improved change handling for Privacy chooser
Breaks up large change in such a way as to make it unclear what the real send might be. Fixes #1203
This commit is contained in:
parent
ea49e8dc96
commit
2763b0feea
|
@ -17,7 +17,8 @@
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from collections import defaultdict, namedtuple
|
from collections import defaultdict, namedtuple
|
||||||
from random import shuffle
|
from random import choice, randint, shuffle
|
||||||
|
from math import floor, log10
|
||||||
|
|
||||||
from bitcoin import COIN
|
from bitcoin import COIN
|
||||||
from transaction import Transaction
|
from transaction import Transaction
|
||||||
|
@ -58,17 +59,23 @@ class CoinChooserBase(PrintError):
|
||||||
return 0
|
return 0
|
||||||
return penalty
|
return penalty
|
||||||
|
|
||||||
def add_change(self, tx, change_addrs, fee_estimator, dust_threshold):
|
def change_amounts(self, tx, count, fee_estimator, dust_threshold):
|
||||||
# How much is left if we add 1 change output?
|
# The amount left after adding 1 change output
|
||||||
change_amount = tx.get_fee() - fee_estimator(1)
|
return [tx.get_fee() - fee_estimator(1)]
|
||||||
|
|
||||||
|
def change_outputs(self, tx, change_addrs, fee_estimator, dust_threshold):
|
||||||
|
amounts = self.change_amounts(tx, len(change_addrs), fee_estimator,
|
||||||
|
dust_threshold)
|
||||||
# If change is above dust threshold after accounting for the
|
# If change is above dust threshold after accounting for the
|
||||||
# size of the change output, add it to the transaction.
|
# size of the change output, add it to the transaction.
|
||||||
if change_amount > dust_threshold:
|
dust = sum(amount for amount in amounts if amount < dust_threshold)
|
||||||
tx.outputs.append(('address', change_addrs[0], change_amount))
|
amounts = [amount for amount in amounts if amount >= dust_threshold]
|
||||||
self.print_error('change', change_amount)
|
change = [('address', addr, amount)
|
||||||
elif change_amount:
|
for addr, amount in zip(change_addrs, amounts)]
|
||||||
self.print_error('not keeping dust', change_amount)
|
self.print_error('change:', change)
|
||||||
|
if dust:
|
||||||
|
self.print_error('not keeping dust', dust)
|
||||||
|
return change
|
||||||
|
|
||||||
def make_tx(self, coins, outputs, change_addrs, fee_estimator,
|
def make_tx(self, coins, outputs, change_addrs, fee_estimator,
|
||||||
dust_threshold):
|
dust_threshold):
|
||||||
|
@ -101,7 +108,8 @@ class CoinChooserBase(PrintError):
|
||||||
# This takes a count of change outputs and returns a tx fee;
|
# This takes a count of change outputs and returns a tx fee;
|
||||||
# each pay-to-bitcoin-address output serializes as 34 bytes
|
# each pay-to-bitcoin-address output serializes as 34 bytes
|
||||||
fee = lambda count: fee_estimator(tx_size + count * 34)
|
fee = lambda count: fee_estimator(tx_size + count * 34)
|
||||||
self.add_change(tx, change_addrs, fee, dust_threshold)
|
change = self.change_outputs(tx, change_addrs, fee, dust_threshold)
|
||||||
|
tx.outputs.extend(change)
|
||||||
|
|
||||||
self.print_error("using %d inputs" % len(tx.inputs))
|
self.print_error("using %d inputs" % len(tx.inputs))
|
||||||
self.print_error("using buckets:", [bucket.desc for bucket in buckets])
|
self.print_error("using buckets:", [bucket.desc for bucket in buckets])
|
||||||
|
@ -179,7 +187,11 @@ class CoinChooserPrivacy(CoinChooserRandom):
|
||||||
reduce blockchain UTXO bloat, and reduce future privacy loss
|
reduce blockchain UTXO bloat, and reduce future privacy loss
|
||||||
that would come from reusing that address' remaining UTXOs.
|
that would come from reusing that address' remaining UTXOs.
|
||||||
Second, it penalizes change that is quite different to the sent
|
Second, it penalizes change that is quite different to the sent
|
||||||
amount. Third, it penalizes change that is too big.'''
|
amount. Third, it penalizes change that is too big. Fourth, it
|
||||||
|
breaks large change up into amounts comparable to the spent
|
||||||
|
amount. Finally, change is rounded to similar precision to
|
||||||
|
sent amounts. Extra change outputs and rounding might raise
|
||||||
|
the transaction fee slightly'''
|
||||||
|
|
||||||
def keys(self, coins):
|
def keys(self, coins):
|
||||||
return [coin['address'] for coin in coins]
|
return [coin['address'] for coin in coins]
|
||||||
|
@ -208,5 +220,57 @@ class CoinChooserPrivacy(CoinChooserRandom):
|
||||||
|
|
||||||
return penalty
|
return penalty
|
||||||
|
|
||||||
|
def change_amounts(self, tx, count, fee_estimator, dust_threshold):
|
||||||
|
|
||||||
|
# Break change up if bigger than max_change
|
||||||
|
output_amounts = [o[2] for o in tx.outputs]
|
||||||
|
max_change = max(max(output_amounts) * 1.25, dust_threshold * 10)
|
||||||
|
|
||||||
|
# Use N change outputs
|
||||||
|
for n in range(1, count + 1):
|
||||||
|
# How much is left if we add this many change outputs?
|
||||||
|
change_amount = tx.get_fee() - fee_estimator(n)
|
||||||
|
if change_amount // n < max_change:
|
||||||
|
break
|
||||||
|
|
||||||
|
# Get a handle on the precision of the output amounts; round our
|
||||||
|
# change to look similar
|
||||||
|
def trailing_zeroes(val):
|
||||||
|
s = str(val)
|
||||||
|
return len(s) - len(s.rstrip('0'))
|
||||||
|
|
||||||
|
zeroes = map(trailing_zeroes, output_amounts)
|
||||||
|
min_zeroes = min(zeroes)
|
||||||
|
max_zeroes = max(zeroes)
|
||||||
|
zeroes = range(max(0, min_zeroes - 1), min(max_zeroes + 1, 8) + 1)
|
||||||
|
|
||||||
|
# Calculate change; randomize it a bit if using more than 1 output
|
||||||
|
remaining = change_amount
|
||||||
|
amounts = []
|
||||||
|
while n > 1:
|
||||||
|
average = remaining // n
|
||||||
|
amount = randint(int(average * 0.7), int(average * 1.3))
|
||||||
|
precision = min(choice(zeroes), int(floor(log10(amount))))
|
||||||
|
amount = int(round(amount, -precision))
|
||||||
|
amounts.append(amount)
|
||||||
|
remaining -= amount
|
||||||
|
n -= 1
|
||||||
|
|
||||||
|
# Last change output. Round down to maximum precision but lose
|
||||||
|
# no more than 100 satoshis to fees (2dp)
|
||||||
|
amount = remaining
|
||||||
|
N = min(2, zeroes[0])
|
||||||
|
if N:
|
||||||
|
amount = int(round(amount, -N))
|
||||||
|
if amount > remaining:
|
||||||
|
amount -= pow(10, N)
|
||||||
|
amounts.append(amount)
|
||||||
|
|
||||||
|
assert sum(amounts) <= change_amount
|
||||||
|
assert min(amounts) >= 0
|
||||||
|
|
||||||
|
return amounts
|
||||||
|
|
||||||
|
|
||||||
COIN_CHOOSERS = {'Classic': CoinChooserClassic,
|
COIN_CHOOSERS = {'Classic': CoinChooserClassic,
|
||||||
'Privacy': CoinChooserPrivacy}
|
'Privacy': CoinChooserPrivacy}
|
||||||
|
|
Loading…
Reference in New Issue