From 15707b55902643cc64aee17201ee0df8d2ad67ad Mon Sep 17 00:00:00 2001 From: Neil Booth Date: Sat, 16 Jan 2016 18:55:50 +0900 Subject: [PATCH] Coin chooser: use deterministic randomness --- lib/coinchooser.py | 53 +++++++++++++++++++++++++++++++++++++++++----- lib/wallet.py | 11 ---------- 2 files changed, 48 insertions(+), 16 deletions(-) diff --git a/lib/coinchooser.py b/lib/coinchooser.py index 476f7f426..b876b87ad 100644 --- a/lib/coinchooser.py +++ b/lib/coinchooser.py @@ -17,13 +17,52 @@ # along with this program. If not, see . from collections import defaultdict, namedtuple -from random import choice, randint, shuffle from math import floor, log10 +import hashlib +import struct -from bitcoin import COIN, TYPE_ADDRESS +from bitcoin import sha256, COIN, TYPE_ADDRESS from transaction import Transaction from util import NotEnoughFunds, PrintError, profiler +# A simple deterministic PRNG. Used to deterministically shuffle a +# set of coins - the same set of coins should produce the same output. +# Although choosing UTXOs "randomly" we want it to be deterministic, +# so if sending twice from the same UTXO set we choose the same UTXOs +# to spend. This prevents attacks on users by malicious or stale +# servers. + +class PRNG: + def __init__(self, seed): + self.sha = sha256(seed) + self.pool = bytearray() + + def get_bytes(self, n): + while len(self.pool) < n: + self.pool.extend(self.sha) + self.sha = sha256(self.sha) + result, self.pool = self.pool[:n], self.pool[n:] + return result + + def random(self): + # Returns random double in [0, 1) + four = self.get_bytes(4) + return struct.unpack("I", four)[0] / 4294967296.0 + + def randint(self, start, end): + # Returns random integer in [start, end) + return start + int(self.random() * (end - start)) + + def choice(self, seq): + return seq[int(self.random() * len(seq))] + + def shuffle(self, x): + for i in reversed(xrange(1, len(x))): + # pick an element in x[:i+1] with which to exchange x[i] + j = int(self.random() * (i+1)) + x[i], x[j] = x[j], x[i] + + Bucket = namedtuple('Bucket', ['desc', 'size', 'value', 'coins']) def strip_unneeded(bkts, sufficient_funds): @@ -87,8 +126,8 @@ class CoinChooserBase(PrintError): 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 = self.p.randint(int(average * 0.7), int(average * 1.3)) + precision = min(self.p.choice(zeroes), int(floor(log10(amount)))) amount = int(round(amount, -precision)) amounts.append(amount) remaining -= amount @@ -127,6 +166,10 @@ class CoinChooserBase(PrintError): the transaction) it is kept, otherwise none is sent and it is added to the transaction fee.''' + # Deterministic randomness from coins + utxos = [c['prevout_hash'] + str(c['prevout_n']) for c in coins] + self.p = PRNG(''.join(sorted(utxos))) + # 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 @@ -201,7 +244,7 @@ class CoinChooserRandom(CoinChooserBase): for i in range(attempts): # Get a random permutation of the buckets, and # incrementally combine buckets until sufficient - shuffle(permutation) + self.p.shuffle(permutation) bkts = [] for count, index in enumerate(permutation): bkts.append(buckets[index]) diff --git a/lib/wallet.py b/lib/wallet.py index 58ee58210..249ec4909 100644 --- a/lib/wallet.py +++ b/lib/wallet.py @@ -196,7 +196,6 @@ class Abstract_Wallet(PrintError): self.lock = threading.Lock() self.transaction_lock = threading.Lock() self.tx_event = threading.Event() - self.tx_cache = (None, None, None, None, None) self.check_history() @@ -966,14 +965,6 @@ class Abstract_Wallet(PrintError): # Change <= dust threshold is added to the tx fee dust_threshold = 182 * 3 * self.relayfee() / 1000 - # Check cache to see if we just calculated this. If prior - # calculated a fee and this fixes it to the same, return same - # answer, to avoid random coin selection changing the answer - if self.tx_cache[:4] == (outputs, coins, change_addrs, dust_threshold): - tx = self.tx_cache[4] - if tx.get_fee() == fee_estimator(tx.estimated_size()): - return tx - # Let the coin chooser select the coins to spend max_change = 3 if self.multiple_change else 1 coin_chooser = self.coin_chooser(config) @@ -983,8 +974,6 @@ class Abstract_Wallet(PrintError): # Sort the inputs and outputs deterministically tx.BIP_LI01_sort() - self.tx_cache = (outputs, coins, change_addrs, dust_threshold, tx) - run_hook('make_unsigned_transaction', self, tx) return tx