Coin chooser: use deterministic randomness

This commit is contained in:
Neil Booth 2016-01-16 18:55:50 +09:00
parent 06809917ca
commit 15707b5590
2 changed files with 48 additions and 16 deletions

View File

@ -17,13 +17,52 @@
# 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 choice, randint, shuffle
from math import floor, log10 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 transaction import Transaction
from util import NotEnoughFunds, PrintError, profiler 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']) Bucket = namedtuple('Bucket', ['desc', 'size', 'value', 'coins'])
def strip_unneeded(bkts, sufficient_funds): def strip_unneeded(bkts, sufficient_funds):
@ -87,8 +126,8 @@ class CoinChooserBase(PrintError):
amounts = [] amounts = []
while n > 1: while n > 1:
average = remaining // n average = remaining // n
amount = randint(int(average * 0.7), int(average * 1.3)) amount = self.p.randint(int(average * 0.7), int(average * 1.3))
precision = min(choice(zeroes), int(floor(log10(amount)))) precision = min(self.p.choice(zeroes), int(floor(log10(amount))))
amount = int(round(amount, -precision)) amount = int(round(amount, -precision))
amounts.append(amount) amounts.append(amount)
remaining -= amount remaining -= amount
@ -127,6 +166,10 @@ class CoinChooserBase(PrintError):
the transaction) it is kept, otherwise none is sent and it is the transaction) it is kept, otherwise none is sent and it is
added to the transaction fee.''' 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" # Copy the ouputs so when adding change we don't modify "outputs"
tx = Transaction.from_io([], outputs[:]) tx = Transaction.from_io([], outputs[:])
# Size of the transaction with no inputs and no change # Size of the transaction with no inputs and no change
@ -201,7 +244,7 @@ class CoinChooserRandom(CoinChooserBase):
for i in range(attempts): for i in range(attempts):
# Get a random permutation of the buckets, and # Get a random permutation of the buckets, and
# incrementally combine buckets until sufficient # incrementally combine buckets until sufficient
shuffle(permutation) self.p.shuffle(permutation)
bkts = [] bkts = []
for count, index in enumerate(permutation): for count, index in enumerate(permutation):
bkts.append(buckets[index]) bkts.append(buckets[index])

View File

@ -196,7 +196,6 @@ class Abstract_Wallet(PrintError):
self.lock = threading.Lock() self.lock = threading.Lock()
self.transaction_lock = threading.Lock() self.transaction_lock = threading.Lock()
self.tx_event = threading.Event() self.tx_event = threading.Event()
self.tx_cache = (None, None, None, None, None)
self.check_history() self.check_history()
@ -966,14 +965,6 @@ class Abstract_Wallet(PrintError):
# Change <= dust threshold is added to the tx fee # Change <= dust threshold is added to the tx fee
dust_threshold = 182 * 3 * self.relayfee() / 1000 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 # Let the coin chooser select the coins to spend
max_change = 3 if self.multiple_change else 1 max_change = 3 if self.multiple_change else 1
coin_chooser = self.coin_chooser(config) coin_chooser = self.coin_chooser(config)
@ -983,8 +974,6 @@ class Abstract_Wallet(PrintError):
# Sort the inputs and outputs deterministically # Sort the inputs and outputs deterministically
tx.BIP_LI01_sort() tx.BIP_LI01_sort()
self.tx_cache = (outputs, coins, change_addrs, dust_threshold, tx)
run_hook('make_unsigned_transaction', self, tx) run_hook('make_unsigned_transaction', self, tx)
return tx return tx