Merge #604: poller: spend transaction RBF handling

0785bb7f9f63c788af7b7e755b118d642d9b0402 poller: don't filter out coins with already a spend txid when checking for spends (Antoine Poinsot)

Pull request description:

  It turns out we didn't actually notice RBF'd spends.

  Fixes #382. And also fixes #73.

ACKs for top commit:
  edouardparis:
    utACK 0785bb7f9f63c788af7b7e755b118d642d9b0402

Tree-SHA512: 2c37708fb1ecf4e6122fdb1d0f92b5a89978632ac4f31f24c7628a88160b76e1f6edf956544e88544dbefc46b7cf06e2cfb77db5477fd6f05ce698ff18b2d919
This commit is contained in:
Antoine Poinsot 2023-08-11 12:50:12 +02:00
commit 6c969aeacd
No known key found for this signature in database
GPG Key ID: E13FC145CD3F4304
2 changed files with 65 additions and 2 deletions

View File

@ -115,7 +115,8 @@ fn update_coins(
.values()
.chain(received.iter())
.filter_map(|coin| {
if coin.spend_txid.is_none() {
// Always check for spends when the spend tx is not confirmed as it might get RBF'd.
if coin.spend_txid.is_none() || coin.spend_block.is_none() {
Some(coin.outpoint)
} else {
None

View File

@ -1,5 +1,14 @@
import copy
from fixtures import *
from test_framework.utils import wait_for, get_txid, spend_coins, RpcError, COIN, sign_and_broadcast
from test_framework.utils import (
wait_for,
get_txid,
spend_coins,
RpcError,
COIN,
sign_and_broadcast,
)
from test_framework.serializations import PSBT
@ -305,6 +314,7 @@ def test_deposit_replacement(lianad, bitcoind):
bitcoind.rpc.sendtoaddress(addr, 2)
wait_for(lambda: len(lianad.rpc.listcoins()["coins"]) == 1)
def test_rescan_and_recovery(lianad, bitcoind):
"""Test user recovery flow"""
# Get initial_tip to use for rescan later
@ -339,3 +349,55 @@ def test_rescan_and_recovery(lianad, bitcoind):
assert len(reco_psbt.tx.vout) == 1
assert int(0.4999 * COIN) < int(reco_psbt.tx.vout[0].nValue) < int(0.5 * COIN)
sign_and_broadcast(lianad, bitcoind, reco_psbt, recovery=True)
def test_conflicting_unconfirmed_spend_txs(lianad, bitcoind):
"""Test we'll update the spending txid of a coin if a conflicting spend enters our mempool."""
# Get an (unconfirmed, on purpose) coin to be spent by 2 different txs.
addr = lianad.rpc.getnewaddress()["address"]
txid = bitcoind.rpc.sendtoaddress(addr, 0.01)
wait_for(lambda: len(lianad.rpc.listcoins()["coins"]) == 1)
spent_coin = lianad.rpc.listcoins()["coins"][0]
# Create a first transaction, register it in our wallet.
outpoints = [c["outpoint"] for c in lianad.rpc.listcoins()["coins"]]
destinations = {
bitcoind.rpc.getnewaddress(): 100_000,
}
res = lianad.rpc.createspend(destinations, outpoints, 2)
psbt_a = PSBT.from_base64(res["psbt"])
txid_a = psbt_a.tx.txid()
# Create a conflicting transaction, not to be registered in our wallet.
psbt_b = copy.deepcopy(psbt_a)
psbt_b.tx.vout[0].scriptPubKey = bytes.fromhex(
"0014218612c653e0827f73a6a040d7805acefa6530cb"
)
psbt_b.tx.vout[0].nValue -= 10_000
psbt_b.tx.rehash()
txid_b = psbt_b.tx.txid()
# Sign and broadcast the first Spend transaction.
signed_psbt = lianad.signer.sign_psbt(psbt_a)
lianad.rpc.updatespend(signed_psbt.to_base64())
lianad.rpc.broadcastspend(txid_a.hex())
# We detect the coin as being spent by the first transaction.
wait_for(lambda: get_coin(lianad, spent_coin["outpoint"])["spend_info"] is not None)
assert (
get_coin(lianad, spent_coin["outpoint"])["spend_info"]["txid"] == txid_a.hex()
)
# Now sign and broadcast the conflicting transaction, as if coming from an external
# wallet.
signed_psbt = lianad.signer.sign_psbt(psbt_b)
finalized_psbt = lianad.finalize_psbt(signed_psbt)
tx_hex = finalized_psbt.tx.serialize_with_witness().hex()
bitcoind.rpc.sendrawtransaction(tx_hex)
# We must now detect the coin as being spent by the second transaction.
wait_for(
lambda: get_coin(lianad, spent_coin["outpoint"])["spend_info"] is not None
and get_coin(lianad, spent_coin["outpoint"])["spend_info"]["txid"]
== txid_b.hex()
)