mirror of
https://github.com/bitcoin-s/bitcoin-s.git
synced 2025-03-03 10:46:42 +01:00
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:
parent
0a127368f0
commit
c210052640
7 changed files with 95 additions and 64 deletions
|
@ -1,6 +1,6 @@
|
|||
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.currency._
|
||||
import org.bitcoins.core.hd._
|
||||
|
@ -16,9 +16,10 @@ class CoinSelectorTest extends BitcoinSUnitTest {
|
|||
|
||||
behavior of "CoinSelector"
|
||||
|
||||
val utxos: Vector[SpendingInfoDb] =
|
||||
val utxos: Vector[CoinSelectorUtxo] =
|
||||
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 changeCost: Satoshis = Satoshis.one
|
||||
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
package org.bitcoins.core.api.wallet
|
||||
|
||||
import org.bitcoins.core.api.wallet.CoinSelectionAlgo._
|
||||
import org.bitcoins.core.api.wallet.db.SpendingInfoDb
|
||||
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 scala.annotation.tailrec
|
||||
|
@ -16,9 +16,9 @@ trait CoinSelector {
|
|||
* should only be used for research purposes
|
||||
*/
|
||||
def randomSelection(
|
||||
walletUtxos: Vector[SpendingInfoDb],
|
||||
walletUtxos: Vector[CoinSelectorUtxo],
|
||||
outputs: Vector[TransactionOutput],
|
||||
feeRate: FeeUnit): Vector[SpendingInfoDb] = {
|
||||
feeRate: FeeUnit): Vector[CoinSelectorUtxo] = {
|
||||
val randomUtxos = Random.shuffle(walletUtxos)
|
||||
|
||||
accumulate(randomUtxos, outputs, feeRate)
|
||||
|
@ -28,11 +28,11 @@ trait CoinSelector {
|
|||
* below their fees. Better for high fee environments than accumulateSmallestViable.
|
||||
*/
|
||||
def accumulateLargest(
|
||||
walletUtxos: Vector[SpendingInfoDb],
|
||||
walletUtxos: Vector[CoinSelectorUtxo],
|
||||
outputs: Vector[TransactionOutput],
|
||||
feeRate: FeeUnit): Vector[SpendingInfoDb] = {
|
||||
feeRate: FeeUnit): Vector[CoinSelectorUtxo] = {
|
||||
val sortedUtxos =
|
||||
walletUtxos.sortBy(_.output.value).reverse
|
||||
walletUtxos.sortBy(_.prevOut.value).reverse
|
||||
|
||||
accumulate(sortedUtxos, outputs, feeRate)
|
||||
}
|
||||
|
@ -43,29 +43,29 @@ trait CoinSelector {
|
|||
* Has the potential privacy breach of connecting a ton of UTXOs to one address.
|
||||
*/
|
||||
def accumulateSmallestViable(
|
||||
walletUtxos: Vector[SpendingInfoDb],
|
||||
walletUtxos: Vector[CoinSelectorUtxo],
|
||||
outputs: Vector[TransactionOutput],
|
||||
feeRate: FeeUnit): Vector[SpendingInfoDb] = {
|
||||
val sortedUtxos = walletUtxos.sortBy(_.output.value)
|
||||
feeRate: FeeUnit): Vector[CoinSelectorUtxo] = {
|
||||
val sortedUtxos = walletUtxos.sortBy(_.prevOut.value)
|
||||
|
||||
accumulate(sortedUtxos, outputs, feeRate)
|
||||
}
|
||||
|
||||
/** Greedily selects from walletUtxos in order, skipping outputs with values below their fees */
|
||||
def accumulate(
|
||||
walletUtxos: Vector[SpendingInfoDb],
|
||||
walletUtxos: Vector[CoinSelectorUtxo],
|
||||
outputs: Vector[TransactionOutput],
|
||||
feeRate: FeeUnit): Vector[SpendingInfoDb] = {
|
||||
feeRate: FeeUnit): Vector[CoinSelectorUtxo] = {
|
||||
val totalValue = outputs.foldLeft(CurrencyUnits.zero) {
|
||||
case (totVal, output) => totVal + output.value
|
||||
}
|
||||
|
||||
@tailrec
|
||||
def addUtxos(
|
||||
alreadyAdded: Vector[SpendingInfoDb],
|
||||
alreadyAdded: Vector[CoinSelectorUtxo],
|
||||
valueSoFar: CurrencyUnit,
|
||||
bytesSoFar: Long,
|
||||
utxosLeft: Vector[SpendingInfoDb]): Vector[SpendingInfoDb] = {
|
||||
utxosLeft: Vector[CoinSelectorUtxo]): Vector[CoinSelectorUtxo] = {
|
||||
val fee = feeRate * bytesSoFar
|
||||
if (valueSoFar > totalValue + fee) {
|
||||
alreadyAdded
|
||||
|
@ -79,7 +79,7 @@ trait CoinSelector {
|
|||
addUtxos(alreadyAdded, valueSoFar, bytesSoFar, utxosLeft.tail)
|
||||
} else {
|
||||
val newAdded = alreadyAdded.:+(nextUtxo)
|
||||
val newValue = valueSoFar + nextUtxo.output.value
|
||||
val newValue = valueSoFar + nextUtxo.prevOut.value
|
||||
val approxUtxoSize = CoinSelector.approximateUtxoSize(nextUtxo)
|
||||
|
||||
addUtxos(newAdded,
|
||||
|
@ -93,30 +93,38 @@ trait CoinSelector {
|
|||
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)
|
||||
feeRate * approxUtxoSize
|
||||
}
|
||||
|
||||
def calcEffectiveValue(
|
||||
utxo: SpendingInfoDb,
|
||||
utxo: CoinSelectorUtxo,
|
||||
feeRate: FeeUnit): CurrencyUnit = {
|
||||
val utxoFee = calculateUtxoFee(utxo, feeRate)
|
||||
utxo.output.value - utxoFee
|
||||
utxo.prevOut.value - utxoFee
|
||||
}
|
||||
}
|
||||
|
||||
object CoinSelector extends CoinSelector {
|
||||
|
||||
/** 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 scriptSize = utxo.redeemScriptOpt match {
|
||||
case Some(script) => script.bytes.length
|
||||
case None =>
|
||||
utxo.scriptWitnessOpt match {
|
||||
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(
|
||||
coinSelectionAlgo: CoinSelectionAlgo,
|
||||
walletUtxos: Vector[SpendingInfoDb],
|
||||
walletUtxos: Vector[CoinSelectorUtxo],
|
||||
outputs: Vector[TransactionOutput],
|
||||
feeRate: FeeUnit,
|
||||
longTermFeeRateOpt: Option[FeeUnit] = None): Vector[SpendingInfoDb] =
|
||||
longTermFeeRateOpt: Option[FeeUnit] = None): Vector[CoinSelectorUtxo] =
|
||||
coinSelectionAlgo match {
|
||||
case RandomSelection =>
|
||||
randomSelection(walletUtxos, outputs, feeRate)
|
||||
|
@ -147,7 +155,7 @@ object CoinSelector extends CoinSelector {
|
|||
"longTermFeeRateOpt must be defined for LeastWaste")
|
||||
}
|
||||
case SelectedUtxos(outPoints) =>
|
||||
val result = walletUtxos.foldLeft(Vector.empty[SpendingInfoDb]) {
|
||||
val result = walletUtxos.foldLeft(Vector.empty[CoinSelectorUtxo]) {
|
||||
(acc, utxo) =>
|
||||
val outPoint = (utxo.outPoint.txId, utxo.outPoint.vout.toInt)
|
||||
if (outPoints(outPoint)) acc :+ utxo else acc
|
||||
|
@ -170,7 +178,7 @@ object CoinSelector extends CoinSelector {
|
|||
private case class CoinSelectionResults(
|
||||
waste: CurrencyUnit,
|
||||
totalSpent: CurrencyUnit,
|
||||
selection: Vector[SpendingInfoDb])
|
||||
selection: Vector[CoinSelectorUtxo])
|
||||
|
||||
implicit
|
||||
private val coinSelectionResultsOrder: Ordering[CoinSelectionResults] = {
|
||||
|
@ -181,11 +189,11 @@ object CoinSelector extends CoinSelector {
|
|||
}
|
||||
|
||||
def selectByLeastWaste(
|
||||
walletUtxos: Vector[SpendingInfoDb],
|
||||
walletUtxos: Vector[CoinSelectorUtxo],
|
||||
outputs: Vector[TransactionOutput],
|
||||
feeRate: FeeUnit,
|
||||
longTermFeeRate: FeeUnit
|
||||
): Vector[SpendingInfoDb] = {
|
||||
): Vector[CoinSelectorUtxo] = {
|
||||
val target = outputs.map(_.value).sum
|
||||
val results = CoinSelectionAlgo.independentAlgos.flatMap { algo =>
|
||||
// Skip failed selection attempts
|
||||
|
@ -207,7 +215,7 @@ object CoinSelector extends CoinSelector {
|
|||
feeRate,
|
||||
longTermFeeRate)
|
||||
|
||||
val totalSpent = selection.map(_.output.value).sum
|
||||
val totalSpent = selection.map(_.prevOut.value).sum
|
||||
CoinSelectionResults(waste, totalSpent, selection)
|
||||
}.toOption
|
||||
}
|
||||
|
@ -236,7 +244,7 @@ object CoinSelector extends CoinSelector {
|
|||
* @return The waste
|
||||
*/
|
||||
def calculateSelectionWaste(
|
||||
utxos: Vector[SpendingInfoDb],
|
||||
utxos: Vector[CoinSelectorUtxo],
|
||||
changeCostOpt: Option[CurrencyUnit],
|
||||
target: CurrencyUnit,
|
||||
feeRate: FeeUnit,
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -2,6 +2,7 @@ package org.bitcoins.core.api.wallet.db
|
|||
|
||||
import org.bitcoins.core.api.db.DbRowAutoInc
|
||||
import org.bitcoins.core.api.keymanager.BIP39KeyManagerApi
|
||||
import org.bitcoins.core.api.wallet.CoinSelectorUtxo
|
||||
import org.bitcoins.core.hd._
|
||||
import org.bitcoins.core.protocol.script.{
|
||||
P2SHScriptPubKey,
|
||||
|
@ -195,6 +196,10 @@ sealed trait SpendingInfoDb extends DbRowAutoInc[SpendingInfoDb] {
|
|||
hashType
|
||||
)
|
||||
}
|
||||
|
||||
def toCoinSelectorUtxo: CoinSelectorUtxo = {
|
||||
CoinSelectorUtxo.fromSpendingInfoDb(this)
|
||||
}
|
||||
}
|
||||
|
||||
object SpendingInfoDb {
|
||||
|
|
|
@ -1,13 +1,11 @@
|
|||
package org.bitcoins.wallet
|
||||
|
||||
import org.bitcoins.core.api.wallet.CoinSelector
|
||||
import org.bitcoins.core.api.wallet.db.{SegwitV0SpendingInfo, SpendingInfoDb}
|
||||
import org.bitcoins.core.api.wallet.{CoinSelector, CoinSelectorUtxo}
|
||||
import org.bitcoins.core.currency._
|
||||
import org.bitcoins.core.protocol.script.ScriptPubKey
|
||||
import org.bitcoins.core.protocol.transaction.TransactionOutput
|
||||
import org.bitcoins.core.wallet.fee.{FeeUnit, SatoshisPerByte}
|
||||
import org.bitcoins.core.wallet.utxo.TxoState
|
||||
import org.bitcoins.testkit.wallet.{BitcoinSWalletTest, WalletTestUtil}
|
||||
import org.bitcoins.testkit.wallet.BitcoinSWalletTest
|
||||
import org.bitcoins.testkitcore.Implicits._
|
||||
import org.bitcoins.testkitcore.gen.{TransactionGenerators, WitnessGenerators}
|
||||
import org.scalatest.FutureOutcome
|
||||
|
@ -17,10 +15,12 @@ class CoinSelectorTest extends BitcoinSWalletTest {
|
|||
case class CoinSelectionFixture(
|
||||
output: TransactionOutput,
|
||||
feeRate: FeeUnit,
|
||||
utxo1: SpendingInfoDb,
|
||||
utxo2: SpendingInfoDb,
|
||||
utxo3: SpendingInfoDb) {
|
||||
val utxoSet: Vector[SpendingInfoDb] = Vector(utxo1, utxo2, utxo3)
|
||||
utxo1: CoinSelectorUtxo,
|
||||
utxo2: CoinSelectorUtxo,
|
||||
utxo3: CoinSelectorUtxo) {
|
||||
|
||||
val utxoSet: Vector[CoinSelectorUtxo] = Vector(utxo1, utxo2, utxo3)
|
||||
|
||||
}
|
||||
|
||||
override type FixtureParam = CoinSelectionFixture
|
||||
|
@ -30,35 +30,26 @@ class CoinSelectorTest extends BitcoinSWalletTest {
|
|||
val feeRate = SatoshisPerByte(CurrencyUnits.zero)
|
||||
|
||||
val outpoint1 = TransactionGenerators.outPoint.sampleSome
|
||||
val utxo1 = SegwitV0SpendingInfo(
|
||||
state = TxoState.PendingConfirmationsReceived,
|
||||
id = Some(1),
|
||||
val utxo1 = CoinSelectorUtxo(
|
||||
outPoint = outpoint1,
|
||||
output = TransactionOutput(10.sats, ScriptPubKey.empty),
|
||||
privKeyPath = WalletTestUtil.sampleSegwitPath,
|
||||
scriptWitness = WitnessGenerators.scriptWitness.sampleSome,
|
||||
spendingTxIdOpt = None
|
||||
prevOut = TransactionOutput(10.sats, ScriptPubKey.empty),
|
||||
scriptWitnessOpt = Some(WitnessGenerators.scriptWitness.sampleSome),
|
||||
redeemScriptOpt = None
|
||||
)
|
||||
val outPoint2 = TransactionGenerators.outPoint.sampleSome
|
||||
val utxo2 = SegwitV0SpendingInfo(
|
||||
state = TxoState.ConfirmedReceived,
|
||||
id = Some(2),
|
||||
val utxo2 = CoinSelectorUtxo(
|
||||
outPoint = outPoint2,
|
||||
output = TransactionOutput(90.sats, ScriptPubKey.empty),
|
||||
privKeyPath = WalletTestUtil.sampleSegwitPath,
|
||||
scriptWitness = WitnessGenerators.scriptWitness.sampleSome,
|
||||
spendingTxIdOpt = None
|
||||
prevOut = TransactionOutput(90.sats, ScriptPubKey.empty),
|
||||
scriptWitnessOpt = Some(WitnessGenerators.scriptWitness.sampleSome),
|
||||
redeemScriptOpt = None
|
||||
)
|
||||
|
||||
val outPoint3 = TransactionGenerators.outPoint.sampleSome
|
||||
val utxo3 = SegwitV0SpendingInfo(
|
||||
state = TxoState.ConfirmedReceived,
|
||||
id = Some(3),
|
||||
val utxo3 = CoinSelectorUtxo(
|
||||
outPoint = outPoint3,
|
||||
output = TransactionOutput(20.sats, ScriptPubKey.empty),
|
||||
privKeyPath = WalletTestUtil.sampleSegwitPath,
|
||||
scriptWitness = WitnessGenerators.scriptWitness.sampleSome,
|
||||
spendingTxIdOpt = None
|
||||
prevOut = TransactionOutput(20.sats, ScriptPubKey.empty),
|
||||
scriptWitnessOpt = Some(WitnessGenerators.scriptWitness.sampleSome),
|
||||
redeemScriptOpt = None
|
||||
)
|
||||
|
||||
test(CoinSelectionFixture(output, feeRate, utxo1, utxo2, utxo3))
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
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.number.{Int32, UInt32}
|
||||
import org.bitcoins.core.protocol.BitcoinAddress
|
||||
|
@ -464,7 +464,10 @@ class WalletSendingTest extends BitcoinSWalletTest {
|
|||
for {
|
||||
account <- wallet.getDefaultAccount()
|
||||
feeRate <- wallet.getFeeRate()
|
||||
allUtxos <- wallet.listUtxos(account.hdAccount)
|
||||
allUtxos <- wallet
|
||||
.listUtxos(account.hdAccount)
|
||||
.map(_.map(CoinSelectorUtxo.fromSpendingInfoDb))
|
||||
|
||||
output = TransactionOutput(amountToSend, testAddress.scriptPubKey)
|
||||
expectedUtxos =
|
||||
CoinSelector.selectByAlgo(algo, allUtxos, Vector(output), feeRate)
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package org.bitcoins.wallet.internal
|
||||
|
||||
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.protocol.transaction._
|
||||
import org.bitcoins.core.wallet.builder._
|
||||
|
@ -93,6 +93,7 @@ trait FundTransactionHandling extends WalletLogger { self: Wallet =>
|
|||
selectableUtxos = walletUtxos
|
||||
.map(_._1)
|
||||
.filter(_.output.value > Policy.dustThreshold)
|
||||
.map(CoinSelectorUtxo.fromSpendingInfoDb)
|
||||
|
||||
utxos = CoinSelector.selectByAlgo(
|
||||
coinSelectionAlgo = coinSelectionAlgo,
|
||||
|
@ -101,7 +102,8 @@ trait FundTransactionHandling extends WalletLogger { self: Wallet =>
|
|||
feeRate = feeRate,
|
||||
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))
|
||||
else Future.unit
|
||||
|
|
Loading…
Add table
Reference in a new issue