wallet: mark coinbase outputs as 'immature' until spendable

Changelog-Changed: JSON-RPC: `listfunds` now lists coinbase outputs as 'immature' until they're spendable
Changelog-Changed: JSON-RPC: UTXOs aren't spendable while immature
This commit is contained in:
niftynei 2022-10-17 14:59:57 -05:00 committed by Christian Decker
parent d60dbba43b
commit 26f5dcd2a5
15 changed files with 77 additions and 14 deletions

View File

@ -76,6 +76,7 @@
},
"ListfundsOutputsStatus": {
"confirmed": 1,
"immature": 3,
"spent": 2,
"unconfirmed": 0
},

Binary file not shown.

Binary file not shown.

View File

@ -25,6 +25,8 @@ void towire_utxo(u8 **pptr, const struct utxo *utxo)
towire_bool(pptr, utxo->close_info->option_anchor_outputs);
towire_u32(pptr, utxo->close_info->csv);
}
towire_bool(pptr, utxo->is_in_coinbase);
}
struct utxo *fromwire_utxo(const tal_t *ctx, const u8 **ptr, size_t *max)
@ -55,6 +57,8 @@ struct utxo *fromwire_utxo(const tal_t *ctx, const u8 **ptr, size_t *max)
} else {
utxo->close_info = NULL;
}
utxo->is_in_coinbase = fromwire_bool(ptr, max);
return utxo;
}

View File

@ -53,6 +53,9 @@ struct utxo {
/* The scriptPubkey if it is known */
u8 *scriptPubkey;
/* Is this utxo a coinbase output */
bool is_in_coinbase;
};
/* We lazy-evaluate whether a utxo is really still reserved. */

View File

@ -27,7 +27,7 @@ On success, an object is returned, containing:
- **output** (u32): the index within *txid*
- **amount\_msat** (msat): the amount of the output
- **scriptpubkey** (hex): the scriptPubkey of the output
- **status** (string) (one of "unconfirmed", "confirmed", "spent")
- **status** (string) (one of "unconfirmed", "confirmed", "spent", "immature")
- **reserved** (boolean): whether this UTXO is currently reserved for an in-flight tx
- **address** (string, optional): the bitcoin address of the output
- **redeemscript** (hex, optional): the redeemscript, only if it's p2sh-wrapped
@ -73,4 +73,4 @@ RESOURCES
Main web site: <https://github.com/ElementsProject/lightning>
[comment]: # ( SHA256STAMP:e5c1f54c8a5008a30648e0fe5883132759fcdabd72bd7e8a00bedc360363e85e)
[comment]: # ( SHA256STAMP:62a8754ad2a24dfb5bb4e412a2e710748bd54ef0cffaaeb7ce352f6273742431)

View File

@ -50,7 +50,8 @@
"enum": [
"unconfirmed",
"confirmed",
"spent"
"spent",
"immature"
]
},
"reserved": {

View File

@ -92,7 +92,7 @@ static void filter_block_txs(struct chain_topology *topo, struct block *b)
txid = b->txids[i];
if (txfilter_match(topo->bitcoind->ld->owned_txfilter, tx)) {
wallet_extract_owned_outputs(topo->bitcoind->ld->wallet,
tx->wtx, &b->height, &owned);
tx->wtx, i, &b->height, &owned);
wallet_transaction_add(topo->ld->wallet, tx->wtx,
b->height, i);
}

View File

@ -1461,7 +1461,8 @@ static void handle_tx_broadcast(struct channel_send *cs)
/* This might have spent UTXOs from our wallet */
num_utxos = wallet_extract_owned_outputs(ld->wallet,
wtx, NULL,
/* FIXME: what txindex? */
wtx, 1, NULL,
&unused);
if (num_utxos)
wallet_transaction_add(ld->wallet, wtx, 0, 0);

View File

@ -1909,7 +1909,6 @@ def test_zeroreserve_alldust(node_factory):
l1.rpc.fundchannel(l2.info['id'], minfunding + 1)
@pytest.mark.xfail
def test_coinbase_unspendable(node_factory, bitcoind):
""" A node should not be able to spend a coinbase output
before it's mature """
@ -1923,6 +1922,29 @@ def test_coinbase_unspendable(node_factory, bitcoind):
# Wait til money in wallet
wait_for(lambda: len(l1.rpc.listfunds()['outputs']) == 1)
l1.rpc.withdraw(addr2, "all")
out = only_one(l1.rpc.listfunds()['outputs'])
assert out['status'] == 'immature'
with pytest.raises(RpcError, match='Could not afford all using all 0 available UTXOs'):
l1.rpc.withdraw(addr2, "all")
# Nothing sent to the mempool!
assert len(bitcoind.rpc.getrawmempool()) == 0
# Mine 98 blocks
bitcoind.rpc.generatetoaddress(98, l1.rpc.newaddr()['bech32'])
assert len([out for out in l1.rpc.listfunds()['outputs'] if out['status'] == 'confirmed']) == 0
with pytest.raises(RpcError, match='Could not afford all using all 0 available UTXOs'):
l1.rpc.withdraw(addr2, "all")
# One more and the first coinbase unlocks
bitcoind.rpc.generatetoaddress(1, l1.rpc.newaddr()['bech32'])
wait_for(lambda: len(l1.rpc.listfunds()['outputs']) == 100)
assert len([out for out in l1.rpc.listfunds()['outputs'] if out['status'] == 'confirmed']) == 1
l1.rpc.withdraw(addr2, "all")
# One tx in the mempool now!
assert len(bitcoind.rpc.getrawmempool()) == 1
# Mine one block, assert one more is spendable
bitcoind.rpc.generatetoaddress(1, l1.rpc.newaddr()['bech32'])
assert len([out for out in l1.rpc.listfunds()['outputs'] if out['status'] == 'confirmed']) == 1

View File

@ -929,6 +929,7 @@ static struct migration dbmigrations[] = {
/* Adds scid column, then moves short_channel_id across to it */
{SQL("ALTER TABLE channels ADD scid BIGINT;"), migrate_channels_scids_as_integers},
{SQL("ALTER TABLE payments ADD failscid BIGINT;"), migrate_payments_scids_as_integers},
{SQL("ALTER TABLE outputs ADD is_in_coinbase INTEGER DEFAULT 0;"), NULL},
};
/* Released versions are of form v{num}[.{num}]* */

View File

@ -148,7 +148,8 @@ static bool wallet_add_utxo(struct wallet *w, struct utxo *utxo,
", confirmation_height"
", spend_height"
", scriptpubkey"
") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);"));
", is_in_coinbase"
") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);"));
db_bind_txid(stmt, 0, &utxo->outpoint.txid);
db_bind_int(stmt, 1, utxo->outpoint.n);
db_bind_amount_sat(stmt, 2, &utxo->amount);
@ -183,6 +184,7 @@ static bool wallet_add_utxo(struct wallet *w, struct utxo *utxo,
db_bind_blob(stmt, 12, utxo->scriptPubkey,
tal_bytelen(utxo->scriptPubkey));
db_bind_int(stmt, 13, utxo->is_in_coinbase);
db_exec_prepared_v2(take(stmt));
return true;
}
@ -200,6 +202,9 @@ static struct utxo *wallet_stmt2output(const tal_t *ctx, struct db_stmt *stmt)
utxo->is_p2sh = db_col_int(stmt, "type") == p2sh_wpkh;
utxo->status = db_col_int(stmt, "status");
utxo->keyindex = db_col_int(stmt, "keyindex");
utxo->is_in_coinbase = db_col_int(stmt, "is_in_coinbase") == 1;
if (!db_col_is_null(stmt, "channel_id")) {
utxo->close_info = tal(utxo, struct unilateral_close_info);
utxo->close_info->channel_id = db_col_u64(stmt, "channel_id");
@ -297,6 +302,7 @@ struct utxo **wallet_get_utxos(const tal_t *ctx, struct wallet *w, const enum ou
", scriptpubkey "
", reserved_til "
", csv_lock "
", is_in_coinbase "
"FROM outputs"));
} else {
stmt = db_prepare_v2(w->db, SQL("SELECT"
@ -315,6 +321,7 @@ struct utxo **wallet_get_utxos(const tal_t *ctx, struct wallet *w, const enum ou
", scriptpubkey "
", reserved_til "
", csv_lock "
", is_in_coinbase "
"FROM outputs "
"WHERE status= ? "));
db_bind_int(stmt, 0, output_status_in_db(state));
@ -354,6 +361,7 @@ struct utxo **wallet_get_unconfirmed_closeinfo_utxos(const tal_t *ctx,
", scriptpubkey"
", reserved_til"
", csv_lock"
", is_in_coinbase"
" FROM outputs"
" WHERE channel_id IS NOT NULL AND "
"confirmation_height IS NULL"));
@ -391,6 +399,7 @@ struct utxo *wallet_utxo_get(const tal_t *ctx, struct wallet *w,
", scriptpubkey"
", reserved_til"
", csv_lock"
", is_in_coinbase"
" FROM outputs"
" WHERE prev_out_tx = ?"
" AND prev_out_index = ?"));
@ -501,6 +510,17 @@ static bool deep_enough(u32 maxheight, const struct utxo *utxo,
if (csv_free > current_blockheight)
return false;
}
/* If the utxo is a coinbase, we over-write the maxheight
* to the coinbase maxheight (current - 99) */
if (utxo->is_in_coinbase) {
/* Nothing is spendable the first 100 blocks */
if (current_blockheight < 100)
return false;
if (maxheight > current_blockheight - 99)
maxheight = current_blockheight - 99;
}
/* If we require confirmations check that we have a
* confirmation height and that it is below the required
* maxheight (current_height - minconf) */
@ -539,6 +559,7 @@ struct utxo *wallet_find_utxo(const tal_t *ctx, struct wallet *w,
", scriptpubkey "
", reserved_til"
", csv_lock"
", is_in_coinbase"
" FROM outputs"
" WHERE status = ?"
" OR (status = ? AND reserved_til <= ?)"
@ -2296,6 +2317,7 @@ void wallet_confirm_tx(struct wallet *w,
}
int wallet_extract_owned_outputs(struct wallet *w, const struct wally_tx *wtx,
u32 tx_index,
const u32 *blockheight,
struct amount_sat *total)
{
@ -2330,19 +2352,21 @@ int wallet_extract_owned_outputs(struct wallet *w, const struct wally_tx *wtx,
wally_txid(wtx, &utxo->outpoint.txid);
utxo->outpoint.n = output;
utxo->close_info = NULL;
utxo->is_in_coinbase = tx_index == 0;
utxo->blockheight = blockheight ? blockheight : NULL;
utxo->spendheight = NULL;
utxo->scriptPubkey = tal_dup_talarr(utxo, u8, script);
log_debug(w->log, "Owning output %zu %s (%s) txid %s%s",
log_debug(w->log, "Owning output %zu %s (%s) txid %s%s%s",
output,
type_to_string(tmpctx, struct amount_sat,
&utxo->amount),
is_p2sh ? "P2SH" : "SEGWIT",
type_to_string(tmpctx, struct bitcoin_txid,
&utxo->outpoint.txid),
blockheight ? " CONFIRMED" : "");
blockheight ? " CONFIRMED" : "",
tx_index == 0 ? " COINBASE" : "");
/* We only record final ledger movements */
if (blockheight) {

View File

@ -685,6 +685,7 @@ void wallet_blocks_heights(struct wallet *w, u32 def, u32 *min, u32 *max);
* wallet_extract_owned_outputs - given a tx, extract all of our outputs
*/
int wallet_extract_owned_outputs(struct wallet *w, const struct wally_tx *tx,
u32 tx_index,
const u32 *blockheight,
struct amount_sat *total);

View File

@ -240,6 +240,7 @@ static void json_add_utxo(struct json_stream *response,
{
const char *out;
bool reserved;
u32 current_height = get_block_height(wallet->ld->topology);
json_object_start(response, fieldname);
json_add_txid(response, "txid", &utxo->outpoint.txid);
@ -271,13 +272,16 @@ static void json_add_utxo(struct json_stream *response,
if (utxo->spendheight)
json_add_string(response, "status", "spent");
else if (utxo->blockheight) {
json_add_string(response, "status", "confirmed");
if (utxo->is_in_coinbase
&& *utxo->blockheight + 99 > current_height) {
json_add_string(response, "status", "immature");
} else
json_add_string(response, "status", "confirmed");
json_add_num(response, "blockheight", *utxo->blockheight);
} else
json_add_string(response, "status", "unconfirmed");
reserved = utxo_is_reserved(utxo,
get_block_height(wallet->ld->topology));
reserved = utxo_is_reserved(utxo, current_height);
json_add_bool(response, "reserved", reserved);
if (reserved)
json_add_num(response, "reserved_to_block",
@ -884,7 +888,8 @@ static void sendpsbt_done(struct bitcoind *bitcoind UNUSED,
wallet_transaction_add(ld->wallet, sending->wtx, 0, 0);
/* Extract the change output and add it to the DB */
wallet_extract_owned_outputs(ld->wallet, sending->wtx, NULL, &change);
/* FIXME: what txindex? */
wallet_extract_owned_outputs(ld->wallet, sending->wtx, 1, NULL, &change);
wally_txid(sending->wtx, &txid);
for (size_t i = 0; i < sending->psbt->num_outputs; i++)