mirror of
https://github.com/ElementsProject/lightning.git
synced 2025-01-19 05:44:12 +01:00
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:
parent
d60dbba43b
commit
26f5dcd2a5
@ -76,6 +76,7 @@
|
||||
},
|
||||
"ListfundsOutputsStatus": {
|
||||
"confirmed": 1,
|
||||
"immature": 3,
|
||||
"spent": 2,
|
||||
"unconfirmed": 0
|
||||
},
|
||||
|
Binary file not shown.
Binary file not shown.
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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. */
|
||||
|
Binary file not shown.
@ -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)
|
||||
|
@ -50,7 +50,8 @@
|
||||
"enum": [
|
||||
"unconfirmed",
|
||||
"confirmed",
|
||||
"spent"
|
||||
"spent",
|
||||
"immature"
|
||||
]
|
||||
},
|
||||
"reserved": {
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
|
@ -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}]* */
|
||||
|
@ -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) {
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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++)
|
||||
|
Loading…
Reference in New Issue
Block a user