def test_random_draw(self): utxo_pool = self.estimates(utxo(2 * CENT), utxo(3 * CENT), utxo(4 * CENT)) selector = CoinSelector(utxo_pool, CENT, 0, '\x00') match = selector.select() self.assertEqual([2 * CENT], [c.txo.amount for c in match]) self.assertFalse(selector.exact_match)
def test_exact_match(self): fee = utxo(CENT).get_estimator(self.ledger).fee utxo_pool = self.estimates(utxo(CENT + fee), utxo(CENT), utxo(CENT - fee)) selector = CoinSelector(utxo_pool, CENT, 0) match = selector.select() self.assertEqual([CENT + fee], [c.txo.amount for c in match]) self.assertTrue(selector.exact_match)
def test_branch_and_bound_coin_selection(self): self.ledger.fee_per_byte = 0 utxo_pool = self.estimates(utxo(1 * CENT), utxo(2 * CENT), utxo(3 * CENT), utxo(4 * CENT)) # Select 1 Cent self.assertEqual([1 * CENT], search(utxo_pool, 1 * CENT, 0.5 * CENT)) # Select 2 Cent self.assertEqual([2 * CENT], search(utxo_pool, 2 * CENT, 0.5 * CENT)) # Select 5 Cent self.assertEqual([3 * CENT, 2 * CENT], search(utxo_pool, 5 * CENT, 0.5 * CENT)) # Select 11 Cent, not possible self.assertIsNone(search(utxo_pool, 11 * CENT, 0.5 * CENT)) # Select 10 Cent utxo_pool += self.estimates(utxo(5 * CENT)) self.assertEqual([4 * CENT, 3 * CENT, 2 * CENT, 1 * CENT], search(utxo_pool, 10 * CENT, 0.5 * CENT)) # Negative effective value # Select 10 Cent but have 1 Cent not be possible because too small # TODO: bitcoin has [5, 3, 2] self.assertEqual([4 * CENT, 3 * CENT, 2 * CENT, 1 * CENT], search(utxo_pool, 10 * CENT, 5000)) # Select 0.25 Cent, not possible self.assertIsNone(search(utxo_pool, 0.25 * CENT, 0.5 * CENT)) # Iteration exhaustion test utxo_pool, target = self.make_hard_case(17) selector = CoinSelector(utxo_pool, target, 0) self.assertIsNone(selector.branch_and_bound()) self.assertEqual(selector.tries, MAXIMUM_TRIES) # Should exhaust utxo_pool, target = self.make_hard_case(14) self.assertIsNotNone(search(utxo_pool, target, 0)) # Should not exhaust # Test same value early bailout optimization utxo_pool = self.estimates([ utxo(7 * CENT), utxo(7 * CENT), utxo(7 * CENT), utxo(7 * CENT), utxo(2 * CENT) ] + [utxo(5 * CENT)] * 50000) self.assertEqual([7 * CENT, 7 * CENT, 7 * CENT, 7 * CENT, 2 * CENT], search(utxo_pool, 30 * CENT, 5000)) # Select 1 Cent with pool of only greater than 5 Cent utxo_pool = self.estimates(utxo(i * CENT) for i in range(5, 21)) for _ in range(100): self.assertIsNone(search(utxo_pool, 1 * CENT, 2 * CENT))
def test_pick(self): utxo_pool = self.estimates( utxo(1 * CENT), utxo(1 * CENT), utxo(3 * CENT), utxo(5 * CENT), utxo(10 * CENT), ) selector = CoinSelector(utxo_pool, 3 * CENT, 0) match = selector.select() self.assertEqual([5 * CENT], [c.txo.amount for c in match])
async def get_spendable_utxos(self, amount: int, funding_accounts): async with self._utxo_reservation_lock: txos = await self.get_effective_amount_estimators(funding_accounts) selector = CoinSelector( txos, amount, self.transaction_class.output_class.pay_pubkey_hash( COIN, NULL_HASH32).get_fee(self)) spendables = selector.select() if spendables: await self.reserve_outputs(s.txo for s in spendables) return spendables
def get_spendable_utxos(self, amount: int, funding_accounts): yield self._utxo_reservation_lock.acquire() try: txos = yield self.get_effective_amount_estimators(funding_accounts) selector = CoinSelector( txos, amount, self.transaction_class.output_class.pay_pubkey_hash( COIN, NULL_HASH32).get_fee(self)) spendables = selector.select() if spendables: yield self.reserve_outputs(s.txo for s in spendables) except Exception: log.exception('Failed to get spendable utxos:') raise finally: self._utxo_reservation_lock.release() defer.returnValue(spendables)
def pay(cls, outputs, funding_accounts, change_account, reserve_outputs=True): # type: (List[BaseOutput], List[torba.baseaccount.BaseAccount], torba.baseaccount.BaseAccount) -> defer.Deferred """ Efficiently spend utxos from funding_accounts to cover the new outputs. """ tx = cls().add_outputs(outputs) ledger = cls.ensure_all_have_same_ledger(funding_accounts, change_account) amount = tx.output_sum + ledger.get_transaction_base_fee(tx) txos = yield ledger.get_effective_amount_estimators(funding_accounts) selector = CoinSelector( txos, amount, ledger.get_input_output_fee( cls.output_class.pay_pubkey_hash(COIN, NULL_HASH) ) ) spendables = selector.select() if not spendables: raise ValueError('Not enough funds to cover this transaction.') reserved_outputs = [s.txo.txoid for s in spendables] if reserve_outputs: yield ledger.db.reserve_spent_outputs(reserved_outputs) try: spent_sum = sum(s.effective_amount for s in spendables) if spent_sum > amount: change_address = yield change_account.change.get_or_create_usable_address() change_hash160 = change_account.ledger.address_to_hash160(change_address) change_amount = spent_sum - amount tx.add_outputs([cls.output_class.pay_pubkey_hash(change_amount, change_hash160)]) tx.add_inputs([s.txi for s in spendables]) yield tx.sign(funding_accounts) except Exception: if reserve_outputs: yield ledger.db.release_reserved_outputs(reserved_outputs) raise defer.returnValue(tx)
def test_skip_binary_search_if_total_not_enough(self): fee = utxo(CENT).get_estimator(self.ledger).fee big_pool = self.estimates(utxo(CENT + fee) for _ in range(100)) selector = CoinSelector(big_pool, 101 * CENT, 0) self.assertIsNone(selector.select()) self.assertEqual(selector.tries, 0) # Never tried. # check happy path selector = CoinSelector(big_pool, 100 * CENT, 0) self.assertEqual(len(selector.select()), 100) self.assertEqual(selector.tries, 201)
def test_empty_coins(self): self.assertIsNone(CoinSelector([], 0, 0).select())
def search(*args, **kwargs): selection = CoinSelector(*args, **kwargs).branch_and_bound() return [o.txo.amount for o in selection] if selection else selection
def test_empty_coins(self): self.assertEqual(CoinSelector([], 0, 0).select(), [])