From 7802b34f212b24db1e52a84a4a8173606974ada3 Mon Sep 17 00:00:00 2001 From: vladmelnyk Date: Fri, 21 Dec 2018 20:44:08 +0200 Subject: [PATCH] Fix bugs and update tests (#2) * result now returns accumulated commission and selected utxo list * check if total utxo amount is enough * Fix bug in improve algorithm and refactor tests * add test case --- build.gradle | 2 +- .../BtcCoinSelectionProvider.kt | 14 +++--- .../BtcCoinSelectionProviderTest.kt | 50 ++++++------------- 3 files changed, 24 insertions(+), 42 deletions(-) diff --git a/build.gradle b/build.gradle index a22fb44..4426f8d 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,5 @@ group = 'com.coinselection' -version = '0.0.2' +version = '0.0.3' buildscript { ext.kotlin_version = '1.3.11' ext.spring_boot_version = '2.0.6.RELEASE' diff --git a/src/main/kotlin/com.coinselection/BtcCoinSelectionProvider.kt b/src/main/kotlin/com.coinselection/BtcCoinSelectionProvider.kt index df47b1f..ff3b6fa 100644 --- a/src/main/kotlin/com.coinselection/BtcCoinSelectionProvider.kt +++ b/src/main/kotlin/com.coinselection/BtcCoinSelectionProvider.kt @@ -13,7 +13,7 @@ class BtcCoinSelectionProvider : CoinSelectionProvider { val cumulativeSum = selectedUtxoListSumAndFee.second val cumulativeFee = selectedUtxoListSumAndFee.third val improvedUtxoList = if (selectedUtxoList != null) { - improve(utxoList.subtract(selectedUtxoList).toList(), cumulativeSum, cumulativeFee, targetValue, feeRatePerByte, maxNumberOfInputs, + improve(utxoList.subtract(selectedUtxoList).toList(), cumulativeSum, cumulativeFee, targetValue, feeRatePerByte, maxNumberOfInputs - selectedUtxoList.size, inputSize) } else { listOf() @@ -44,6 +44,7 @@ class BtcCoinSelectionProvider : CoinSelectionProvider { .asSequence() .sortedByDescending { it.amount } .takeWhile { cumulativeSum.get() < targetValue + cumulativeFee.get() } + .take(maxNumberOfInputs) .onEach { append(atomicReference = cumulativeSum, with = it.amount) } .onEach { append(atomicReference = cumulativeFee, with = costPerInput) } .toList() @@ -60,18 +61,17 @@ class BtcCoinSelectionProvider : CoinSelectionProvider { val costPerInput = inputSize.toBigDecimal() * feeRatePerByte val maxTargetValue = BigDecimal(3) * targetValue val optimalTargetValue = BigDecimal(2) * targetValue - val delta = AtomicReference((currentCumulativeSum.get() - (optimalTargetValue + currentCumulativeFee.get())).abs()) - val improvedUtxoList = remainingUtxoList + val delta = AtomicReference(BigDecimal.valueOf(Long.MAX_VALUE)) + return remainingUtxoList .shuffled() .asSequence() - .takeWhile { currentCumulativeSum.get() < maxTargetValue + currentCumulativeFee.get() } .take(maxNumOfInputs) + .takeWhile { currentCumulativeSum.get() < maxTargetValue + currentCumulativeFee.get() } + .onEach { delta.getAndSet((currentCumulativeSum.get() - (optimalTargetValue + currentCumulativeFee.get())).abs()) } + .takeWhile { (currentCumulativeSum.get() + it.amount - (optimalTargetValue + currentCumulativeFee.get() + costPerInput)).abs() < delta.get() } .onEach { append(atomicReference = currentCumulativeSum, with = it.amount) } .onEach { append(atomicReference = currentCumulativeFee, with = costPerInput) } - .takeWhile { (currentCumulativeSum.get() - (optimalTargetValue + currentCumulativeFee.get())).abs() < delta.get() } - .onEach { delta.getAndSet((currentCumulativeSum.get() - (optimalTargetValue + currentCumulativeFee.get())).abs()) } .toList() - return improvedUtxoList } private fun append(atomicReference: AtomicReference, with: BigDecimal?): BigDecimal { diff --git a/src/test/kotlin/com/coinselection/BtcCoinSelectionProviderTest.kt b/src/test/kotlin/com/coinselection/BtcCoinSelectionProviderTest.kt index b587170..a226665 100644 --- a/src/test/kotlin/com/coinselection/BtcCoinSelectionProviderTest.kt +++ b/src/test/kotlin/com/coinselection/BtcCoinSelectionProviderTest.kt @@ -7,7 +7,7 @@ import org.junit.jupiter.api.Test import java.math.BigDecimal import java.util.* -private const val KB = 1000L +private val KB = 1000.toBigDecimal() class BtcCoinSelectionProviderTest { @@ -16,7 +16,7 @@ class BtcCoinSelectionProviderTest { // 3. complex scenario: how does the UTXO pool behave over time and what is the average cost per transaction? private val coinSelectionProvider: CoinSelectionProvider = BtcCoinSelectionProvider() - private val smartFee = BigDecimal(0.00000005) + private val smartFeePerByte = BigDecimal.ONE.movePointLeft(8) private val random = Random() @RepeatedTest(100) @@ -25,31 +25,14 @@ class BtcCoinSelectionProviderTest { val rangeMin = 0 val rangeMax = 1 val utxoList = (1..1000).map { rangeMin + (rangeMax - rangeMin) * random.nextDouble() }.map { createUnspentOutput(it) } - val coinSelectionResult = coinSelectionProvider.provide(utxoList, targetValue, smartFee) + val coinSelectionResult = coinSelectionProvider.provide(utxoList, targetValue, smartFeePerByte) val sum = coinSelectionResult.selectedUtxos?.sumByBigDecimal { it.amount } val count = coinSelectionResult.selectedUtxos?.size - val feeSimple = calculateTransactionFee(count!!, 2, smartFee) - val feeCalculated = coinSelectionResult.totalFee.movePointLeft(8) + val feeSimple = calculateTransactionFee(count!!, 2, smartFeePerByte) + val feeCalculated = coinSelectionResult.totalFee Assertions.assertTrue(sum!! > targetValue + feeCalculated) - Assertions.assertTrue(feeSimple == feeCalculated.toLong()) - } - - @Test - fun `should fallback to largest first`() { - val targetValue = BigDecimal(5) - val rangeMin = 0 - val rangeMax = 1 - val maxNumOfInputs = 3 - val utxoList = (1..1000).map { rangeMin + (rangeMax - rangeMin) * random.nextDouble() }.map { createUnspentOutput(it) } - val coinSelectionResult = coinSelectionProvider.provide(utxoList, targetValue, smartFee, maxNumberOfInputs = maxNumOfInputs) - val sum = coinSelectionResult.selectedUtxos?.sumByBigDecimal { it.amount } - val count = coinSelectionResult.selectedUtxos?.size - val feeSimple = calculateTransactionFee(count!!, 2, smartFee) - val feeCalculated = coinSelectionResult.totalFee.movePointLeft(8) - - Assertions.assertTrue(sum!! > targetValue + feeCalculated) - Assertions.assertTrue(feeSimple == feeCalculated.toLong()) - Assertions.assertTrue(coinSelectionResult.selectedUtxos!!.contains(utxoList.asSequence().sortedByDescending { it.amount }.first())) + Assertions.assertTrue(sum < targetValue * 3.toBigDecimal() + feeCalculated) + Assertions.assertTrue(feeSimple == feeCalculated) } @Test @@ -57,16 +40,16 @@ class BtcCoinSelectionProviderTest { val targetValue = BigDecimal(3) val rangeMin = 1.1 val rangeMax = 1.2 - val maxNumOfInputs = 3 + val maxNumOfInputs = 6 val utxoList = (1..1000).map { rangeMin + (rangeMax - rangeMin) * random.nextDouble() }.map { createUnspentOutput(it) } - val coinSelectionResult = coinSelectionProvider.provide(utxoList, targetValue, smartFee, maxNumberOfInputs = maxNumOfInputs) + val coinSelectionResult = coinSelectionProvider.provide(utxoList, targetValue, smartFeePerByte, maxNumberOfInputs = maxNumOfInputs) val sum = coinSelectionResult.selectedUtxos?.sumByBigDecimal { it.amount } val count = coinSelectionResult.selectedUtxos?.size - val feeSimple = calculateTransactionFee(count!!, 2, smartFee) - val feeCalculated = coinSelectionResult.totalFee.movePointLeft(8) + val feeSimple = calculateTransactionFee(count!!, 2, smartFeePerByte) + val feeCalculated = coinSelectionResult.totalFee Assertions.assertTrue(sum!! > targetValue + feeCalculated) - Assertions.assertTrue(feeSimple == feeCalculated.toLong()) - Assertions.assertSame(maxNumOfInputs * 2 - 1, coinSelectionResult.selectedUtxos!!.size) + Assertions.assertTrue(feeSimple == feeCalculated) + Assertions.assertSame(maxNumOfInputs, coinSelectionResult.selectedUtxos!!.size) } @Test @@ -75,7 +58,7 @@ class BtcCoinSelectionProviderTest { val rangeMin = 1.1 val rangeMax = 1.2 val utxoList = (1..50).map { rangeMin + (rangeMax - rangeMin) * random.nextDouble() }.map { createUnspentOutput(it) } - val coinSelectionResult = coinSelectionProvider.provide(utxoList, targetValue, smartFee) + val coinSelectionResult = coinSelectionProvider.provide(utxoList, targetValue, smartFeePerByte) Assertions.assertNull(coinSelectionResult.selectedUtxos) } @@ -87,10 +70,9 @@ class BtcCoinSelectionProviderTest { return this.fold(BigDecimal.ZERO) { acc, e -> acc + transform.invoke(e) } } - private fun calculateTransactionFee(inputsCount: Int, outputsCount: Int, smartFeePerKB: BigDecimal): Long { + private fun calculateTransactionFee(inputsCount: Int, outputsCount: Int, smartFeePerByte: BigDecimal): BigDecimal { val size = inputsCount * 91 + outputsCount * 32 + 11 - val smartFeePerByte = smartFeePerKB.div(BigDecimal(KB)) - return smartFeePerByte.movePointLeft(8).toLong() * size + return (smartFeePerByte * size.toBigDecimal()) } }