diff --git a/doc/developers-guide/plugin-development/event-notifications.md b/doc/developers-guide/plugin-development/event-notifications.md index 4ee034a8548d..ea906c25c17b 100644 --- a/doc/developers-guide/plugin-development/event-notifications.md +++ b/doc/developers-guide/plugin-development/event-notifications.md @@ -223,7 +223,8 @@ A notification for topic `forward_event` is sent every time the status of a forw "fee_msat": 1001, "status": "settled", "received_time": 1560696342.368, - "resolved_time": 1560696342.556 + "resolved_time": 1560696342.556, + "preimage": "0000000000000000000000000000000000000000000000000000000000000000" } } ``` @@ -262,6 +263,7 @@ or fields; - `received_time` means when we received the htlc of this payment from the previous peer. It will be contained into all status case; - `resolved_time` means when the htlc of this payment between us and the next peer was resolved. The resolved result may success or fail, so only `settled` and `failed` case contain `resolved_time`; +- `preimage` is the 64-hex-char payment preimage revealed when the HTLC was fulfilled. Only present when `status` is `settled`; - The `failcode` and `failreason` are defined in [BOLT 4](https://github.com/lightning/bolts/blob/master/04-onion-routing.md#failure-messages). ### `sendpay_success` diff --git a/lightningd/forwards.c b/lightningd/forwards.c index 6c86d5063614..4ca7c0ee3803 100644 --- a/lightningd/forwards.c +++ b/lightningd/forwards.c @@ -1,7 +1,9 @@ #include "config.h" +#include #include #include #include +#include #include #include #include @@ -86,7 +88,8 @@ bool string_to_forward_status(const char *status_str, * between 'listforwards' API and 'forward_event' notification. */ void json_add_forwarding_fields(struct json_stream *response, const struct forwarding *cur, - const struct sha256 *payment_hash) + const struct sha256 *payment_hash, + const struct preimage *preimage) { /* We don't bother grabbing id from db on update. */ if (cur->created_index) @@ -136,6 +139,8 @@ void json_add_forwarding_fields(struct json_stream *response, json_add_timeabs(response, "received_time", cur->received_time); if (cur->resolved_time) json_add_timeabs(response, "resolved_time", *cur->resolved_time); + if (preimage) + json_add_preimage(response, "preimage", preimage); } static void listforwardings_add_forwardings(struct json_stream *response, @@ -155,7 +160,7 @@ static void listforwardings_add_forwardings(struct json_stream *response, while (stmt) { const struct forwarding *cur = forwarding_details(tmpctx, wallet, stmt); json_object_start(response, NULL); - json_add_forwarding_fields(response, cur, NULL); + json_add_forwarding_fields(response, cur, NULL, NULL); json_object_end(response); tal_free(cur); stmt = forwarding_next(wallet, stmt); diff --git a/lightningd/forwards.h b/lightningd/forwards.h index 0436035b1e76..0156e63d384c 100644 --- a/lightningd/forwards.h +++ b/lightningd/forwards.h @@ -56,7 +56,8 @@ struct forwarding { * `listforwardings_add_forwardings()`. */ void json_add_forwarding_fields(struct json_stream *response, const struct forwarding *cur, - const struct sha256 *payment_hash); + const struct sha256 *payment_hash, + const struct preimage *preimage); static inline const char* forward_status_name(enum forward_status status) { diff --git a/lightningd/notification.c b/lightningd/notification.c index 755a606b17cd..9a5eac5041c3 100644 --- a/lightningd/notification.c +++ b/lightningd/notification.c @@ -1,4 +1,5 @@ #include "config.h" +#include #include #include #include @@ -389,8 +390,7 @@ static void forward_event_notification_serialize(struct json_stream *stream, cur->htlc_id_in = in->key.id; cur->created_index = created_index; cur->updated_index = updated_index; - - json_add_forwarding_fields(stream, cur, &in->payment_hash); + json_add_forwarding_fields(stream, cur, &in->payment_hash, in->preimage); } REGISTER_NOTIFICATION(forward_event); diff --git a/lightningd/peer_control.c b/lightningd/peer_control.c index fea267e61541..8e95aed099f4 100644 --- a/lightningd/peer_control.c +++ b/lightningd/peer_control.c @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -356,8 +357,45 @@ void drop_to_chain(struct lightningd *ld, struct channel *channel, /* If we withheld the funding tx, we simply close */ if (channel->withheld) { + struct htlc_out_map_iter outi; + struct htlc_out *hout; + log_info(channel->log, "Withheld channel: not sending a close transaction"); + + for (hout = htlc_out_map_first(ld->htlcs_out, &outi); + hout; + hout = htlc_out_map_next(ld->htlcs_out, &outi)) { + if (hout->key.channel != channel) + continue; + /* Has already been settled or failed */ + if (!hout->in + || hout->in->badonion != 0 + || hout->in->failonion + || hout->in->preimage) + continue; + local_fail_in_htlc(hout->in, + take(towire_permanent_channel_failure(NULL))); + } + + /* Unreserve any UTXOs from the withheld funding PSBT */ + if (channel->funding_psbt) { + for (size_t i = 0; i < channel->funding_psbt->num_inputs; i++) { + struct bitcoin_outpoint outpoint; + struct utxo *utxo; + + wally_psbt_input_get_outpoint( + &channel->funding_psbt->inputs[i], &outpoint); + utxo = wallet_utxo_get(tmpctx, ld->wallet, &outpoint); + if (!utxo || utxo->status != OUTPUT_STATE_RESERVED) + continue; + + wallet_unreserve_utxo(ld->wallet, utxo, + get_block_height(ld->topology), + utxo->reserved_til); + } + } + resolve_close_command(ld, channel, cooperative, tal_arr(tmpctx, const struct bitcoin_tx *, 0)); free_htlcs(ld, channel); diff --git a/tests/test_opening.py b/tests/test_opening.py index 47db560aadcc..13863df45b92 100644 --- a/tests/test_opening.py +++ b/tests/test_opening.py @@ -2917,8 +2917,79 @@ def test_zeroconf_withhold(node_factory, bitcoind, stay_withheld, mutual_close): if stay_withheld: assert l1.rpc.listpeerchannels()['channels'] == [] assert only_one(l1.rpc.listclosedchannels()['closedchannels'])['funding_withheld'] is True + # Verify UTXOs are unreserved after withheld channel close + funds = l1.rpc.listfunds() + for utxo in funds['outputs']: + assert utxo['status'] == 'confirmed', \ + f"UTXO still reserved after withheld close" else: if mutual_close: wait_for(lambda: only_one(l1.rpc.listpeerchannels()['channels'])['state'] == 'CLOSINGD_COMPLETE') else: wait_for(lambda: only_one(l1.rpc.listpeerchannels()['channels'])['state'] == 'AWAITING_UNILATERAL') + + +def test_zeroconf_withhold_htlc_failback(node_factory, bitcoind): + """Test that CLTV timeout on a withheld channel fails HTLCs back upstream without force-close.""" + zeroconf_plugin = str(Path(__file__).parent / "plugins" / "zeroconf-selective.py") + hold_plugin = str(Path(__file__).parent / "plugins" / "hold_htlcs.py") + + l1, l2, l3 = node_factory.get_nodes(3, opts=[ + {}, + {}, + {'plugin': [zeroconf_plugin, hold_plugin], + 'zeroconf_allow': 'any', + 'hold-time': 10000}, + ]) + + # l1 -> l2: normal funded channel + l1.fundwallet(10**7) + l1.rpc.connect(l2.info['id'], 'localhost', l2.port) + l1.rpc.fundchannel(l2.info['id'], 1000000) + bitcoind.generate_block(6, wait_for_mempool=1) + wait_for(lambda: only_one(l1.rpc.listpeerchannels(l2.info['id'])['channels'])['state'] == 'CHANNELD_NORMAL') + scid12 = only_one(l1.rpc.listpeerchannels(l2.info['id'])['channels'])['short_channel_id'] + + # l2 -> l3: withheld zeroconf channel + l2.fundwallet(10**7) + l2.rpc.connect(l3.info['id'], 'localhost', l3.port) + amount = 1000000 + funding_addr = l2.rpc.fundchannel_start(l3.info['id'], f"{amount}sat", mindepth=0)['funding_address'] + psbt = l2.rpc.fundpsbt(amount, "1000perkw", 1000, excess_as_change=True)['psbt'] + psbt = l2.rpc.addpsbtoutput(amount, psbt, destination=funding_addr)['psbt'] + assert l2.rpc.fundchannel_complete(l3.info['id'], psbt, withhold=True)['commitments_secured'] + + # Wait for withheld channel to be usable + wait_for(lambda: 'remote' in only_one(l2.rpc.listpeerchannels(l3.info['id'])['channels'])['updates']) + alias23 = only_one(l2.rpc.listpeerchannels(l3.info['id'])['channels'])['alias']['local'] + + # Create invoice on l3 and send payment from l1 via manual route + inv = l3.rpc.invoice(10000, 'test_withhold_failback', 'desc') + route = [{'amount_msat': 10001, + 'id': l2.info['id'], + 'delay': 12, + 'channel': scid12}, + {'amount_msat': 10000, + 'id': l3.info['id'], + 'delay': 6, + 'channel': alias23}] + l1.rpc.sendpay(route, inv['payment_hash'], payment_secret=inv['payment_secret']) + + # Wait for HTLC to be held at l3 + l3.daemon.wait_for_log("Holding onto an incoming htlc") + + # Mine blocks to hit the CLTV deadline. + bitcoind.generate_block(8) + + # CLTV expiry triggers force-close on the withheld channel + l2.daemon.wait_for_log(r'cltv .* hit deadline') + + # Withheld channel is gone (no on-chain tx was broadcast) + assert l2.rpc.listpeerchannels(l3.info['id'])['channels'] == [] + + # Payment should fail (HTLC was failed back) + with pytest.raises(RpcError): + l1.rpc.waitsendpay(inv['payment_hash']) + + # l1's channel to l2 is still normal — no force-close + assert only_one(l1.rpc.listpeerchannels(l2.info['id'])['channels'])['state'] == 'CHANNELD_NORMAL' diff --git a/tests/test_plugin.py b/tests/test_plugin.py index 594e4ab3bec4..75e8b51fa3b6 100644 --- a/tests/test_plugin.py +++ b/tests/test_plugin.py @@ -1490,9 +1490,12 @@ def test_forward_event_notification(node_factory, bitcoind, executor): plugin_stats = l2.rpc.call('listforwards_plugin')['forwards'] assert len(plugin_stats) == 6 - # We don't have payment_hash in listforwards any more. + # We don't have payment_hash in listforwards any more. We also don't have + # preimage in listforwards for p in plugin_stats: del p['payment_hash'] + if p.get('preimage') is not None: + del p['preimage'] # use stats to build what we expect went to plugin. expect = stats[0].copy()