diff --git a/db/bindings.c b/db/bindings.c index d61fca9f49cb..41aba612a680 100644 --- a/db/bindings.c +++ b/db/bindings.c @@ -496,9 +496,12 @@ struct wally_psbt *db_col_psbt(const tal_t *ctx, struct db_stmt *stmt, const cha const u8 *src = db_column_blob(stmt, col); size_t len = db_column_bytes(stmt, col); - db_column_null_warn(stmt, colname, col); + if (db_column_null_warn(stmt, colname, col)) + return NULL; + psbt = psbt_from_bytes(ctx, src, len); - psbt_set_version(psbt, 2); + if (psbt) + psbt_set_version(psbt, 2); return psbt; } diff --git a/tests/test_closing.py b/tests/test_closing.py index d5a5b382e63e..c9d0b4512a0c 100644 --- a/tests/test_closing.py +++ b/tests/test_closing.py @@ -4878,6 +4878,54 @@ def test_anchorspend_using_to_remote(node_factory, bitcoind, anchors): bitcoind.generate_block(1, wait_for_mempool=2) +@unittest.skipIf(TEST_NETWORK != 'regtest', 'elementsd anchors not supported') +def test_anchorspend_ignores_immature_coinbase(node_factory, bitcoind, executor): + """Fee rescue must not select an immature coinbase: spending one is + consensus-invalid, so the resulting anchor spend would be rejected.""" + # l1 stops responding once it has the fulfill, so l2 has to go onchain + # to claim before the incoming HTLC expires. + l1, l2 = node_factory.get_nodes(2, + opts=[{'disconnect': ['-WIRE_REVOKE_AND_ACK*2'], + 'dev-no-reconnect': None, + 'feerates': (1000,) * 4}, + {'feerates': (1000,) * 4}]) + + # l2's only wallet output is a block reward it just mined: immature, and + # thus unspendable, for the next 100 blocks. + coinbase_block = only_one(bitcoind.rpc.generatetoaddress(1, l2.rpc.newaddr('bech32')['bech32'])) + sync_blockheight(bitcoind, [l1, l2]) + coinbase_txid = bitcoind.rpc.getblock(coinbase_block)['tx'][0] + assert only_one([o for o in l2.rpc.listfunds()['outputs'] + if o['txid'] == coinbase_txid])['status'] == 'immature' + + # l1 funds the channel, so l2 acquires no other wallet output. + l1.rpc.connect(l2.info['id'], 'localhost', l2.port) + l1.fundchannel(l2, 1000000) + + inv = l2.rpc.invoice(200000000, 'stuck', 'stuck') + executor.submit(l1.rpc.xpay, inv['bolt11']) + l1.daemon.wait_for_log('dev_disconnect: -WIRE_REVOKE_AND_ACK') + + # Now make the commitment tx feerate inadequate, so the close needs a + # wallet-funded boost, and mine until l2 hits the HTLC deadline. + l2.set_feerates((7500,) * 4) + for _ in range(20): + bitcoind.generate_block(1) + sync_blockheight(bitcoind, [l2]) + if only_one(l2.rpc.listpeerchannels()['channels'])['state'] == 'AWAITING_UNILATERAL': + break + wait_for(lambda: only_one(l2.rpc.listpeerchannels()['channels'])['state'] == 'AWAITING_UNILATERAL') + + # The immature coinbase is the only candidate, so there is nothing to + # boost with: l2 must say so rather than build an invalid rescue. + l2.daemon.wait_for_log('No utxos to bump commit_tx') + assert not l2.daemon.is_in_log('Creating anchor spend for local commit tx') + + # And it's still sitting there, unspent. + assert only_one([o for o in l2.rpc.listfunds()['outputs'] + if o['txid'] == coinbase_txid])['status'] == 'immature' + + def test_onchain_reestablish_reply(node_factory, bitcoind, executor): l1, l2, l3 = node_factory.line_graph(3, opts={'may_reconnect': True, 'dev-no-reconnect': None}) diff --git a/wallet/wallet.c b/wallet/wallet.c index bb66fb44ebf5..49c94bafc2d3 100644 --- a/wallet/wallet.c +++ b/wallet/wallet.c @@ -631,6 +631,11 @@ struct utxo **wallet_utxo_boost(const tal_t *ctx, if (utxo_is_csv_locked(utxo, blockheight)) continue; + /* Don't add immature coinbase outputs: spending them is + * consensus-invalid. */ + if (utxo_is_immature(utxo, blockheight)) + continue; + /* UTXOs must be sane amounts */ if (!amount_sat_add(&new_excess_sats, excess_sats, utxo->amount)) @@ -1250,6 +1255,9 @@ static bool wallet_shachain_load(struct wallet *wallet, u64 id, while (db_step(stmt)) { int pos = db_col_int(stmt, "pos"); + if (pos < 0 || pos >= ARRAY_SIZE(chain->chain.known)) + db_fatal(wallet->db, + "shachain_known pos %i out of range", pos); chain->chain.known[pos].index = db_col_u64(stmt, "idx"); db_col_sha256(stmt, "hash", &chain->chain.known[pos].hash); }