Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Track coinbases in wallet #5664

Merged
merged 4 commits into from
Nov 9, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.

22 changes: 22 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 All @@ -69,3 +73,21 @@ size_t utxo_spend_weight(const struct utxo *utxo, size_t min_witness_weight)

return bitcoin_tx_input_weight(utxo->is_p2sh, wit_weight);
}

u32 utxo_is_immature(const struct utxo *utxo, u32 blockheight)
{
if (utxo->is_in_coinbase) {
/* We got this from a block, it must have a known
* blockheight. */
assert(utxo->blockheight);

if (blockheight < *utxo->blockheight + 100)
return *utxo->blockheight + 99 - blockheight;

else
return 0;
} else {
/* Non-coinbase outputs are always mature. */
return 0;
}
}
10 changes: 10 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 Expand Up @@ -80,4 +83,11 @@ struct utxo *fromwire_utxo(const tal_t *ctx, const u8 **ptr, size_t *max);

/* Estimate of (signed) UTXO weight in transaction */
size_t utxo_spend_weight(const struct utxo *utxo, size_t min_witness_weight);

/**
* Determine how many blocks until a UTXO becomes mature.
*
* Returns 0 for non-coinbase outputs or the number of blocks to mature.
*/
u32 utxo_is_immature(const struct utxo *utxo, u32 blockheight);
#endif /* LIGHTNING_COMMON_UTXO_H */
566 changes: 283 additions & 283 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
3 changes: 2 additions & 1 deletion lightningd/chaintopology.c
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ static void filter_block_txs(struct chain_topology *topo, struct block *b)
const struct bitcoin_tx *tx = b->full_txs[i];
struct bitcoin_txid txid;
size_t j;
bool is_coinbase = i == 0;

/* Tell them if it spends a txo we care about. */
for (j = 0; j < tx->wtx->num_inputs; j++) {
Expand All @@ -92,7 +93,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, is_coinbase, &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 @@ -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, false, NULL,
&unused);
if (num_utxos)
wallet_transaction_add(ld->wallet, wtx, 0, 0);
Expand Down
41 changes: 41 additions & 0 deletions tests/test_opening.py
Original file line number Diff line number Diff line change
Expand Up @@ -1907,3 +1907,44 @@ def test_zeroreserve_alldust(node_factory):
# Now try with just a bit more
l1.connect(l2)
l1.rpc.fundchannel(l2.info['id'], minfunding + 1)


def test_coinbase_unspendable(node_factory, bitcoind):
""" A node should not be able to spend a coinbase output
before it's mature """

[l1] = node_factory.get_nodes(1)

addr = l1.rpc.newaddr()["bech32"]
bitcoind.rpc.generatetoaddress(1, addr)

addr2 = l1.rpc.newaddr()["bech32"]

# Wait til money in wallet
wait_for(lambda: len(l1.rpc.listfunds()['outputs']) == 1)
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
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
1 change: 1 addition & 0 deletions wallet/test/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ WALLET_TEST_COMMON_OBJS := \
common/setup.o \
common/timeout.o \
common/utils.o \
common/utxo.o \
common/wireaddr.o \
common/version.o \
wallet/db_sqlite3_sqlgen.o \
Expand Down
24 changes: 21 additions & 3 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,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"
Expand All @@ -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));
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,11 @@ static bool deep_enough(u32 maxheight, const struct utxo *utxo,
if (csv_free > current_blockheight)
return false;
}

bool immature = utxo_is_immature(utxo, current_blockheight);
if (immature)
return false;

/* 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 +553,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 +2311,7 @@ void wallet_confirm_tx(struct wallet *w,
}

int wallet_extract_owned_outputs(struct wallet *w, const struct wally_tx *wtx,
bool is_coinbase,
const u32 *blockheight,
struct amount_sat *total)
{
Expand Down Expand Up @@ -2330,19 +2346,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 = is_coinbase;

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" : "",
is_coinbase == 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,
bool is_coinbase,
const u32 *blockheight,
struct amount_sat *total);

Expand Down
12 changes: 8 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");
json_add_string(response, "status",
utxo_is_immature(utxo, current_height)
? "immature"
: "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,7 @@ 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);
wallet_extract_owned_outputs(ld->wallet, sending->wtx, false, NULL, &change);
wally_txid(sending->wtx, &txid);

for (size_t i = 0; i < sending->psbt->num_outputs; i++)
Expand Down