Refactor coin selection to be not be bitcoin-s specific (#4496)

* Refactor coin selection to be not be bitcoin-s specific

* Add to CoinSelectorUtxo
This commit is contained in:
benthecarman 2022-07-20 08:40:11 -05:00 committed by GitHub
parent 0a127368f0
commit c210052640
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 95 additions and 64 deletions

View file

@ -1,6 +1,6 @@
package org.bitcoins.core.wallet package org.bitcoins.core.wallet
import org.bitcoins.core.api.wallet.CoinSelector import org.bitcoins.core.api.wallet.{CoinSelector, CoinSelectorUtxo}
import org.bitcoins.core.api.wallet.db._ import org.bitcoins.core.api.wallet.db._
import org.bitcoins.core.currency._ import org.bitcoins.core.currency._
import org.bitcoins.core.hd._ import org.bitcoins.core.hd._
@ -16,9 +16,10 @@ class CoinSelectorTest extends BitcoinSUnitTest {
behavior of "CoinSelector" behavior of "CoinSelector"
val utxos: Vector[SpendingInfoDb] = val utxos: Vector[CoinSelectorUtxo] =
createSpendingInfoDbs(Vector(Bitcoins(1), Bitcoins(2))) createSpendingInfoDbs(Vector(Bitcoins(1), Bitcoins(2)))
val inAmt: CurrencyUnit = utxos.map(_.output.value).sum .map(CoinSelectorUtxo.fromSpendingInfoDb)
val inAmt: CurrencyUnit = utxos.map(_.prevOut.value).sum
val target: Bitcoins = Bitcoins(2) val target: Bitcoins = Bitcoins(2)
val changeCost: Satoshis = Satoshis.one val changeCost: Satoshis = Satoshis.one

View file

@ -1,9 +1,9 @@
package org.bitcoins.core.api.wallet package org.bitcoins.core.api.wallet
import org.bitcoins.core.api.wallet.CoinSelectionAlgo._ import org.bitcoins.core.api.wallet.CoinSelectionAlgo._
import org.bitcoins.core.api.wallet.db.SpendingInfoDb
import org.bitcoins.core.currency._ import org.bitcoins.core.currency._
import org.bitcoins.core.protocol.transaction.TransactionOutput import org.bitcoins.core.protocol.script._
import org.bitcoins.core.protocol.transaction._
import org.bitcoins.core.wallet.fee.FeeUnit import org.bitcoins.core.wallet.fee.FeeUnit
import scala.annotation.tailrec import scala.annotation.tailrec
@ -16,9 +16,9 @@ trait CoinSelector {
* should only be used for research purposes * should only be used for research purposes
*/ */
def randomSelection( def randomSelection(
walletUtxos: Vector[SpendingInfoDb], walletUtxos: Vector[CoinSelectorUtxo],
outputs: Vector[TransactionOutput], outputs: Vector[TransactionOutput],
feeRate: FeeUnit): Vector[SpendingInfoDb] = { feeRate: FeeUnit): Vector[CoinSelectorUtxo] = {
val randomUtxos = Random.shuffle(walletUtxos) val randomUtxos = Random.shuffle(walletUtxos)
accumulate(randomUtxos, outputs, feeRate) accumulate(randomUtxos, outputs, feeRate)
@ -28,11 +28,11 @@ trait CoinSelector {
* below their fees. Better for high fee environments than accumulateSmallestViable. * below their fees. Better for high fee environments than accumulateSmallestViable.
*/ */
def accumulateLargest( def accumulateLargest(
walletUtxos: Vector[SpendingInfoDb], walletUtxos: Vector[CoinSelectorUtxo],
outputs: Vector[TransactionOutput], outputs: Vector[TransactionOutput],
feeRate: FeeUnit): Vector[SpendingInfoDb] = { feeRate: FeeUnit): Vector[CoinSelectorUtxo] = {
val sortedUtxos = val sortedUtxos =
walletUtxos.sortBy(_.output.value).reverse walletUtxos.sortBy(_.prevOut.value).reverse
accumulate(sortedUtxos, outputs, feeRate) accumulate(sortedUtxos, outputs, feeRate)
} }
@ -43,29 +43,29 @@ trait CoinSelector {
* Has the potential privacy breach of connecting a ton of UTXOs to one address. * Has the potential privacy breach of connecting a ton of UTXOs to one address.
*/ */
def accumulateSmallestViable( def accumulateSmallestViable(
walletUtxos: Vector[SpendingInfoDb], walletUtxos: Vector[CoinSelectorUtxo],
outputs: Vector[TransactionOutput], outputs: Vector[TransactionOutput],
feeRate: FeeUnit): Vector[SpendingInfoDb] = { feeRate: FeeUnit): Vector[CoinSelectorUtxo] = {
val sortedUtxos = walletUtxos.sortBy(_.output.value) val sortedUtxos = walletUtxos.sortBy(_.prevOut.value)
accumulate(sortedUtxos, outputs, feeRate) accumulate(sortedUtxos, outputs, feeRate)
} }
/** Greedily selects from walletUtxos in order, skipping outputs with values below their fees */ /** Greedily selects from walletUtxos in order, skipping outputs with values below their fees */
def accumulate( def accumulate(
walletUtxos: Vector[SpendingInfoDb], walletUtxos: Vector[CoinSelectorUtxo],
outputs: Vector[TransactionOutput], outputs: Vector[TransactionOutput],
feeRate: FeeUnit): Vector[SpendingInfoDb] = { feeRate: FeeUnit): Vector[CoinSelectorUtxo] = {
val totalValue = outputs.foldLeft(CurrencyUnits.zero) { val totalValue = outputs.foldLeft(CurrencyUnits.zero) {
case (totVal, output) => totVal + output.value case (totVal, output) => totVal + output.value
} }
@tailrec @tailrec
def addUtxos( def addUtxos(
alreadyAdded: Vector[SpendingInfoDb], alreadyAdded: Vector[CoinSelectorUtxo],
valueSoFar: CurrencyUnit, valueSoFar: CurrencyUnit,
bytesSoFar: Long, bytesSoFar: Long,
utxosLeft: Vector[SpendingInfoDb]): Vector[SpendingInfoDb] = { utxosLeft: Vector[CoinSelectorUtxo]): Vector[CoinSelectorUtxo] = {
val fee = feeRate * bytesSoFar val fee = feeRate * bytesSoFar
if (valueSoFar > totalValue + fee) { if (valueSoFar > totalValue + fee) {
alreadyAdded alreadyAdded
@ -79,7 +79,7 @@ trait CoinSelector {
addUtxos(alreadyAdded, valueSoFar, bytesSoFar, utxosLeft.tail) addUtxos(alreadyAdded, valueSoFar, bytesSoFar, utxosLeft.tail)
} else { } else {
val newAdded = alreadyAdded.:+(nextUtxo) val newAdded = alreadyAdded.:+(nextUtxo)
val newValue = valueSoFar + nextUtxo.output.value val newValue = valueSoFar + nextUtxo.prevOut.value
val approxUtxoSize = CoinSelector.approximateUtxoSize(nextUtxo) val approxUtxoSize = CoinSelector.approximateUtxoSize(nextUtxo)
addUtxos(newAdded, addUtxos(newAdded,
@ -93,30 +93,38 @@ trait CoinSelector {
addUtxos(Vector.empty, CurrencyUnits.zero, bytesSoFar = 0L, walletUtxos) addUtxos(Vector.empty, CurrencyUnits.zero, bytesSoFar = 0L, walletUtxos)
} }
def calculateUtxoFee(utxo: SpendingInfoDb, feeRate: FeeUnit): CurrencyUnit = { def calculateUtxoFee(
utxo: CoinSelectorUtxo,
feeRate: FeeUnit): CurrencyUnit = {
val approxUtxoSize = CoinSelector.approximateUtxoSize(utxo) val approxUtxoSize = CoinSelector.approximateUtxoSize(utxo)
feeRate * approxUtxoSize feeRate * approxUtxoSize
} }
def calcEffectiveValue( def calcEffectiveValue(
utxo: SpendingInfoDb, utxo: CoinSelectorUtxo,
feeRate: FeeUnit): CurrencyUnit = { feeRate: FeeUnit): CurrencyUnit = {
val utxoFee = calculateUtxoFee(utxo, feeRate) val utxoFee = calculateUtxoFee(utxo, feeRate)
utxo.output.value - utxoFee utxo.prevOut.value - utxoFee
} }
} }
object CoinSelector extends CoinSelector { object CoinSelector extends CoinSelector {
/** Cribbed from [[https://github.com/bitcoinjs/coinselect/blob/master/utils.js]] */ /** Cribbed from [[https://github.com/bitcoinjs/coinselect/blob/master/utils.js]] */
def approximateUtxoSize(utxo: SpendingInfoDb): Long = { def approximateUtxoSize(utxo: CoinSelectorUtxo): Long = {
val inputBase = 32 + 4 + 1 + 4 val inputBase = 32 + 4 + 1 + 4
val scriptSize = utxo.redeemScriptOpt match { val scriptSize = utxo.redeemScriptOpt match {
case Some(script) => script.bytes.length case Some(script) => script.bytes.length
case None => case None =>
utxo.scriptWitnessOpt match { utxo.scriptWitnessOpt match {
case Some(script) => script.bytes.length case Some(script) => script.bytes.length
case None => 107 // PUBKEYHASH case None =>
utxo.prevOut.scriptPubKey match {
case _: NonWitnessScriptPubKey => 107 // P2PKH
case _: WitnessScriptPubKeyV0 => 107 // P2WPKH
case _: TaprootScriptPubKey => 64 // Single Schnorr signature
case _: UnassignedWitnessScriptPubKey => 0 // unknown
}
} }
} }
@ -125,10 +133,10 @@ object CoinSelector extends CoinSelector {
def selectByAlgo( def selectByAlgo(
coinSelectionAlgo: CoinSelectionAlgo, coinSelectionAlgo: CoinSelectionAlgo,
walletUtxos: Vector[SpendingInfoDb], walletUtxos: Vector[CoinSelectorUtxo],
outputs: Vector[TransactionOutput], outputs: Vector[TransactionOutput],
feeRate: FeeUnit, feeRate: FeeUnit,
longTermFeeRateOpt: Option[FeeUnit] = None): Vector[SpendingInfoDb] = longTermFeeRateOpt: Option[FeeUnit] = None): Vector[CoinSelectorUtxo] =
coinSelectionAlgo match { coinSelectionAlgo match {
case RandomSelection => case RandomSelection =>
randomSelection(walletUtxos, outputs, feeRate) randomSelection(walletUtxos, outputs, feeRate)
@ -147,7 +155,7 @@ object CoinSelector extends CoinSelector {
"longTermFeeRateOpt must be defined for LeastWaste") "longTermFeeRateOpt must be defined for LeastWaste")
} }
case SelectedUtxos(outPoints) => case SelectedUtxos(outPoints) =>
val result = walletUtxos.foldLeft(Vector.empty[SpendingInfoDb]) { val result = walletUtxos.foldLeft(Vector.empty[CoinSelectorUtxo]) {
(acc, utxo) => (acc, utxo) =>
val outPoint = (utxo.outPoint.txId, utxo.outPoint.vout.toInt) val outPoint = (utxo.outPoint.txId, utxo.outPoint.vout.toInt)
if (outPoints(outPoint)) acc :+ utxo else acc if (outPoints(outPoint)) acc :+ utxo else acc
@ -170,7 +178,7 @@ object CoinSelector extends CoinSelector {
private case class CoinSelectionResults( private case class CoinSelectionResults(
waste: CurrencyUnit, waste: CurrencyUnit,
totalSpent: CurrencyUnit, totalSpent: CurrencyUnit,
selection: Vector[SpendingInfoDb]) selection: Vector[CoinSelectorUtxo])
implicit implicit
private val coinSelectionResultsOrder: Ordering[CoinSelectionResults] = { private val coinSelectionResultsOrder: Ordering[CoinSelectionResults] = {
@ -181,11 +189,11 @@ object CoinSelector extends CoinSelector {
} }
def selectByLeastWaste( def selectByLeastWaste(
walletUtxos: Vector[SpendingInfoDb], walletUtxos: Vector[CoinSelectorUtxo],
outputs: Vector[TransactionOutput], outputs: Vector[TransactionOutput],
feeRate: FeeUnit, feeRate: FeeUnit,
longTermFeeRate: FeeUnit longTermFeeRate: FeeUnit
): Vector[SpendingInfoDb] = { ): Vector[CoinSelectorUtxo] = {
val target = outputs.map(_.value).sum val target = outputs.map(_.value).sum
val results = CoinSelectionAlgo.independentAlgos.flatMap { algo => val results = CoinSelectionAlgo.independentAlgos.flatMap { algo =>
// Skip failed selection attempts // Skip failed selection attempts
@ -207,7 +215,7 @@ object CoinSelector extends CoinSelector {
feeRate, feeRate,
longTermFeeRate) longTermFeeRate)
val totalSpent = selection.map(_.output.value).sum val totalSpent = selection.map(_.prevOut.value).sum
CoinSelectionResults(waste, totalSpent, selection) CoinSelectionResults(waste, totalSpent, selection)
}.toOption }.toOption
} }
@ -236,7 +244,7 @@ object CoinSelector extends CoinSelector {
* @return The waste * @return The waste
*/ */
def calculateSelectionWaste( def calculateSelectionWaste(
utxos: Vector[SpendingInfoDb], utxos: Vector[CoinSelectorUtxo],
changeCostOpt: Option[CurrencyUnit], changeCostOpt: Option[CurrencyUnit],
target: CurrencyUnit, target: CurrencyUnit,
feeRate: FeeUnit, feeRate: FeeUnit,

View file

@ -0,0 +1,21 @@
package org.bitcoins.core.api.wallet
import org.bitcoins.core.api.wallet.db.SpendingInfoDb
import org.bitcoins.core.protocol.script._
import org.bitcoins.core.protocol.transaction._
case class CoinSelectorUtxo(
prevOut: TransactionOutput,
outPoint: TransactionOutPoint,
redeemScriptOpt: Option[ScriptPubKey],
scriptWitnessOpt: Option[ScriptWitness])
object CoinSelectorUtxo {
def fromSpendingInfoDb(db: SpendingInfoDb): CoinSelectorUtxo = {
CoinSelectorUtxo(db.output,
db.outPoint,
db.redeemScriptOpt,
db.scriptWitnessOpt)
}
}

View file

@ -2,6 +2,7 @@ package org.bitcoins.core.api.wallet.db
import org.bitcoins.core.api.db.DbRowAutoInc import org.bitcoins.core.api.db.DbRowAutoInc
import org.bitcoins.core.api.keymanager.BIP39KeyManagerApi import org.bitcoins.core.api.keymanager.BIP39KeyManagerApi
import org.bitcoins.core.api.wallet.CoinSelectorUtxo
import org.bitcoins.core.hd._ import org.bitcoins.core.hd._
import org.bitcoins.core.protocol.script.{ import org.bitcoins.core.protocol.script.{
P2SHScriptPubKey, P2SHScriptPubKey,
@ -195,6 +196,10 @@ sealed trait SpendingInfoDb extends DbRowAutoInc[SpendingInfoDb] {
hashType hashType
) )
} }
def toCoinSelectorUtxo: CoinSelectorUtxo = {
CoinSelectorUtxo.fromSpendingInfoDb(this)
}
} }
object SpendingInfoDb { object SpendingInfoDb {

View file

@ -1,13 +1,11 @@
package org.bitcoins.wallet package org.bitcoins.wallet
import org.bitcoins.core.api.wallet.CoinSelector import org.bitcoins.core.api.wallet.{CoinSelector, CoinSelectorUtxo}
import org.bitcoins.core.api.wallet.db.{SegwitV0SpendingInfo, SpendingInfoDb}
import org.bitcoins.core.currency._ import org.bitcoins.core.currency._
import org.bitcoins.core.protocol.script.ScriptPubKey import org.bitcoins.core.protocol.script.ScriptPubKey
import org.bitcoins.core.protocol.transaction.TransactionOutput import org.bitcoins.core.protocol.transaction.TransactionOutput
import org.bitcoins.core.wallet.fee.{FeeUnit, SatoshisPerByte} import org.bitcoins.core.wallet.fee.{FeeUnit, SatoshisPerByte}
import org.bitcoins.core.wallet.utxo.TxoState import org.bitcoins.testkit.wallet.BitcoinSWalletTest
import org.bitcoins.testkit.wallet.{BitcoinSWalletTest, WalletTestUtil}
import org.bitcoins.testkitcore.Implicits._ import org.bitcoins.testkitcore.Implicits._
import org.bitcoins.testkitcore.gen.{TransactionGenerators, WitnessGenerators} import org.bitcoins.testkitcore.gen.{TransactionGenerators, WitnessGenerators}
import org.scalatest.FutureOutcome import org.scalatest.FutureOutcome
@ -17,10 +15,12 @@ class CoinSelectorTest extends BitcoinSWalletTest {
case class CoinSelectionFixture( case class CoinSelectionFixture(
output: TransactionOutput, output: TransactionOutput,
feeRate: FeeUnit, feeRate: FeeUnit,
utxo1: SpendingInfoDb, utxo1: CoinSelectorUtxo,
utxo2: SpendingInfoDb, utxo2: CoinSelectorUtxo,
utxo3: SpendingInfoDb) { utxo3: CoinSelectorUtxo) {
val utxoSet: Vector[SpendingInfoDb] = Vector(utxo1, utxo2, utxo3)
val utxoSet: Vector[CoinSelectorUtxo] = Vector(utxo1, utxo2, utxo3)
} }
override type FixtureParam = CoinSelectionFixture override type FixtureParam = CoinSelectionFixture
@ -30,35 +30,26 @@ class CoinSelectorTest extends BitcoinSWalletTest {
val feeRate = SatoshisPerByte(CurrencyUnits.zero) val feeRate = SatoshisPerByte(CurrencyUnits.zero)
val outpoint1 = TransactionGenerators.outPoint.sampleSome val outpoint1 = TransactionGenerators.outPoint.sampleSome
val utxo1 = SegwitV0SpendingInfo( val utxo1 = CoinSelectorUtxo(
state = TxoState.PendingConfirmationsReceived,
id = Some(1),
outPoint = outpoint1, outPoint = outpoint1,
output = TransactionOutput(10.sats, ScriptPubKey.empty), prevOut = TransactionOutput(10.sats, ScriptPubKey.empty),
privKeyPath = WalletTestUtil.sampleSegwitPath, scriptWitnessOpt = Some(WitnessGenerators.scriptWitness.sampleSome),
scriptWitness = WitnessGenerators.scriptWitness.sampleSome, redeemScriptOpt = None
spendingTxIdOpt = None
) )
val outPoint2 = TransactionGenerators.outPoint.sampleSome val outPoint2 = TransactionGenerators.outPoint.sampleSome
val utxo2 = SegwitV0SpendingInfo( val utxo2 = CoinSelectorUtxo(
state = TxoState.ConfirmedReceived,
id = Some(2),
outPoint = outPoint2, outPoint = outPoint2,
output = TransactionOutput(90.sats, ScriptPubKey.empty), prevOut = TransactionOutput(90.sats, ScriptPubKey.empty),
privKeyPath = WalletTestUtil.sampleSegwitPath, scriptWitnessOpt = Some(WitnessGenerators.scriptWitness.sampleSome),
scriptWitness = WitnessGenerators.scriptWitness.sampleSome, redeemScriptOpt = None
spendingTxIdOpt = None
) )
val outPoint3 = TransactionGenerators.outPoint.sampleSome val outPoint3 = TransactionGenerators.outPoint.sampleSome
val utxo3 = SegwitV0SpendingInfo( val utxo3 = CoinSelectorUtxo(
state = TxoState.ConfirmedReceived,
id = Some(3),
outPoint = outPoint3, outPoint = outPoint3,
output = TransactionOutput(20.sats, ScriptPubKey.empty), prevOut = TransactionOutput(20.sats, ScriptPubKey.empty),
privKeyPath = WalletTestUtil.sampleSegwitPath, scriptWitnessOpt = Some(WitnessGenerators.scriptWitness.sampleSome),
scriptWitness = WitnessGenerators.scriptWitness.sampleSome, redeemScriptOpt = None
spendingTxIdOpt = None
) )
test(CoinSelectionFixture(output, feeRate, utxo1, utxo2, utxo3)) test(CoinSelectionFixture(output, feeRate, utxo1, utxo2, utxo3))

View file

@ -1,6 +1,6 @@
package org.bitcoins.wallet package org.bitcoins.wallet
import org.bitcoins.core.api.wallet.{CoinSelectionAlgo, CoinSelector} import org.bitcoins.core.api.wallet._
import org.bitcoins.core.currency._ import org.bitcoins.core.currency._
import org.bitcoins.core.number.{Int32, UInt32} import org.bitcoins.core.number.{Int32, UInt32}
import org.bitcoins.core.protocol.BitcoinAddress import org.bitcoins.core.protocol.BitcoinAddress
@ -464,7 +464,10 @@ class WalletSendingTest extends BitcoinSWalletTest {
for { for {
account <- wallet.getDefaultAccount() account <- wallet.getDefaultAccount()
feeRate <- wallet.getFeeRate() feeRate <- wallet.getFeeRate()
allUtxos <- wallet.listUtxos(account.hdAccount) allUtxos <- wallet
.listUtxos(account.hdAccount)
.map(_.map(CoinSelectorUtxo.fromSpendingInfoDb))
output = TransactionOutput(amountToSend, testAddress.scriptPubKey) output = TransactionOutput(amountToSend, testAddress.scriptPubKey)
expectedUtxos = expectedUtxos =
CoinSelector.selectByAlgo(algo, allUtxos, Vector(output), feeRate) CoinSelector.selectByAlgo(algo, allUtxos, Vector(output), feeRate)

View file

@ -1,7 +1,7 @@
package org.bitcoins.wallet.internal package org.bitcoins.wallet.internal
import org.bitcoins.core.api.wallet.db.{AccountDb, SpendingInfoDb} import org.bitcoins.core.api.wallet.db.{AccountDb, SpendingInfoDb}
import org.bitcoins.core.api.wallet.{CoinSelectionAlgo, CoinSelector} import org.bitcoins.core.api.wallet._
import org.bitcoins.core.policy.Policy import org.bitcoins.core.policy.Policy
import org.bitcoins.core.protocol.transaction._ import org.bitcoins.core.protocol.transaction._
import org.bitcoins.core.wallet.builder._ import org.bitcoins.core.wallet.builder._
@ -93,6 +93,7 @@ trait FundTransactionHandling extends WalletLogger { self: Wallet =>
selectableUtxos = walletUtxos selectableUtxos = walletUtxos
.map(_._1) .map(_._1)
.filter(_.output.value > Policy.dustThreshold) .filter(_.output.value > Policy.dustThreshold)
.map(CoinSelectorUtxo.fromSpendingInfoDb)
utxos = CoinSelector.selectByAlgo( utxos = CoinSelector.selectByAlgo(
coinSelectionAlgo = coinSelectionAlgo, coinSelectionAlgo = coinSelectionAlgo,
@ -101,7 +102,8 @@ trait FundTransactionHandling extends WalletLogger { self: Wallet =>
feeRate = feeRate, feeRate = feeRate,
longTermFeeRateOpt = Some(self.walletConfig.longTermFeeRate) longTermFeeRateOpt = Some(self.walletConfig.longTermFeeRate)
) )
filtered = walletUtxos.filter(utxo => utxos.contains(utxo._1)) filtered = walletUtxos.filter(utxo =>
utxos.exists(_.outPoint == utxo._1.outPoint))
_ <- _ <-
if (markAsReserved) markUTXOsAsReserved(filtered.map(_._1)) if (markAsReserved) markUTXOsAsReserved(filtered.map(_._1))
else Future.unit else Future.unit