Skip to content

Commit

Permalink
wallet: mark coinbase outputs as 'immature' until spendable
Browse files Browse the repository at this point in the history
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
  • Loading branch information
niftynei committed Oct 18, 2022
1 parent 978f23c commit a53ed79
Show file tree
Hide file tree
Showing 15 changed files with 370 additions and 301 deletions.
1 change: 1 addition & 0 deletions .msggen.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
},
"ListfundsOutputsStatus": {
"confirmed": 1,
"immature": 3,
"spent": 2,
"unconfirmed": 0
},
Expand Down
1 change: 1 addition & 0 deletions cln-grpc/proto/node.proto

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions cln-rpc/src/model.rs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions common/utxo.c
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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;
}

Expand Down
3 changes: 3 additions & 0 deletions common/utxo.h
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down
570 changes: 285 additions & 285 deletions contrib/pyln-testing/pyln/testing/node_pb2.py

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions doc/lightning-listfunds.7.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -73,4 +73,4 @@ RESOURCES

Main web site: <https://github.com/ElementsProject/lightning>

[comment]: # ( SHA256STAMP:e5c1f54c8a5008a30648e0fe5883132759fcdabd72bd7e8a00bedc360363e85e)
[comment]: # ( SHA256STAMP:62a8754ad2a24dfb5bb4e412a2e710748bd54ef0cffaaeb7ce352f6273742431)
3 changes: 2 additions & 1 deletion doc/schemas/listfunds.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@
"enum": [
"unconfirmed",
"confirmed",
"spent"
"spent",
"immature"
]
},
"reserved": {
Expand Down
2 changes: 1 addition & 1 deletion lightningd/chaintopology.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
3 changes: 2 additions & 1 deletion lightningd/dual_open_control.c
Original file line number Diff line number Diff line change
Expand Up @@ -1447,7 +1447,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);
Expand Down
28 changes: 26 additions & 2 deletions tests/test_opening.py
Original file line number Diff line number Diff line change
Expand Up @@ -1900,7 +1900,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 """
Expand All @@ -1914,6 +1913,31 @@ 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'

error = (f'Could not afford all using all 0 available UTXOs')
with pytest.raises(RpcError, match=error):
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
error = (f'Could not afford all using all 0 available UTXOs')
with pytest.raises(RpcError, match=error):
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
1 change: 1 addition & 0 deletions wallet/db.c
Original file line number Diff line number Diff line change
Expand Up @@ -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}]* */
Expand Down
34 changes: 29 additions & 5 deletions wallet/wallet.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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;
}
Expand All @@ -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");
Expand Down Expand Up @@ -297,7 +302,8 @@ struct utxo **wallet_get_utxos(const tal_t *ctx, struct wallet *w, const enum ou
", scriptpubkey "
", reserved_til "
", csv_lock "
"FROM outputs"));
", is_in_coinbase"
" FROM outputs"));
} else {
stmt = db_prepare_v2(w->db, SQL("SELECT"
" prev_out_tx"
Expand All @@ -315,7 +321,8 @@ struct utxo **wallet_get_utxos(const tal_t *ctx, struct wallet *w, const enum ou
", scriptpubkey "
", reserved_til "
", csv_lock "
"FROM outputs "
", is_in_coinbase"
" FROM outputs "
"WHERE status= ? "));
db_bind_int(stmt, 0, output_status_in_db(state));
}
Expand Down Expand Up @@ -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"));
Expand Down Expand Up @@ -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 = ?"));
Expand Down Expand Up @@ -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) */
Expand Down Expand Up @@ -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 <= ?)"
Expand Down Expand Up @@ -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)
{
Expand Down Expand Up @@ -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) {
Expand Down
1 change: 1 addition & 0 deletions wallet/wallet.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
13 changes: 9 additions & 4 deletions wallet/walletrpc.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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++)
Expand Down

0 comments on commit a53ed79

Please sign in to comment.