Skip to content

Commit

Permalink
Send P2WPKH transaction (#73)
Browse files Browse the repository at this point in the history
  • Loading branch information
tmedetbekov committed Nov 6, 2018
1 parent f46fab8 commit 39c0d09
Show file tree
Hide file tree
Showing 8 changed files with 223 additions and 145 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,10 @@ class TransactionBuilderTest {
whenever(network.addressVersion).thenReturn(111)
whenever(network.addressScriptVersion).thenReturn(196)

unspentOutputs = SelectedUnspentOutputInfo(listOf(previousTransaction.outputs[0]!!), previousTransaction.outputs[0]!!.value, fee)
unspentOutputs = SelectedUnspentOutputInfo(listOf(previousTransaction.outputs[0]!!), previousTransaction.outputs[0]!!.value, fee, false)

whenever(unspentOutputProvider.allUnspentOutputs()).thenReturn(unspentOutputs.outputs)
whenever(unspentOutputSelector.select(any(), any(), any(), any(), any())).thenReturn(unspentOutputs)
whenever(unspentOutputSelector.select(any(), any(), any(), any(), any(), any())).thenReturn(unspentOutputs)
whenever(transactionSizeCalculator.outputSize(any())).thenReturn(34)

//receive address locking script P2PKH
Expand All @@ -94,13 +94,10 @@ class TransactionBuilderTest {
assertEquals(1, transaction.inputs.size)
assertEquals(unspentOutputs.outputs[0], transaction.inputs[0]?.previousOutput)

assertEquals(2, transaction.outputs.size)
assertEquals(1, transaction.outputs.size)

assertEquals(toAddressP2PKH, transaction.outputs[0]?.address)
assertEquals(txValue.toLong(), transaction.outputs[0]?.value)

assertEquals(changePubKey.publicKeyHash, transaction.outputs[1]?.keyHash)
assertEquals(unspentOutputs.outputs[0].value - txValue.toLong() - fee, transaction.outputs[1]?.value)
}

@Test
Expand All @@ -114,14 +111,10 @@ class TransactionBuilderTest {
assertEquals(unspentOutputs.outputs[0], transaction.inputs[0]?.previousOutput)
assertNull(transaction.outputs[0]?.publicKey)

assertEquals(2, transaction.outputs.size)
assertEquals(1, transaction.outputs.size)

assertEquals(toAddressP2PKH, transaction.outputs[0]?.address)
assertEquals((txValue - fee).toLong(), transaction.outputs[0]?.value)

assertEquals(changePubKey.publicKeyHash, transaction.outputs[1]?.keyHash)
assertEquals(changePubKey, transaction.outputs[1]?.publicKey)
assertEquals(unspentOutputs.outputs[0].value - txValue, transaction.outputs[1]?.value)
}

@Test
Expand All @@ -134,13 +127,10 @@ class TransactionBuilderTest {
assertEquals(1, transaction.inputs.size)
assertEquals(unspentOutputs.outputs[0], transaction.inputs[0]?.previousOutput)

assertEquals(2, transaction.outputs.size)
assertEquals(1, transaction.outputs.size)

assertEquals(toAddressP2SH, transaction.outputs[0]?.address)
assertEquals((txValue - fee).toLong(), transaction.outputs[0]?.value)

assertEquals(changePubKey.publicKeyHash, transaction.outputs[1]?.keyHash)
assertEquals(unspentOutputs.outputs[0].value - txValue, transaction.outputs[1]?.value)
}

@Test
Expand Down Expand Up @@ -180,8 +170,8 @@ class TransactionBuilderTest {

@Test
fun fee() {
val unspentOutputs = SelectedUnspentOutputInfo(listOf(), 11_805_400, 112_800)
whenever(unspentOutputSelector.select(any(), any(), any(), any(), any())).thenReturn(unspentOutputs)
val unspentOutputs = SelectedUnspentOutputInfo(listOf(), 11_805_400, 112_800, false)
whenever(unspentOutputSelector.select(any(), any(), any(), any(), any(), any())).thenReturn(unspentOutputs)
val fee = transactionBuilder.fee(10_782_000, 600, true, toAddressP2PKH)

assertEquals(133_200, fee)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ class UnspentOutputProvider(private val realmFactory: RealmFactory) {
realmFactory.realm.use {
unspentOutputs = it.where(TransactionOutput::class.java)
.isNotNull("publicKey")
.`in`("scriptType", arrayOf(ScriptType.P2PKH, ScriptType.P2PK))
.notEqualTo("scriptType", ScriptType.UNKNOWN)
.isEmpty("inputs")
.findAll()
}
Expand Down
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
)
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ class TransactionCreator(private val realmFactory: RealmFactory,
private val peerGroup: PeerGroup,
private val addressManager: AddressManager) {

val feeRate = 60
val feeRate = 8

fun create(address: String, value: Int) {
val realm = realmFactory.realm
Expand Down
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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ class TransactionBuilder(private val addressConverter: AddressConverter,
fun fee(value: Int, feeRate: Int, senderPay: Boolean, address: String? = null): Int {
val outputType = if (address == null) ScriptType.P2PKH else addressConverter.convert(address).scriptType

val selectedOutputsInfo = unspentOutputsSelector.select(value = value, feeRate = feeRate, outputScriptType = outputType, senderPay = senderPay, unspentOutputs = unspentOutputProvider.allUnspentOutputs())
val selectedOutputsInfo = unspentOutputsSelector.select(value = value, feeRate = feeRate, outputType = outputType, senderPay = senderPay, outputs = unspentOutputProvider.allUnspentOutputs())

val feeWithChangeOutput = if (senderPay) selectedOutputsInfo.fee + transactionSizeCalculator.outputSize(scripType = ScriptType.P2PKH) * feeRate else 0

Expand All @@ -37,7 +37,14 @@ class TransactionBuilder(private val addressConverter: AddressConverter,
fun buildTransaction(value: Int, toAddress: String, feeRate: Int, senderPay: Boolean, changePubKey: PublicKey, changeScriptType: Int = ScriptType.P2PKH): Transaction {

val address = addressConverter.convert(toAddress)
val selectedOutputsInfo = unspentOutputsSelector.select(value = value, feeRate = feeRate, outputScriptType = address.scriptType, senderPay = senderPay, unspentOutputs = unspentOutputProvider.allUnspentOutputs())
val selectedOutputsInfo = unspentOutputsSelector.select(
value = value,
feeRate = feeRate,
outputType = address.scriptType,
changeType = changeScriptType,
senderPay = senderPay,
outputs = unspentOutputProvider.allUnspentOutputs()
)

val transaction = Transaction(version = 1, lockTime = 0)

Expand Down Expand Up @@ -66,7 +73,7 @@ class TransactionBuilder(private val addressConverter: AddressConverter,
})

// calculate fee and add change output if needed
check(senderPay || selectedOutputsInfo.fee < value) {
if (!senderPay && selectedOutputsInfo.fee > value) {
throw TransactionBuilderException.FeeMoreThanValue()
}

Expand All @@ -75,7 +82,7 @@ class TransactionBuilder(private val addressConverter: AddressConverter,

transaction.outputs[0]?.value = receivedValue.toLong()

if (selectedOutputsInfo.totalValue > sentValue + transactionSizeCalculator.outputSize(scripType = changeScriptType) * feeRate) {
if (selectedOutputsInfo.addChangeOutput) {
val changeAddress = addressConverter.convert(changePubKey.publicKeyHash, changeScriptType)
transaction.outputs.add(TransactionOutput().apply {
this.value = selectedOutputsInfo.totalValue - sentValue
Expand Down
Loading

0 comments on commit 39c0d09

Please sign in to comment.