Skip to content
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
7 changes: 5 additions & 2 deletions db/bindings.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
48 changes: 48 additions & 0 deletions tests/test_closing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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})
Expand Down
8 changes: 8 additions & 0 deletions wallet/wallet.c
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -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);
}
Expand Down
Loading