diff --git a/bitcoincore/src/main/kotlin/io/horizontalsystems/bitcoincore/BitcoinCoreBuilder.kt b/bitcoincore/src/main/kotlin/io/horizontalsystems/bitcoincore/BitcoinCoreBuilder.kt index 86b2aa2e..da4ee20c 100644 --- a/bitcoincore/src/main/kotlin/io/horizontalsystems/bitcoincore/BitcoinCoreBuilder.kt +++ b/bitcoincore/src/main/kotlin/io/horizontalsystems/bitcoincore/BitcoinCoreBuilder.kt @@ -491,7 +491,7 @@ class BitcoinCoreBuilder { val signer = TransactionSigner(ecdsaInputSigner, schnorrInputSigner) transactionCreator = TransactionCreator(transactionBuilder, pendingTransactionProcessor, transactionSenderInstance, signer, bloomFilterManager) replacementTransactionBuilder = ReplacementTransactionBuilder( - storage, transactionSizeCalculator, dustCalculator, metadataExtractor, pluginManager, unspentOutputProvider, publicKeyManager + storage, transactionSizeCalculator, dustCalculator, metadataExtractor, pluginManager, unspentOutputProvider, publicKeyManager, conflictsResolver ) } diff --git a/bitcoincore/src/main/kotlin/io/horizontalsystems/bitcoincore/models/TransactionInfo.kt b/bitcoincore/src/main/kotlin/io/horizontalsystems/bitcoincore/models/TransactionInfo.kt index 91995532..6b4c014f 100644 --- a/bitcoincore/src/main/kotlin/io/horizontalsystems/bitcoincore/models/TransactionInfo.kt +++ b/bitcoincore/src/main/kotlin/io/horizontalsystems/bitcoincore/models/TransactionInfo.kt @@ -20,6 +20,9 @@ open class TransactionInfo { var conflictingTxHash: String? = null var rbfEnabled: Boolean = false + val replaceable: Boolean + get() = rbfEnabled && blockHeight == null && conflictingTxHash == null + constructor( uid: String, transactionHash: String, diff --git a/bitcoincore/src/main/kotlin/io/horizontalsystems/bitcoincore/rbf/ReplacementTransactionBuilder.kt b/bitcoincore/src/main/kotlin/io/horizontalsystems/bitcoincore/rbf/ReplacementTransactionBuilder.kt index 34c7378c..4cca8a22 100644 --- a/bitcoincore/src/main/kotlin/io/horizontalsystems/bitcoincore/rbf/ReplacementTransactionBuilder.kt +++ b/bitcoincore/src/main/kotlin/io/horizontalsystems/bitcoincore/rbf/ReplacementTransactionBuilder.kt @@ -17,6 +17,7 @@ import io.horizontalsystems.bitcoincore.storage.FullTransactionInfo import io.horizontalsystems.bitcoincore.storage.InputToSign import io.horizontalsystems.bitcoincore.storage.InputWithPreviousOutput import io.horizontalsystems.bitcoincore.storage.UnspentOutput +import io.horizontalsystems.bitcoincore.transactions.TransactionConflictsResolver import io.horizontalsystems.bitcoincore.transactions.TransactionSizeCalculator import io.horizontalsystems.bitcoincore.transactions.builder.MutableTransaction import io.horizontalsystems.bitcoincore.transactions.extractors.TransactionMetadataExtractor @@ -31,6 +32,7 @@ class ReplacementTransactionBuilder( private val pluginManager: PluginManager, private val unspentOutputProvider: UnspentOutputProvider, private val publicKeyManager: IPublicKeyManager, + private val conflictsResolver: TransactionConflictsResolver ) { private fun replacementTransaction( @@ -279,7 +281,13 @@ class ReplacementTransactionBuilder( val descendantTransactions = storage.getDescendantTransactionsFullInfo(transactionHash.toReversedByteArray()) val absoluteFee = descendantTransactions.sumOf { it.metadata.fee ?: 0 } - check(descendantTransactions.all { it.header.conflictingTxHash == null }) { throw BuildError.InvalidTransaction("Already replaced") } + check( + descendantTransactions.all { it.header.conflictingTxHash == null } && + !conflictsResolver.isTransactionReplaced(originalFullInfo.fullTransaction) + ) { + throw BuildError.InvalidTransaction("Already replaced") + } + check(absoluteFee <= minFee) { throw BuildError.FeeTooLow } val mutableTransaction = when (type) { @@ -329,6 +337,13 @@ class ReplacementTransactionBuilder( val descendantTransactions = storage.getDescendantTransactionsFullInfo(transactionHash.toReversedByteArray()) val absoluteFee = descendantTransactions.sumOf { it.metadata.fee ?: 0 } + check( + descendantTransactions.all { it.header.conflictingTxHash == null } && + !conflictsResolver.isTransactionReplaced(originalFullInfo.fullTransaction) + ) { + return null + } + val replacementTxMinSize: Long val removableOutputsValue: Long diff --git a/bitcoincore/src/main/kotlin/io/horizontalsystems/bitcoincore/storage/DataObjects.kt b/bitcoincore/src/main/kotlin/io/horizontalsystems/bitcoincore/storage/DataObjects.kt index 7ed3ab62..2cb02b1b 100644 --- a/bitcoincore/src/main/kotlin/io/horizontalsystems/bitcoincore/storage/DataObjects.kt +++ b/bitcoincore/src/main/kotlin/io/horizontalsystems/bitcoincore/storage/DataObjects.kt @@ -104,8 +104,11 @@ class FullTransactionInfo( val rawTransaction: String get() { - val fullTransaction = FullTransaction(header, inputs.map { it.input }, outputs) return TransactionSerializer.serialize(fullTransaction).toHexString() } + + val fullTransaction: FullTransaction + get() = FullTransaction(header, inputs.map { it.input }, outputs) + } diff --git a/bitcoincore/src/main/kotlin/io/horizontalsystems/bitcoincore/transactions/TransactionConflictsResolver.kt b/bitcoincore/src/main/kotlin/io/horizontalsystems/bitcoincore/transactions/TransactionConflictsResolver.kt index 47b3279a..a3fce4ff 100644 --- a/bitcoincore/src/main/kotlin/io/horizontalsystems/bitcoincore/transactions/TransactionConflictsResolver.kt +++ b/bitcoincore/src/main/kotlin/io/horizontalsystems/bitcoincore/transactions/TransactionConflictsResolver.kt @@ -65,6 +65,20 @@ class TransactionConflictsResolver(private val storage: IStorage) { } } + // Checks if the transactions has a conflicting input with higher sequence + fun isTransactionReplaced(transaction: FullTransaction): Boolean { + val conflictingTransactions = getConflictingTransactionsForTransaction(transaction) + + if (conflictingTransactions.isEmpty() || conflictingTransactions.any { it.blockHash == null }) { + return false + } + + val conflictingFullTransactions = storage.getFullTransactions(conflictingTransactions) + + return conflictingFullTransactions + .any { existingHasHigherSequence(mempoolTransaction = transaction, existingTransaction = it) } + } + private fun getConflictingTransactionsForTransaction(transaction: FullTransaction): List { return transaction.inputs.mapNotNull { input -> val conflictingTxHash = storage.getTransactionInput(input.previousOutputTxHash, input.previousOutputIndex)?.transactionHash