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
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
```
Expand Down Expand Up @@ -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`
Expand Down
9 changes: 7 additions & 2 deletions lightningd/forwards.c
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
#include "config.h"
#include <bitcoin/preimage.h>
#include <ccan/mem/mem.h>
#include <ccan/tal/str/str.h>
#include <common/json_command.h>
#include <common/json_stream.h>
#include <inttypes.h>
#include <lightningd/forwards.h>
#include <lightningd/jsonrpc.h>
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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,
Expand All @@ -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);
Expand Down
3 changes: 2 additions & 1 deletion lightningd/forwards.h
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down
4 changes: 2 additions & 2 deletions lightningd/notification.c
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#include "config.h"
#include <bitcoin/preimage.h>
#include <ccan/cast/cast.h>
#include <lightningd/channel.h>
#include <lightningd/coin_mvts.h>
Expand Down Expand Up @@ -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);
Expand Down
38 changes: 38 additions & 0 deletions lightningd/peer_control.c
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
#include <common/initial_commit_tx.h>
#include <common/json_channel_type.h>
#include <common/json_command.h>
#include <common/psbt_open.h>
#include <common/timeout.h>
#include <common/version.h>
#include <common/wire_error.h>
Expand Down Expand Up @@ -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);
Expand Down
71 changes: 71 additions & 0 deletions tests/test_opening.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'
5 changes: 4 additions & 1 deletion tests/test_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Loading