-
Notifications
You must be signed in to change notification settings - Fork 68
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
f46fab8
commit 39c0d09
Showing
8 changed files
with
223 additions
and
145 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
74 changes: 49 additions & 25 deletions
74
bitcoinkit/src/main/kotlin/io/horizontalsystems/bitcoinkit/managers/UnspentOutputSelector.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,56 +1,80 @@ | ||
package io.horizontalsystems.bitcoinkit.managers | ||
|
||
import io.horizontalsystems.bitcoinkit.models.TransactionOutput | ||
import io.horizontalsystems.bitcoinkit.scripts.ScriptType | ||
import io.horizontalsystems.bitcoinkit.scripts.ScriptType.P2PKH | ||
import io.horizontalsystems.bitcoinkit.transactions.TransactionSizeCalculator | ||
|
||
class UnspentOutputSelector(private val txSizeCalculator: TransactionSizeCalculator) { | ||
class UnspentOutputSelector(private val calculator: TransactionSizeCalculator) { | ||
|
||
class EmptyUnspentOutputs : Exception() | ||
class InsufficientUnspentOutputs : Exception() | ||
|
||
fun select(value: Int, feeRate: Int, outputScriptType: Int, senderPay: Boolean, unspentOutputs: List<TransactionOutput>): SelectedUnspentOutputInfo { | ||
fun select(value: Int, feeRate: Int, outputType: Int = P2PKH, changeType: Int = P2PKH, senderPay: Boolean, outputs: List<TransactionOutput>): SelectedUnspentOutputInfo { | ||
|
||
if (unspentOutputs.isEmpty()) { | ||
if (outputs.isEmpty()) { | ||
throw EmptyUnspentOutputs() | ||
} | ||
|
||
val selected = mutableListOf<TransactionOutput>() | ||
var calculatedFee = (txSizeCalculator.emptyTxSize + txSizeCalculator.outputSize(outputScriptType)) * feeRate | ||
val dust = txSizeCalculator.inputSize(ScriptType.P2PKH) * feeRate | ||
val dust = (calculator.inputSize(changeType) + calculator.outputSize(changeType)) * feeRate | ||
|
||
// try to find 1 unspent output with exactly matching value | ||
unspentOutputs.firstOrNull { | ||
val totalFee = if (senderPay) calculatedFee + txSizeCalculator.inputSize(it.scriptType) * feeRate else 0 | ||
(value + totalFee <= it.value) && (value + totalFee + dust > it.value) //value + input fee + dust | ||
}?.let { output -> | ||
selected.add(output) | ||
calculatedFee += txSizeCalculator.inputSize(output.scriptType) * feeRate | ||
// try to find 1 unspent output with exactly matching value | ||
for (output in outputs) { | ||
val fee = calculator.transactionSize(listOf(output.scriptType), listOf(outputType)) * feeRate | ||
val totalFee = if (senderPay) fee else 0 | ||
|
||
return SelectedUnspentOutputInfo(outputs = selected, totalValue = output.value, fee = calculatedFee) | ||
if (value + totalFee <= output.value && value + totalFee + dust > output.value) { | ||
return SelectedUnspentOutputInfo( | ||
outputs = listOf(output), | ||
totalValue = output.value, | ||
fee = if (senderPay) output.value.toInt() - value else fee, | ||
addChangeOutput = false) | ||
} | ||
} | ||
|
||
// select outputs with least value until we get needed value | ||
val sortedOutputs = unspentOutputs.sortedBy { it.value } | ||
// select outputs with least value until we get needed value | ||
val sortedOutputs = outputs.sortedBy { it.value } | ||
val selectedOutputs = mutableListOf<TransactionOutput>() | ||
val selectedOutputTypes = mutableListOf<Int>() | ||
var totalValue = 0L | ||
|
||
var fee = 0 | ||
var lastCalculatedFee = 0 | ||
for (output in sortedOutputs) { | ||
if (totalValue >= value + (if (senderPay) calculatedFee else 0)) { | ||
lastCalculatedFee = calculator.transactionSize(inputs = selectedOutputTypes, outputs = listOf(outputType)) * feeRate | ||
if (senderPay) { | ||
fee = lastCalculatedFee | ||
} | ||
|
||
if (totalValue >= value + fee) { | ||
break | ||
} | ||
selected.add(output) | ||
calculatedFee += txSizeCalculator.inputSize(output.scriptType) * feeRate | ||
|
||
selectedOutputs.add(output) | ||
selectedOutputTypes.add(output.scriptType) | ||
totalValue += output.value | ||
} | ||
|
||
// if all outputs are selected and total value less than needed throw error | ||
if (totalValue < value + (if (senderPay) calculatedFee else 0)) { | ||
if (totalValue < value + fee) { | ||
throw InsufficientUnspentOutputs() | ||
} | ||
|
||
return SelectedUnspentOutputInfo(outputs = selected, totalValue = totalValue, fee = calculatedFee) | ||
} | ||
// if total selected outputs value more than value and fee for transaction with change output + change input -> add fee for change output and mark as need change address | ||
var addChangeOutput = false | ||
if (totalValue > value + lastCalculatedFee + (if (senderPay) dust else 0)) { | ||
lastCalculatedFee = calculator.transactionSize(inputs = selectedOutputTypes, outputs = listOf(outputType, changeType)) * feeRate | ||
addChangeOutput = true | ||
} else if (senderPay) { | ||
lastCalculatedFee = totalValue.toInt() - value | ||
} | ||
|
||
return SelectedUnspentOutputInfo(selectedOutputs, totalValue, lastCalculatedFee, addChangeOutput) | ||
} | ||
} | ||
|
||
data class SelectedUnspentOutputInfo(val outputs: List<TransactionOutput>, val totalValue: Long, val fee: Int) | ||
|
||
data class SelectedUnspentOutputInfo( | ||
val outputs: List<TransactionOutput>, | ||
val totalValue: Long, | ||
val fee: Int, | ||
val addChangeOutput: Boolean | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
97 changes: 63 additions & 34 deletions
97
...src/main/kotlin/io/horizontalsystems/bitcoinkit/transactions/TransactionSizeCalculator.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,53 +1,82 @@ | ||
package io.horizontalsystems.bitcoinkit.transactions | ||
|
||
import io.horizontalsystems.bitcoinkit.scripts.ScriptType | ||
import io.horizontalsystems.bitcoinkit.scripts.ScriptType.P2PK | ||
import io.horizontalsystems.bitcoinkit.scripts.ScriptType.P2PKH | ||
import io.horizontalsystems.bitcoinkit.scripts.ScriptType.P2WPKHSH | ||
|
||
class TransactionSizeCalculator { | ||
private val signatureLength = 72 + 1 // signature length + pushByte | ||
private val pubKeyLength = 33 + 1 // pubKey length + pushByte | ||
private val p2wpkhShLength = 22 + 1 // 0014<20-byte-script-hash> + pushByte | ||
|
||
companion object { | ||
private const val VERSION_SIZE = 4 | ||
private const val OUTPUT_COUNT_SIZE = 1 | ||
private const val INPUT_COUNT_SIZE = 1 | ||
private const val LOCK_TIME_SIZE = 4 | ||
|
||
private const val OUTPUT_HEX_SIZE = 32 | ||
private const val OUTPUT_INDEX_SIZE = 4 | ||
private const val SEQUENCE_SIZE = 4 | ||
private const val SCRIPT_SIZE = 1 | ||
private const val VALUE_SIZE = 8 | ||
|
||
private const val SIGNATURE_SIZE = 73 | ||
private const val PUB_KEY_SIZE = 33 | ||
private const val PUB_KEY_HASH_SIZE = 20 | ||
private const val SCRIPT_HASH_ADDRESS_SIZE = 20 | ||
|
||
private const val OP_PUSHDATA_SIZE = 1 | ||
private const val OP_CHECKSIG_SIZE = 1 | ||
private const val OP_DUP_SIZE = 1 | ||
private const val OP_HASH160_SIZE = 1 | ||
private const val OP_EQUALVERIFY_SIZE = 1 | ||
private val legacyTx = 16 + 4 + 4 + 16 // 40 Version + number of inputs + number of outputs + locktime | ||
private val legacyWitnessData = 1 // 1 Only 0x00 for legacy input | ||
private val witnessTx = legacyTx + 1 + 1 // 42 segwit marker + segwit flag | ||
private val witnessData = 1 + signatureLength + pubKeyLength // 108 Number of stack items for input + Size of stack item 0 + Stack item 0, signature + Size of stack item 1 + Stack item 1, pubkey | ||
|
||
fun outputSize(scripType: Int): Int { | ||
return 8 + 1 + getLockingScriptSize(scripType) | ||
} | ||
|
||
val emptyTxSize = VERSION_SIZE + OUTPUT_COUNT_SIZE + INPUT_COUNT_SIZE + LOCK_TIME_SIZE | ||
fun inputSize(scriptType: Int): Int { | ||
val sigLength = when (scriptType) { | ||
P2PKH -> signatureLength + pubKeyLength | ||
P2PK -> signatureLength | ||
P2WPKHSH -> p2wpkhShLength | ||
else -> 0 | ||
} | ||
|
||
fun outputSize(scripType: Int) = VALUE_SIZE + SCRIPT_SIZE + getLockingScriptSize(scripType) | ||
return 32 + 4 + 1 + sigLength + 4 // PreviousOutputHex + OutputIndex + sigLength + sigScript + sequence | ||
} | ||
|
||
fun inputSize(scriptType: Int): Int { | ||
return OUTPUT_HEX_SIZE + OUTPUT_INDEX_SIZE + SCRIPT_SIZE + getUnlockingScriptSize(scriptType) + SEQUENCE_SIZE | ||
fun transactionSize(inputs: List<Int>, outputs: List<Int>): Int { | ||
var segwit = false | ||
var inputWeight = 0 | ||
|
||
for (input in inputs) { | ||
if (isWitness(input)) { | ||
segwit = true | ||
break | ||
} | ||
} | ||
|
||
inputs.forEach { input -> | ||
inputWeight += inputSize(input) * 4 // to vbytes | ||
if (isWitness(input)) { | ||
inputWeight += witnessSize(input) | ||
} | ||
} | ||
|
||
val outputWeight = outputs.fold(0) { memo, next -> memo + outputSize(next) } * 4 // to vbytes | ||
val txWeight = if (segwit) witnessTx else legacyTx | ||
|
||
return toBytes(txWeight + inputWeight + outputWeight) | ||
} | ||
|
||
private fun getUnlockingScriptSize(scriptType: Int) = OP_PUSHDATA_SIZE + SIGNATURE_SIZE + when (scriptType) { | ||
ScriptType.P2PK -> 0 | ||
ScriptType.P2PKH -> OP_PUSHDATA_SIZE + PUB_KEY_SIZE | ||
ScriptType.P2SH -> OP_PUSHDATA_SIZE + SCRIPT_HASH_ADDRESS_SIZE | ||
else -> 0 | ||
private fun witnessSize(type: Int): Int { // in vbytes | ||
if (isWitness(type)) { | ||
return witnessData | ||
} | ||
|
||
return legacyWitnessData | ||
} | ||
|
||
private fun toBytes(fee: Int): Int { | ||
return (fee / 4) + if (fee % 4 == 0) 0 else 1 | ||
} | ||
|
||
private fun getLockingScriptSize(scriptType: Int) = when (scriptType) { | ||
ScriptType.P2PK -> OP_PUSHDATA_SIZE + PUB_KEY_SIZE + OP_CHECKSIG_SIZE | ||
ScriptType.P2PKH -> OP_DUP_SIZE + OP_HASH160_SIZE + OP_PUSHDATA_SIZE + PUB_KEY_HASH_SIZE + OP_EQUALVERIFY_SIZE + OP_CHECKSIG_SIZE | ||
ScriptType.P2SH -> 23 //todo need to change after adding p2sh addresses | ||
ScriptType.P2PK -> 35 | ||
ScriptType.P2PKH -> 25 | ||
ScriptType.P2SH -> 23 | ||
ScriptType.P2WPKH -> 22 | ||
ScriptType.P2WSH -> 34 | ||
ScriptType.P2WPKHSH -> 23 | ||
else -> 0 | ||
} | ||
|
||
private fun isWitness(type: Int): Boolean { | ||
return type in arrayOf(ScriptType.P2WPKH, ScriptType.P2WSH, ScriptType.P2WPKHSH) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.