Coin chooser: use deterministic randomness
This commit is contained in:
parent
06809917ca
commit
15707b5590
|
@ -17,13 +17,52 @@
|
|||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
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])
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
Loading…
Reference in New Issue