Merge #126: Spent coin detection fixes

5721ab0fdeb5927b4a53a90d4820e30d3dcfea6a qa: test spent coins are always marked as such (Antoine Poinsot)
478707df27f411c1ca983def07b6f804369a6e82 bitcoind: detect coins as spent even if they are unconfirmed (Antoine Poinsot)
8e7e46a52c74fbd2c14f3647e5395c5737153fb1 bitcoind: detect spent coins for send-to-self transactions (Antoine Poinsot)

Pull request description:

  There were a couple cases in which we would not detect a coin as spent:
  1. If the deposit transaction was still unconfirmed
  2. If the spend transaction did not contain any external output

  This comes with a functional test that will fail on master. It tests both above cases as well as many other.

  Fixes #125

ACKs for top commit:
  edouardparis:
    utACK 5721ab0fdeb5927b4a53a90d4820e30d3dcfea6a

Tree-SHA512: b05d3c3f7f4e9810346e5a86d4d789ab3dc2e1b5d6595e59bf140cb1c6d9c823ac3974ec89f1107e21e45711a8fe3e56dc20b9d094c965c92bb5640297baeea9
This commit is contained in:
Antoine Poinsot 2022-11-23 12:25:00 +01:00
commit ad8a034ed0
No known key found for this signature in database
GPG Key ID: E13FC145CD3F4304
2 changed files with 135 additions and 10 deletions

View File

@ -683,20 +683,19 @@ impl BitcoinD {
/// and iterating through each of those to check if it spends the transaction we are interested
/// in (requiring an other RPC call for each!!).
pub fn get_spender_txid(&self, spent_outpoint: &bitcoin::OutPoint) -> Option<bitcoin::Txid> {
// Get the hash of the block parent of the spent transaction's block.
// Get the hash of the spent transaction's block parent. If the spent transaction is still
// unconfirmed, just use the tip.
let req = self.make_wallet_request(
"gettransaction",
&params!(Json::String(spent_outpoint.txid.to_string())),
);
let spent_tx_height = match req.get("blockheight").and_then(Json::as_i64) {
Some(h) => h,
// FIXME: we assume it's confirmed. If we were to change the logic in the poller, we'd
// need to handle it here.
None => return None,
let list_since_height = match req.get("blockheight").and_then(Json::as_i64) {
Some(h) => h as i32,
None => self.chain_tip().height,
};
let block_hash = if let Ok(res) = self.make_fallible_node_request(
"getblockhash",
&params!(Json::Number((spent_tx_height - 1).into())),
&params!(Json::Number((list_since_height - 1).into())),
) {
res.as_str()
.expect("'getblockhash' result isn't a string")
@ -708,8 +707,17 @@ impl BitcoinD {
// Now we can get all transactions related to us since the spent transaction confirmed.
// We'll use it to locate the spender.
let lsb_res =
self.make_wallet_request("listsinceblock", &params!(Json::String(block_hash)));
// TODO: merge this with the existing list_since_block method.
let lsb_res = self.make_wallet_request(
"listsinceblock",
&params!(
Json::String(block_hash),
Json::Number(1.into()), // Default for min_confirmations for the returned
Json::Bool(true), // Whether to include watchonly
Json::Bool(false), // Whether to include an array of txs that were removed in reorgs
Json::Bool(true) // Whether to include UTxOs treated as change.
),
);
let transactions = lsb_res
.get("transactions")
.and_then(Json::as_array)

View File

@ -1,6 +1,6 @@
from fixtures import *
from test_framework.serializations import PSBT
from test_framework.utils import wait_for
from test_framework.utils import wait_for, COIN
def test_spend_change(lianad, bitcoind):
@ -53,3 +53,120 @@ def test_spend_change(lianad, bitcoind):
spend_txid = signed_psbt.tx.txid().hex()
lianad.rpc.broadcastspend(spend_txid)
bitcoind.generate_block(1, wait_for_mempool=spend_txid)
def test_coin_marked_spent(lianad, bitcoind):
"""Test a spent coin is marked as such under various conditions."""
# Receive a coin in a single transaction
addr = lianad.rpc.getnewaddress()["address"]
deposit_a = bitcoind.rpc.sendtoaddress(addr, 0.01)
bitcoind.generate_block(1, wait_for_mempool=deposit_a)
wait_for(lambda: len(lianad.rpc.listcoins()["coins"]) == 1)
# Receive another coin on the same address
deposit_b = bitcoind.rpc.sendtoaddress(addr, 0.02)
bitcoind.generate_block(1, wait_for_mempool=deposit_b)
wait_for(lambda: len(lianad.rpc.listcoins()["coins"]) == 2)
# Receive three coins in a single deposit transaction
destinations = {
lianad.rpc.getnewaddress()["address"]: 0.03,
lianad.rpc.getnewaddress()["address"]: 0.04,
lianad.rpc.getnewaddress()["address"]: 0.05,
}
deposit_c = bitcoind.rpc.sendmany("", destinations)
bitcoind.generate_block(1, wait_for_mempool=deposit_c)
wait_for(lambda: len(lianad.rpc.listcoins()["coins"]) == 5)
# Receive a coin in an unconfirmed deposit transaction
addr = lianad.rpc.getnewaddress()["address"]
deposit_d = bitcoind.rpc.sendtoaddress(addr, 0.06)
wait_for(lambda: len(lianad.rpc.listcoins()["coins"]) == 5)
def sign_and_broadcast(psbt):
txid = psbt.tx.txid().hex()
psbt = lianad.sign_psbt(psbt)
lianad.rpc.updatespend(psbt.to_base64())
lianad.rpc.broadcastspend(txid)
return txid
# Spend the first coin with a change output
outpoint = next(
c["outpoint"]
for c in lianad.rpc.listcoins()["coins"]
if deposit_a in c["outpoint"]
)
destinations = {
bitcoind.rpc.getnewaddress(): 500_000,
}
res = lianad.rpc.createspend(destinations, [outpoint], 6)
psbt = PSBT.from_base64(res["psbt"])
sign_and_broadcast(psbt)
# Spend the second coin without a change output
outpoint = next(
c["outpoint"]
for c in lianad.rpc.listcoins()["coins"]
if deposit_b in c["outpoint"]
)
destinations = {
bitcoind.rpc.getnewaddress(): int(0.02 * COIN) - 1_000,
}
res = lianad.rpc.createspend(destinations, [outpoint], 1)
psbt = PSBT.from_base64(res["psbt"])
sign_and_broadcast(psbt)
# Spend the third coin to an address of ours, no change
outpoints = [
c["outpoint"]
for c in lianad.rpc.listcoins()["coins"]
if deposit_c in c["outpoint"]
]
destinations = {
lianad.rpc.getnewaddress()["address"]: int(0.03 * COIN) - 1_000,
}
res = lianad.rpc.createspend(destinations, [outpoints[0]], 1)
psbt = PSBT.from_base64(res["psbt"])
sign_and_broadcast(psbt)
# Spend the fourth coin to an address of ours, with change
destinations = {
lianad.rpc.getnewaddress()["address"]: int(0.04 * COIN / 2),
}
res = lianad.rpc.createspend(destinations, [outpoints[1]], 18)
psbt = PSBT.from_base64(res["psbt"])
sign_and_broadcast(psbt)
# Batch spend the fourth and fifth coins
outpoint = next(
c["outpoint"]
for c in lianad.rpc.listcoins()["coins"]
if deposit_d in c["outpoint"]
)
destinations = {
lianad.rpc.getnewaddress()["address"]: int(0.01 * COIN),
lianad.rpc.getnewaddress()["address"]: int(0.01 * COIN),
bitcoind.rpc.getnewaddress(): int(0.01 * COIN),
}
res = lianad.rpc.createspend(destinations, [outpoints[2], outpoint], 2)
psbt = PSBT.from_base64(res["psbt"])
sign_and_broadcast(psbt)
# All the spent coins must have been detected as such
all_deposits = (deposit_a, deposit_b, deposit_c, deposit_d)
def deposited_coins():
return (
c
for c in lianad.rpc.listcoins()["coins"]
if c["outpoint"][:-2] in all_deposits
)
def is_spent(coin):
if coin["spend_info"] is None:
return False
if coin["spend_info"]["txid"] is None:
return False
return True
wait_for(lambda: all(is_spent(c) for c in deposited_coins()))