Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RBF #647

Merged
merged 6 commits into from
Mar 13, 2024
Merged

RBF #647

Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import io.horizontalsystems.bitcoincore.models.TransactionFilterType
import io.horizontalsystems.bitcoincore.models.TransactionInfo
import io.horizontalsystems.bitcoincore.models.UsedAddress
import io.horizontalsystems.bitcoincore.network.Network
import io.horizontalsystems.bitcoincore.rbf.ReplacementTransaction
import io.horizontalsystems.bitcoincore.rbf.ReplacementTransactionInfo
import io.horizontalsystems.bitcoincore.rbf.ReplacementType
import io.horizontalsystems.bitcoincore.storage.FullTransaction
import io.horizontalsystems.bitcoincore.storage.UnspentOutput
import io.horizontalsystems.bitcoincore.storage.UnspentOutputInfo
Expand Down Expand Up @@ -188,4 +191,30 @@ abstract class AbstractKit {
fun getRawTransaction(transactionHash: String): String? {
return bitcoinCore.getRawTransaction(transactionHash)
}

fun speedUpTransaction(transactionHash: String, minFee: Long): ReplacementTransaction {
return bitcoinCore.replacementTransaction(transactionHash, minFee, ReplacementType.SpeedUp)
}

fun cancelTransaction(transactionHash: String, minFee: Long): ReplacementTransaction {
val publicKey = bitcoinCore.receivePublicKey()
val address = bitcoinCore.address(publicKey)
return bitcoinCore.replacementTransaction(transactionHash, minFee, ReplacementType.Cancel(address, publicKey))
}

fun send(replacementTransaction: ReplacementTransaction): FullTransaction {
return bitcoinCore.send(replacementTransaction)
}

fun speedUpTransactionInfo(transactionHash: String): ReplacementTransactionInfo? {
return bitcoinCore.replacementTransactionInfo(transactionHash, ReplacementType.SpeedUp)
}

fun cancelTransactionInfo(transactionHash: String): ReplacementTransactionInfo? {
val receivePublicKey = bitcoinCore.receivePublicKey()
val address = bitcoinCore.address(receivePublicKey)
val type = ReplacementType.Cancel(address, receivePublicKey)
return bitcoinCore.replacementTransactionInfo(transactionHash, type)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import io.horizontalsystems.bitcoincore.managers.IUnspentOutputSelector
import io.horizontalsystems.bitcoincore.managers.RestoreKeyConverterChain
import io.horizontalsystems.bitcoincore.managers.SyncManager
import io.horizontalsystems.bitcoincore.managers.UnspentOutputSelectorChain
import io.horizontalsystems.bitcoincore.models.Address
import io.horizontalsystems.bitcoincore.models.BalanceInfo
import io.horizontalsystems.bitcoincore.models.BitcoinPaymentData
import io.horizontalsystems.bitcoincore.models.BitcoinSendInfo
Expand All @@ -36,6 +37,10 @@ import io.horizontalsystems.bitcoincore.network.peer.InventoryItemsHandlerChain
import io.horizontalsystems.bitcoincore.network.peer.PeerGroup
import io.horizontalsystems.bitcoincore.network.peer.PeerManager
import io.horizontalsystems.bitcoincore.network.peer.PeerTaskHandlerChain
import io.horizontalsystems.bitcoincore.rbf.ReplacementTransaction
import io.horizontalsystems.bitcoincore.rbf.ReplacementTransactionBuilder
import io.horizontalsystems.bitcoincore.rbf.ReplacementTransactionInfo
import io.horizontalsystems.bitcoincore.rbf.ReplacementType
import io.horizontalsystems.bitcoincore.storage.FullTransaction
import io.horizontalsystems.bitcoincore.storage.UnspentOutput
import io.horizontalsystems.bitcoincore.storage.UnspentOutputInfo
Expand All @@ -62,6 +67,7 @@ class BitcoinCore(
private val restoreKeyConverterChain: RestoreKeyConverterChain,
private val transactionCreator: TransactionCreator?,
private val transactionFeeCalculator: TransactionFeeCalculator?,
private val replacementTransactionBuilder: ReplacementTransactionBuilder?,
private val paymentAddressParser: PaymentAddressParser,
private val syncManager: SyncManager,
private val purpose: Purpose,
Expand Down Expand Up @@ -263,6 +269,10 @@ class BitcoinCore(
return addressConverter.convert(publicKeyManager.receivePublicKey(), purpose.scriptType).stringValue
}

fun address(publicKey: PublicKey): Address {
return addressConverter.convert(publicKey, purpose.scriptType)
}

fun usedAddresses(change: Boolean): List<UsedAddress> {
return publicKeyManager.usedExternalPublicKeys(change).map {
UsedAddress(
Expand Down Expand Up @@ -439,6 +449,25 @@ class BitcoinCore(
return dataProvider.getTransaction(hash)
}

fun replacementTransaction(transactionHash: String, minFee: Long, type: ReplacementType): ReplacementTransaction {
val replacementTransactionBuilder = this.replacementTransactionBuilder ?: throw CoreError.ReadOnlyCore

val (mutableTransaction, fullInfo, descendantTransactionHashes) =
replacementTransactionBuilder.replacementTransaction(transactionHash, minFee, type)
val info = dataProvider.transactionInfo(fullInfo)
return ReplacementTransaction(mutableTransaction, info, descendantTransactionHashes)
}

fun send(replacementTransaction: ReplacementTransaction): FullTransaction {
val transactionCreator = this.transactionCreator ?: throw CoreError.ReadOnlyCore

return transactionCreator.create(replacementTransaction.mutableTransaction)
}

fun replacementTransactionInfo(transactionHash: String, type: ReplacementType): ReplacementTransactionInfo? {
return replacementTransactionBuilder?.replacementInfo(transactionHash, type)
}

sealed class KitState {
object Synced : KitState()
class NotSynced(val exception: Throwable) : KitState()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ import io.horizontalsystems.bitcoincore.network.peer.MempoolTransactions
import io.horizontalsystems.bitcoincore.network.peer.PeerAddressManager
import io.horizontalsystems.bitcoincore.network.peer.PeerGroup
import io.horizontalsystems.bitcoincore.network.peer.PeerManager
import io.horizontalsystems.bitcoincore.rbf.ReplacementTransactionBuilder
import io.horizontalsystems.bitcoincore.serializers.BlockHeaderParser
import io.horizontalsystems.bitcoincore.transactions.BlockTransactionProcessor
import io.horizontalsystems.bitcoincore.transactions.PendingTransactionProcessor
Expand Down Expand Up @@ -443,6 +444,7 @@ class BitcoinCoreBuilder {
var transactionFeeCalculator: TransactionFeeCalculator? = null
var transactionSender: TransactionSender? = null
var transactionCreator: TransactionCreator? = null
var replacementTransactionBuilder: ReplacementTransactionBuilder? = null

if (privateWallet != null) {
val ecdsaInputSigner = EcdsaInputSigner(privateWallet, network)
Expand All @@ -462,8 +464,7 @@ class BitcoinCoreBuilder {
transactionDataSorterFactory
)
val lockTimeSetter = LockTimeSetter(storage)
val signer = TransactionSigner(ecdsaInputSigner, schnorrInputSigner)
val transactionBuilder = TransactionBuilder(recipientSetter, outputSetter, inputSetter, signer, lockTimeSetter)
val transactionBuilder = TransactionBuilder(recipientSetter, outputSetter, inputSetter, lockTimeSetter)
transactionFeeCalculator = TransactionFeeCalculator(
recipientSetter,
inputSetter,
Expand All @@ -485,8 +486,11 @@ class BitcoinCoreBuilder {
transactionSender = transactionSenderInstance

transactionSendTimer.listener = transactionSender

transactionCreator = TransactionCreator(transactionBuilder, pendingTransactionProcessor, transactionSenderInstance, bloomFilterManager)
val signer = TransactionSigner(ecdsaInputSigner, schnorrInputSigner)
transactionCreator = TransactionCreator(transactionBuilder, pendingTransactionProcessor, transactionSenderInstance, signer, bloomFilterManager)
replacementTransactionBuilder = ReplacementTransactionBuilder(
storage, transactionSizeCalculator, dustCalculator, metadataExtractor, pluginManager, unspentOutputProvider, publicKeyManager
)
}

val bitcoinCore = BitcoinCore(
Expand All @@ -497,6 +501,7 @@ class BitcoinCoreBuilder {
restoreKeyConverterChain,
transactionCreator,
transactionFeeCalculator,
replacementTransactionBuilder,
paymentAddressParser,
syncManager,
purpose,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ class BaseTransactionInfoConverter(private val pluginManager: PluginManager) {
outputsInfo.add(outputInfo)
}

val rbfEnabled = fullTransaction.inputs.any { it.input.rbfEnabled }

return TransactionInfo(
uid = transaction.uid,
transactionHash = transaction.hash.toReversedHex(),
Expand All @@ -56,7 +58,8 @@ class BaseTransactionInfoConverter(private val pluginManager: PluginManager) {
blockHeight = fullTransaction.block?.height,
timestamp = transaction.timestamp,
status = TransactionStatus.getByCode(transaction.status) ?: TransactionStatus.NEW,
conflictingTxHash = transaction.conflictingTxHash?.toReversedHex()
conflictingTxHash = transaction.conflictingTxHash?.toReversedHex(),
rbfEnabled = rbfEnabled
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import io.horizontalsystems.bitcoincore.models.BlockInfo
import io.horizontalsystems.bitcoincore.models.Transaction
import io.horizontalsystems.bitcoincore.models.TransactionFilterType
import io.horizontalsystems.bitcoincore.models.TransactionInfo
import io.horizontalsystems.bitcoincore.storage.FullTransactionInfo
import io.horizontalsystems.bitcoincore.storage.TransactionWithBlock
import io.reactivex.Single
import io.reactivex.disposables.Disposable
Expand Down Expand Up @@ -106,6 +107,10 @@ class DataProvider(

fun getSpendableUtxo() = unspentOutputProvider.getSpendableUtxo()

fun transactionInfo(fullInfo: FullTransactionInfo): TransactionInfo {
return transactionInfoConverter.transactionInfo(fullInfo)
}

private fun blockInfo(block: Block) = BlockInfo(
block.headerHash.toReversedHex(),
block.height,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ interface IStorage {

fun getTransaction(hash: ByteArray): Transaction?
fun getFullTransaction(hash: ByteArray): FullTransaction?
fun getFullTransactions(transactions: List<Transaction>): List<FullTransaction>
fun getValidOrInvalidTransaction(uid: String): Transaction?
fun getTransactionOfOutput(output: TransactionOutput): Transaction?
fun addTransaction(transaction: FullTransaction)
Expand All @@ -109,6 +110,8 @@ interface IStorage {
// InvalidTransaction

fun getInvalidTransaction(hash: ByteArray): InvalidTransaction?
fun getDescendantTransactionsFullInfo(txHash: ByteArray): List<FullTransactionInfo>
fun getDescendantTransactions(txHash: ByteArray): List<Transaction>
fun moveTransactionToInvalidTransactions(invalidTransactions: List<InvalidTransaction>)
fun moveInvalidTransactionToTransactions(invalidTransaction: InvalidTransaction, toTransactions: FullTransaction)
fun deleteAllInvalidTransactions()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ package io.horizontalsystems.bitcoincore.core

import io.horizontalsystems.bitcoincore.managers.IRestoreKeyConverter
import io.horizontalsystems.bitcoincore.models.Address
import io.horizontalsystems.bitcoincore.models.PublicKey
import io.horizontalsystems.bitcoincore.models.TransactionOutput
import io.horizontalsystems.bitcoincore.storage.FullTransaction
import io.horizontalsystems.bitcoincore.storage.InputWithPreviousOutput
import io.horizontalsystems.bitcoincore.storage.UnspentOutput
import io.horizontalsystems.bitcoincore.transactions.builder.MutableTransaction
import io.horizontalsystems.bitcoincore.transactions.scripts.Script
Expand Down Expand Up @@ -79,9 +79,16 @@ class PluginManager {
plugin.validateAddress(address)
}
}

fun incrementedSequence(inputWithPreviousOutput: InputWithPreviousOutput): Long {
val plugin = inputWithPreviousOutput.previousOutput?.pluginId?.let { plugins[it] }
val sequence = inputWithPreviousOutput.input.sequence

return plugin?.incrementSequence(sequence) ?: (sequence + 1)
}
}

interface IPlugin: IRestoreKeyConverter {
interface IPlugin : IRestoreKeyConverter {
val id: Byte

fun processOutputs(mutableTransaction: MutableTransaction, pluginData: IPluginData, skipChecking: Boolean)
Expand All @@ -90,6 +97,7 @@ interface IPlugin: IRestoreKeyConverter {
fun getInputSequence(output: TransactionOutput): Long
fun parsePluginData(output: TransactionOutput, txTimestamp: Long): IPluginOutputData
fun validateAddress(address: Address)
fun incrementSequence(sequence: Long): Long
}

interface IPluginData
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,34 +3,60 @@ package io.horizontalsystems.bitcoincore.managers
import io.horizontalsystems.bitcoincore.core.IStorage
import io.horizontalsystems.bitcoincore.core.PluginManager
import io.horizontalsystems.bitcoincore.models.BalanceInfo
import io.horizontalsystems.bitcoincore.models.Transaction
import io.horizontalsystems.bitcoincore.storage.UnspentOutput

class UnspentOutputProvider(private val storage: IStorage, private val confirmationsThreshold: Int = 6, val pluginManager: PluginManager) : IUnspentOutputProvider {
class UnspentOutputProvider(
private val storage: IStorage,
private val confirmationsThreshold: Int = 6,
val pluginManager: PluginManager
) : IUnspentOutputProvider {

override fun getSpendableUtxo(): List<UnspentOutput> {
return getConfirmedUtxo().filter {
pluginManager.isSpendable(it)
return allUtxo().filter {
pluginManager.isSpendable(it) && it.transaction.status == Transaction.Status.RELAYED
}
}

private fun getUnspendableUtxo(): List<UnspentOutput> {
return allUtxo().filter {
!pluginManager.isSpendable(it) || it.transaction.status != Transaction.Status.RELAYED
}
}

fun getBalance(): BalanceInfo {
val spendable = getSpendableUtxo().map { it.output.value }.sum()
val unspendable = getUnspendableUtxo().map { it.output.value }.sum()
val spendable = getSpendableUtxo().sumOf { it.output.value }
val unspendable = getUnspendableUtxo().sumOf { it.output.value }

return BalanceInfo(spendable, unspendable)
}

private fun getConfirmedUtxo(): List<UnspentOutput> {
// Only confirmed spendable outputs
fun getConfirmedSpendableUtxo(): List<UnspentOutput> {
val lastBlockHeight = storage.lastBlock()?.height ?: 0

return getSpendableUtxo().filter {
val block = it.block ?: return@filter false
return@filter block.height <= lastBlockHeight - confirmationsThreshold + 1
}
}

private fun allUtxo(): List<UnspentOutput> {
val unspentOutputs = storage.getUnspentOutputs()

if (confirmationsThreshold == 0) return unspentOutputs

val lastBlockHeight = storage.lastBlock()?.height ?: 0

return unspentOutputs.filter {
// If a transaction is an outgoing transaction, then it can be used
// even if it's not included in a block yet
if (it.transaction.isOutgoing) {
return@filter true
}

// If a transaction is an incoming transaction, then it can be used
// only if it's included in a block and has enough number of confirmations
val block = it.block ?: return@filter false
if (block.height <= lastBlockHeight - confirmationsThreshold + 1) {
return@filter true
Expand All @@ -39,10 +65,4 @@ class UnspentOutputProvider(private val storage: IStorage, private val confirmat
false
}
}

private fun getUnspendableUtxo(): List<UnspentOutput> {
return getConfirmedUtxo().filter {
!pluginManager.isSpendable(it)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,23 @@ open class TransactionInfo {
var timestamp: Long = 0
var status: TransactionStatus = TransactionStatus.NEW
var conflictingTxHash: String? = null

constructor(uid: String,
transactionHash: String,
transactionIndex: Int,
inputs: List<TransactionInputInfo>,
outputs: List<TransactionOutputInfo>,
amount: Long,
type: TransactionType,
fee: Long?,
blockHeight: Int?,
timestamp: Long,
status: TransactionStatus,
conflictingTxHash: String? = null) {
var rbfEnabled: Boolean = false

constructor(
uid: String,
transactionHash: String,
transactionIndex: Int,
inputs: List<TransactionInputInfo>,
outputs: List<TransactionOutputInfo>,
amount: Long,
type: TransactionType,
fee: Long?,
blockHeight: Int?,
timestamp: Long,
status: TransactionStatus,
conflictingTxHash: String? = null,
rbfEnabled: Boolean = false
) {
this.uid = uid
this.transactionHash = transactionHash
this.transactionIndex = transactionIndex
Expand All @@ -43,6 +47,7 @@ open class TransactionInfo {
this.timestamp = timestamp
this.status = status
this.conflictingTxHash = conflictingTxHash
this.rbfEnabled = rbfEnabled
}

@Throws
Expand All @@ -61,6 +66,7 @@ open class TransactionInfo {
status = TransactionStatus.getByCode(jsonObject.get("status").asInt())
?: TransactionStatus.INVALID
conflictingTxHash = jsonObject.get("conflictingTxHash")?.asString()
rbfEnabled = jsonObject.get("rbfEnabled")?.asBoolean() ?: false
}

private fun parseInputs(jsonArray: JsonArray): List<TransactionInputInfo> {
Expand Down
Loading
Loading