1
0

Add coin chooser to try and minimize loss of privacy.

This commit is contained in:
Neil Booth
2015-11-29 23:19:13 +09:00
parent de964f4033
commit 75b3ecee49
4 changed files with 176 additions and 38 deletions

View File

@@ -17,14 +17,19 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from collections import defaultdict, namedtuple
from random import shuffle
from util import NotEnoughFunds, PrintError, profiler
from bitcoin import COIN
from transaction import Transaction
from util import NotEnoughFunds, PrintError, profiler
Bucket = namedtuple('Bucket', ['desc', 'size', 'value', 'coins'])
class CoinChooserBase(PrintError):
def keys(self, coins):
raise NotImplementedError
def bucketize_coins(self, coins):
keys = self.keys(coins)
buckets = defaultdict(list)
@@ -39,55 +44,66 @@ class CoinChooserBase(PrintError):
return map(make_Bucket, buckets.keys(), buckets.values())
def make_tx(self, coins, outputs, change_addrs, fee_estimator,
dust_threshold):
'''Select unspent coins to spend to pay outputs. If the change is
greater than dust_threshold (after adding the change output to
the transaction) it is kept, otherwise none is sent and it is
added to the transaction fee.'''
output_total = sum(map(lambda x: x[2], outputs))
def penalty_func(self, tx):
def penalty(candidate):
return 0
return penalty
# Size of the transaction with no inputs and no change
tx = Transaction.from_io([], outputs)
base_size = tx.estimated_size()
# Returns fee given input size
fee = lambda input_size: fee_estimator(base_size + input_size)
# Collect the coins into buckets, choose a subset of the buckets
buckets = self.bucketize_coins(coins)
buckets = self.choose_buckets(buckets, output_total, fee)
tx.inputs = [coin for b in buckets for coin in b.coins]
input_total = sum(bucket.value for bucket in buckets)
tx_size = base_size + sum(bucket.size for bucket in buckets)
def add_change(self, tx, change_addrs, fee_estimator, dust_threshold):
# How much is left if we add 1 change output?
change_amount = tx.get_fee() - fee_estimator(1)
# If change is above dust threshold after accounting for the
# size of the change output, add it to the transaction.
# Pay to bitcoin address serializes as 34 bytes
change_size = 34
fee = fee_estimator(tx_size + change_size)
change_amount = input_total - (output_total + fee)
if change_amount > dust_threshold:
tx.outputs.append(('address', change_addrs[0], change_amount))
self.print_error('change', change_amount)
elif change_amount:
self.print_error('not keeping dust', change_amount)
def make_tx(self, coins, outputs, change_addrs, fee_estimator,
dust_threshold):
'''Select unspent coins to spend to pay outputs. If the change is
greater than dust_threshold (after adding the change output to
the transaction) it is kept, otherwise none is sent and it is
added to the transaction fee.'''
# Copy the ouputs so when adding change we don't modify "outputs"
tx = Transaction.from_io([], outputs[:])
# Size of the transaction with no inputs and no change
base_size = tx.estimated_size()
# Returns fee given input size
fee = lambda input_size: fee_estimator(base_size + input_size)
# Collect the coins into buckets, choose a subset of the buckets
buckets = self.bucketize_coins(coins)
buckets = self.choose_buckets(buckets, tx.output_value(), fee,
self.penalty_func(tx))
tx.inputs = [coin for b in buckets for coin in b.coins]
tx_size = base_size + sum(bucket.size for bucket in buckets)
# This takes a count of change outputs and returns a tx fee;
# each pay-to-bitcoin-address output serializes as 34 bytes
fee = lambda count: fee_estimator(tx_size + count * 34)
self.add_change(tx, change_addrs, fee, dust_threshold)
self.print_error("using %d inputs" % len(tx.inputs))
self.print_error("using buckets:", [bucket.desc for bucket in buckets])
return tx
class CoinChooser(CoinChooserBase):
'''The original electrum algorithm. Chooses coins starting with the
oldest that are sufficient to cover the spent amount, and then
removes any not needed starting with the smallest in value.'''
class CoinChooserClassic(CoinChooserBase):
'''
The classic electrum algorithm. Chooses coins starting with
the oldest that are sufficient to cover the spent amount, and
then removes any unneeded starting with the smallest in value.'''
def keys(self, coins):
return [coin['prevout_hash'] + ':' + str(coin['prevout_n'])
for coin in coins]
def choose_buckets(self, buckets, spent_amount, fee):
def choose_buckets(self, buckets, spent_amount, fee, penalty_func):
'''Spend the oldest buckets first.'''
# Unconfirmed coins are young, not old
adj_height = lambda height: 99999999 if height == 0 else height
@@ -113,3 +129,89 @@ class CoinChooser(CoinChooserBase):
dropped.append(bucket)
return [bucket for bucket in selected if bucket not in dropped]
class CoinChooserRandom(CoinChooserBase):
def bucket_candidates(self, buckets, sufficient_funds):
'''Returns a list of bucket sets.'''
candidates = set()
# Add all singletons
for n, bucket in enumerate(buckets):
if sufficient_funds([bucket]):
candidates.add((n, ))
# And now some random ones
attempts = min(100, (len(buckets) - 1) * 10 + 1)
permutation = range(len(buckets))
for i in range(attempts):
# Get a random permutation of the buckets, and
# incrementally combine buckets until sufficient
shuffle(permutation)
bkts = []
for count, index in enumerate(permutation):
bkts.append(buckets[index])
if sufficient_funds(bkts):
candidates.add(tuple(sorted(permutation[:count + 1])))
break
else:
raise NotEnoughFunds()
return [[buckets[n] for n in candidate] for candidate in candidates]
def choose_buckets(self, buckets, spent_amount, fee, penalty_func):
def sufficient(buckets):
'''Given a set of buckets, return True if it has enough
value to pay for the transaction'''
total_input = sum(bucket.value for bucket in buckets)
total_size = sum(bucket.size for bucket in buckets)
return total_input >= spent_amount + fee(total_size)
candidates = self.bucket_candidates(buckets, sufficient)
penalties = [penalty_func(cand) for cand in candidates]
winner = candidates[penalties.index(min(penalties))]
self.print_error("Bucket sets:", len(buckets))
self.print_error("Winning penalty:", min(penalties))
return winner
class CoinChooserPrivacy(CoinChooserRandom):
'''
Attempts to better preserve user privacy. First, if any coin is
spent from a user address, all coins are. Compared to spending
from other addresses to make up an amount, this reduces
information leakage about sender holdings. It also helps to
reduce blockchain UTXO bloat, and reduce future privacy loss
that would come from reusing that address' remaining UTXOs.
Second, it penalizes change that is quite different to the sent
amount. Third, it penalizes change that is too big.'''
def keys(self, coins):
return [coin['address'] for coin in coins]
def penalty_func(self, buckets, tx):
'''Returns a penalty for a candidate set of buckets.'''
raise NotImplementedError
def penalty_func(self, tx):
min_change = min(o[2] for o in tx.outputs) * 0.75
max_change = max(o[2] for o in tx.outputs) * 1.33
spent_amount = sum(o[2] for o in tx.outputs)
def penalty(buckets):
badness = len(buckets) - 1
total_input = sum(bucket.value for bucket in buckets)
change = float(total_input - spent_amount)
# Penalize change not roughly in output range
if change < min_change:
badness += (min_change - change) / (min_change + 10000)
elif change > max_change:
badness += (change - max_change) / (max_change + 10000)
# Penalize large change; 5 BTC excess ~= using 1 more input
badness += change / (COIN * 5)
return badness
return penalty
COIN_CHOOSERS = {'Classic': CoinChooserClassic,
'Privacy': CoinChooserPrivacy}