trustwallet/wallet-core

BTC RBF issue - did not use the original utxo

Opened this issue · 2 comments

Describe the bug
The TrustWallet library chooses the best UTXO with bigger amount to spend. The issue happens when we want to replace a transaction. Example, the original input utxos are: [utxo1 (1041 sats), utxo2 (2084 sats)]
and if we want to replace the transaction, and the original utxos are not enough to cover the new fee, we add another utxo [utxo3 (5213 sats)]
When we send the original utxos + the new utxo to replace the transaction, the TW library chose the new utxo and disregard the original utxos. Which means, the app ended up creating a new transaction, not replacing the new one.

To Reproduce
BTC-1 sendAll amount to BTC-2 with [utxo1 (1041 sats), utxo2 (2084 sats)], but set the sequence to 4294967293.
I want to replace, but there's no more balance, so I transfer more funds (5213 sats) to BTC-1.
After I received the new funds, replace the same transaction
send the original UTXOs + additional inputUTXO
[utxo1 (1041 sats), utxo2 (2084 sats), utxo3 (5213 sats)]

Result:
The "replacement" transaction only used utxo3 (5213 sats) -- essentially creating a new transaction and not replacing the old one.

Expected behavior
Is there a way we can force the library to spend the original UTXO first to make sure the tx gets replaced.

Original Transaction:
[txId=a874b98b8b634e18739db549090a175455cd6ebed91a614540b50ce12e4285eb, txRaw=01000000028ab1d8fd56f7de55968624656ee8da22d0f5408e0f36fb144f0ce0106b6b1460000000006a47304402205976722a4cab5d00a720a179f985e95ed5eac6ccf9c664251529f8d1e0b1f76c02207605358cc6fb7bdc82d6e79741aaca76745104630152fe8fbd19e40da2fc78f5012103f854e8b00e451f78360744b72997b0ba44f1bbd9517c75d6857793f5ecabf67afdffffff508b85017432e126c79f1565186e78ed1a3fc7196f2e6ba66ae1d9ffe99aa651000000006b4830450221009f7cc41faf35402fd055ce3d95ce6e9357a966f320d6381f428f472e2ef5b27f02206874c622020551b28214f71c7f24ff712cd2dbfc76143cd9bb4ecfcb80ef2e41012103f854e8b00e451f78360744b72997b0ba44f1bbd9517c75d6857793f5ecabf67afdffffff0139080000000000001976a914f0c61800d9f659449fb00eeaba091bcd5ded578888ac00000000, blockchain=BTC]

Replaced Transaction:
[txId=b1a56c835b1fcf5bb560c191d5a161e2c72c6dd576be98de42ed0a0e58bfa416, txRaw=0100000001d2c9bb30e0365aca5a3c9a3293fee75f37d0c8f3f9b4c9e5b1cc4d52167ba106000000006b4830450221008dcbfd9875953b6c2e35c06acf6876988fba87dd71188d50f49f6784b2661d990220422f94458643fe596da9dfa1c6c0c6b6be280855d35ab28242420575fad9035e012103f854e8b00e451f78360744b72997b0ba44f1bbd9517c75d6857793f5ecabf67afdffffff0239080000000000001976a914f0c61800d9f659449fb00eeaba091bcd5ded578888ac7e090000000000001976a914c125b6c127ca853c5d4a73155f1d4ce525fbd55e88ac00000000, blockchain=BTC]

Screenshots
If applicable, add screenshots to help explain your problem.

Additional context
Add any other context about the problem here.

Hi @jenbitcoin, you can have more control on UTXOs with BitcoinV2 protocol now.
Here's a BRC20 transaction example, but you can adopt for P2TR, P2WPKH, P2PKH transfer:

@Test
fun testSignBrc20Commit() {
// Successfully broadcasted: https://www.blockchain.com/explorer/transactions/btc/797d17d47ae66e598341f9dfdea020b04d4017dcf9cc33f0e51f7a6082171fb1
val privateKeyData = (Numeric.hexStringToByteArray("e253373989199da27c48680e3a3fc0f648d50f9a727ef17a7fe6a4dc3b159129"))
val dustSatoshis = 546.toLong()
val txId = Numeric.hexStringToByteArray("8ec895b4d30adb01e38471ca1019bfc8c3e5fbd1f28d9e7b5653260d89989008").reversedArray()
val privateKey = PrivateKey(privateKeyData)
val publicKey = ByteString.copyFrom(privateKey.getPublicKeySecp256k1(true).data())
val utxo0 = BitcoinV2.Input.newBuilder()
.setOutPoint(BitcoinV2.OutPoint.newBuilder().apply {
hash = ByteString.copyFrom(txId)
vout = 1
})
.setValue(26_400)
.setSighashType(BitcoinSigHashType.ALL.value())
.setScriptBuilder(BitcoinV2.Input.InputBuilder.newBuilder().apply {
p2Wpkh = BitcoinV2.PublicKeyOrHash.newBuilder().setPubkey(publicKey).build()
})
val out0 = BitcoinV2.Output.newBuilder()
.setValue(7_000)
.setBuilder(BitcoinV2.Output.OutputBuilder.newBuilder().apply {
brc20Inscribe = BitcoinV2.Output.OutputBrc20Inscription.newBuilder().apply {
inscribeTo = publicKey
ticker = "oadf"
transferAmount = "20"
}.build()
})
val changeOutput = BitcoinV2.Output.newBuilder()
.setValue(16_400)
.setBuilder(BitcoinV2.Output.OutputBuilder.newBuilder().apply {
p2Wpkh = BitcoinV2.PublicKeyOrHash.newBuilder().setPubkey(publicKey).build()
})
val builder = BitcoinV2.TransactionBuilder.newBuilder()
.setVersion(BitcoinV2.TransactionVersion.V2)
.addInputs(utxo0)
.addOutputs(out0)
.addOutputs(changeOutput)
.setInputSelector(BitcoinV2.InputSelector.UseAll)
.setFixedDustThreshold(dustSatoshis)
val signingInput = BitcoinV2.SigningInput.newBuilder()
.setBuilder(builder)
.addPrivateKeys(ByteString.copyFrom(privateKeyData))
.setChainInfo(BitcoinV2.ChainInfo.newBuilder().apply {
p2PkhPrefix = 0
p2ShPrefix = 5
})
.setDangerousUseFixedSchnorrRng(true)
.build()
val legacySigningInput = Bitcoin.SigningInput.newBuilder().apply {
signingV2 = signingInput
}
val output = AnySigner.sign(legacySigningInput.build(), BITCOIN, SigningOutput.parser())
assertEquals(output.error, SigningError.OK)
assertEquals(output.signingResultV2.error, SigningError.OK)
assertEquals(Numeric.toHexString(output.signingResultV2.encoded.toByteArray()), "0x02000000000101089098890d2653567b9e8df2d1fbe5c3c8bf1910ca7184e301db0ad3b495c88e0100000000ffffffff02581b000000000000225120e8b706a97732e705e22ae7710703e7f589ed13c636324461afa443016134cc051040000000000000160014e311b8d6ddff856ce8e9a4e03bc6d4fe5050a83d02483045022100a44aa28446a9a886b378a4a65e32ad9a3108870bd725dc6105160bed4f317097022069e9de36422e4ce2e42b39884aa5f626f8f94194d1013007d5a1ea9220a06dce0121030f209b6ada5edb42c77fd2bc64ad650ae38314c8f451f3e36d80bc8e26f132cb00000000")
assertEquals(Numeric.toHexString(output.signingResultV2.txid.toByteArray()), "0x797d17d47ae66e598341f9dfdea020b04d4017dcf9cc33f0e51f7a6082171fb1")
}

The thing is that you can choose the UTXO selection pattern via BitcoinV2.InputSelector


It can be:

  1. BitcoinV2.InputSelector.SelectAscending - Select enough inputs in an ascending order to cover the outputs and fee.
  2. BitcoinV2.InputSelector.SelectDescending - Select enough inputs in an descending order to cover the outputs and fee.
  3. BitcoinV2.InputSelector.InOrder - Select enough inputs in the given order to cover the outputs and fee.
  4. BitcoinV2.InputSelector.UseAll - Use all the given inputs.

You can sort the UTXOs as you wish, and then use BitcoinV2.InputSelector.InOrder

thank you, I will try it out.