diff --git a/plugins/autoclean.c b/plugins/autoclean.c index b1670f208..a7fdf4908 100644 --- a/plugins/autoclean.c +++ b/plugins/autoclean.c @@ -8,6 +8,8 @@ #include enum subsystem { + SUCCEEDEDFORWARDS, + FAILEDFORWARDS, SUCCEEDEDPAYS, FAILEDPAYS, PAIDINVOICES, @@ -15,6 +17,8 @@ enum subsystem { #define NUM_SUBSYSTEM (EXPIREDINVOICES + 1) }; static const char *subsystem_str[] = { + "succeededforwards", + "failedforwards", "succeededpays", "failedpays", "paidinvoices", @@ -227,6 +231,73 @@ static struct command_result *listsendpays_done(struct command *cmd, return set_next_timer(plugin); } +static struct command_result *listforwards_done(struct command *cmd, + const char *buf, + const jsmntok_t *result, + char *unused) +{ + const jsmntok_t *t, *fwds = json_get_member(buf, result, "forwards"); + size_t i; + u64 now = time_now().ts.tv_sec; + + json_for_each_arr(i, t, fwds) { + const jsmntok_t *status = json_get_member(buf, t, "status"); + jsmntok_t time; + enum subsystem subsys; + u64 restime; + + if (json_tok_streq(buf, status, "settled")) { + subsys = SUCCEEDEDFORWARDS; + } else if (json_tok_streq(buf, status, "failed") + || json_tok_streq(buf, status, "local_failed")) { + subsys = FAILEDFORWARDS; + } else + continue; + + /* Continue if we don't care. */ + if (subsystem_age[subsys] == 0) + continue; + + time = *json_get_member(buf, t, "resolved_time"); + /* This is a float, so truncate at '.' */ + for (int off = time.start; off < time.end; off++) { + if (buf[off] == '.') + time.end = off; + } + if (!json_to_u64(buf, &time, &restime)) { + plugin_err(plugin, "Bad time '%.*s'", + json_tok_full_len(&time), + json_tok_full(buf, &time)); + } + + if (restime <= now - subsystem_age[subsys]) { + struct out_req *req; + const jsmntok_t *inchan, *inid; + + inchan = json_get_member(buf, t, "in_channel"); + inid = json_get_member(buf, t, "in_htlc_id"); + + req = jsonrpc_request_start(plugin, NULL, "delforward", + del_done, del_failed, + int2ptr(subsys)); + json_add_tok(req->js, "in_channel", inchan, buf); + json_add_tok(req->js, "in_htlc_id", inid, buf); + json_add_tok(req->js, "status", status, buf); + send_outreq(plugin, req); + plugin_log(plugin, LOG_DBG, "Cleaning up fwd %.*s/%.*s", + json_tok_full_len(inchan), + json_tok_full(buf, inchan), + json_tok_full_len(inid), + json_tok_full(buf, inid)); + cleanup_reqs_remaining++; + } + } + + if (cleanup_reqs_remaining) + return command_still_pending(cmd); + return set_next_timer(plugin); +} + static void do_clean(void *unused) { struct out_req *req = NULL; @@ -248,6 +319,14 @@ static void do_clean(void *unused) send_outreq(plugin, req); } + if (subsystem_age[SUCCEEDEDFORWARDS] != 0 + || subsystem_age[FAILEDFORWARDS] != 0) { + req = jsonrpc_request_start(plugin, NULL, "listforwards", + listforwards_done, cmd_failed, + (char *)"listforwards"); + send_outreq(plugin, req); + } + if (!req) set_next_timer(plugin); } diff --git a/tests/test_plugin.py b/tests/test_plugin.py index cd2c0ffc6..89849b6a8 100644 --- a/tests/test_plugin.py +++ b/tests/test_plugin.py @@ -2945,90 +2945,111 @@ def test_commando_badrune(node_factory): def test_autoclean(node_factory): - l0, l1 = node_factory.line_graph(2, opts={'autoclean-cycle': 10, - 'may_reconnect': True}) + l1, l2, l3 = node_factory.line_graph(3, opts={'autoclean-cycle': 10, + 'may_reconnect': True}, + wait_for_announce=True) - assert l1.rpc.autoclean_status('expiredinvoices')['autoclean']['expiredinvoices']['enabled'] is False - l1.rpc.invoice(amount_msat=12300, label='inv1', description='description1', expiry=5) - l1.rpc.invoice(amount_msat=12300, label='inv2', description='description2', expiry=20) - l1.rpc.invoice(amount_msat=12300, label='inv3', description='description3', expiry=20) - inv4 = l1.rpc.invoice(amount_msat=12300, label='inv4', description='description4', expiry=2000) - inv5 = l1.rpc.invoice(amount_msat=12300, label='inv5', description='description5', expiry=2000) - l1.rpc.autoclean(subsystem='expiredinvoices', age=2) - assert l1.rpc.autoclean_status()['autoclean']['expiredinvoices']['enabled'] is True - assert l1.rpc.autoclean_status()['autoclean']['expiredinvoices']['age'] == 2 + assert l3.rpc.autoclean_status('expiredinvoices')['autoclean']['expiredinvoices']['enabled'] is False + l3.rpc.invoice(amount_msat=12300, label='inv1', description='description1', expiry=5) + l3.rpc.invoice(amount_msat=12300, label='inv2', description='description2', expiry=20) + l3.rpc.invoice(amount_msat=12300, label='inv3', description='description3', expiry=20) + inv4 = l3.rpc.invoice(amount_msat=12300, label='inv4', description='description4', expiry=2000) + inv5 = l3.rpc.invoice(amount_msat=12300, label='inv5', description='description5', expiry=2000) + l3.rpc.autoclean(subsystem='expiredinvoices', age=2) + assert l3.rpc.autoclean_status()['autoclean']['expiredinvoices']['enabled'] is True + assert l3.rpc.autoclean_status()['autoclean']['expiredinvoices']['age'] == 2 # Both should still be there. - assert l1.rpc.autoclean_status()['autoclean']['expiredinvoices']['cleaned'] == 0 - assert len(l1.rpc.listinvoices('inv1')['invoices']) == 1 - assert len(l1.rpc.listinvoices('inv2')['invoices']) == 1 - assert l1.rpc.listinvoices('inv1')['invoices'][0]['description'] == 'description1' + assert l3.rpc.autoclean_status()['autoclean']['expiredinvoices']['cleaned'] == 0 + assert len(l3.rpc.listinvoices('inv1')['invoices']) == 1 + assert len(l3.rpc.listinvoices('inv2')['invoices']) == 1 + assert l3.rpc.listinvoices('inv1')['invoices'][0]['description'] == 'description1' # First it expires. - wait_for(lambda: only_one(l1.rpc.listinvoices('inv1')['invoices'])['status'] == 'expired') + wait_for(lambda: only_one(l3.rpc.listinvoices('inv1')['invoices'])['status'] == 'expired') # Now will get autocleaned - wait_for(lambda: l1.rpc.listinvoices('inv1')['invoices'] == []) - assert l1.rpc.autoclean_status()['autoclean']['expiredinvoices']['cleaned'] == 1 + wait_for(lambda: l3.rpc.listinvoices('inv1')['invoices'] == []) + assert l3.rpc.autoclean_status()['autoclean']['expiredinvoices']['cleaned'] == 1 # Keeps settings across restarts - l1.restart() + l3.restart() - assert l1.rpc.autoclean_status()['autoclean']['expiredinvoices']['enabled'] is True - assert l1.rpc.autoclean_status()['autoclean']['expiredinvoices']['age'] == 2 - assert l1.rpc.autoclean_status()['autoclean']['expiredinvoices']['cleaned'] == 1 + assert l3.rpc.autoclean_status()['autoclean']['expiredinvoices']['enabled'] is True + assert l3.rpc.autoclean_status()['autoclean']['expiredinvoices']['age'] == 2 + assert l3.rpc.autoclean_status()['autoclean']['expiredinvoices']['cleaned'] == 1 # Disabling works - l1.rpc.autoclean(subsystem='expiredinvoices', age='never') - assert l1.rpc.autoclean_status()['autoclean']['expiredinvoices']['enabled'] is False - assert 'age' not in l1.rpc.autoclean_status()['autoclean']['expiredinvoices'] + l3.rpc.autoclean(subsystem='expiredinvoices', age='never') + assert l3.rpc.autoclean_status()['autoclean']['expiredinvoices']['enabled'] is False + assert 'age' not in l3.rpc.autoclean_status()['autoclean']['expiredinvoices'] # Same with inv2/3 - wait_for(lambda: only_one(l1.rpc.listinvoices('inv2')['invoices'])['status'] == 'expired') + wait_for(lambda: only_one(l3.rpc.listinvoices('inv2')['invoices'])['status'] == 'expired') # Give it time to notice. time.sleep(15) - assert l1.rpc.listinvoices('inv2')['invoices'] != [] + assert l3.rpc.listinvoices('inv2')['invoices'] != [] # Restart keeps it disabled. - l1.restart() - assert l1.rpc.autoclean_status()['autoclean']['expiredinvoices']['enabled'] is False - assert 'age' not in l1.rpc.autoclean_status()['autoclean']['expiredinvoices'] + l3.restart() + assert l3.rpc.autoclean_status()['autoclean']['expiredinvoices']['enabled'] is False + assert 'age' not in l3.rpc.autoclean_status()['autoclean']['expiredinvoices'] # Now enable: they will get autocleaned - l1.rpc.autoclean(subsystem='expiredinvoices', age=2) - wait_for(lambda: len(l1.rpc.listinvoices()['invoices']) == 2) - assert l1.rpc.autoclean_status()['autoclean']['expiredinvoices']['cleaned'] == 3 + l3.rpc.autoclean(subsystem='expiredinvoices', age=2) + wait_for(lambda: len(l3.rpc.listinvoices()['invoices']) == 2) + assert l3.rpc.autoclean_status()['autoclean']['expiredinvoices']['cleaned'] == 3 - # Reconnect, l0 pays invoice, we test paid expiry. - l1.rpc.connect(l0.info['id'], 'localhost', l0.port) - l0.rpc.pay(inv4['bolt11']) + # Reconnect, l1 pays invoice, we test paid expiry. + l2.rpc.connect(l3.info['id'], 'localhost', l3.port) + l1.rpc.pay(inv4['bolt11']) - # We manually delete inv5 so we can have l0 fail a payment. - l1.rpc.delinvoice('inv5', 'unpaid') + # We manually delete inv5 so we can have l1 fail a payment. + l3.rpc.delinvoice('inv5', 'unpaid') with pytest.raises(RpcError, match='WIRE_INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS'): - l0.rpc.pay(inv5['bolt11']) + l1.rpc.pay(inv5['bolt11']) - assert l1.rpc.autoclean_status()['autoclean']['paidinvoices']['enabled'] is False - assert l1.rpc.autoclean_status()['autoclean']['paidinvoices']['cleaned'] == 0 - l1.rpc.autoclean(subsystem='paidinvoices', age=1) - assert l1.rpc.autoclean_status()['autoclean']['paidinvoices']['enabled'] is True + assert l3.rpc.autoclean_status()['autoclean']['paidinvoices']['enabled'] is False + assert l3.rpc.autoclean_status()['autoclean']['paidinvoices']['cleaned'] == 0 + l3.rpc.autoclean(subsystem='paidinvoices', age=1) + assert l3.rpc.autoclean_status()['autoclean']['paidinvoices']['enabled'] is True - wait_for(lambda: l1.rpc.listinvoices()['invoices'] == []) - assert l1.rpc.autoclean_status()['autoclean']['expiredinvoices']['cleaned'] == 3 - assert l1.rpc.autoclean_status()['autoclean']['paidinvoices']['cleaned'] == 1 + wait_for(lambda: l3.rpc.listinvoices()['invoices'] == []) + assert l3.rpc.autoclean_status()['autoclean']['expiredinvoices']['cleaned'] == 3 + assert l3.rpc.autoclean_status()['autoclean']['paidinvoices']['cleaned'] == 1 - assert only_one(l0.rpc.listpays(inv5['bolt11'])['pays'])['status'] == 'failed' - assert only_one(l0.rpc.listpays(inv4['bolt11'])['pays'])['status'] == 'complete' - l0.rpc.autoclean(subsystem='failedpays', age=2) + assert only_one(l1.rpc.listpays(inv5['bolt11'])['pays'])['status'] == 'failed' + assert only_one(l1.rpc.listpays(inv4['bolt11'])['pays'])['status'] == 'complete' + l1.rpc.autoclean(subsystem='failedpays', age=2) - wait_for(lambda: l0.rpc.listpays(inv5['bolt11'])['pays'] == []) - assert l0.rpc.autoclean_status()['autoclean']['failedpays']['cleaned'] == 1 - assert l0.rpc.autoclean_status()['autoclean']['succeededpays']['cleaned'] == 0 + wait_for(lambda: l1.rpc.listpays(inv5['bolt11'])['pays'] == []) + assert l1.rpc.autoclean_status()['autoclean']['failedpays']['cleaned'] == 1 + assert l1.rpc.autoclean_status()['autoclean']['succeededpays']['cleaned'] == 0 - l0.rpc.autoclean(subsystem='succeededpays', age=2) - wait_for(lambda: l0.rpc.listpays(inv4['bolt11'])['pays'] == []) - assert l0.rpc.listsendpays() == {'payments': []} + l1.rpc.autoclean(subsystem='succeededpays', age=2) + wait_for(lambda: l1.rpc.listpays(inv4['bolt11'])['pays'] == []) + assert l1.rpc.listsendpays() == {'payments': []} + + # Now, we should have 1 failed forward, 1 success. + assert len(l2.rpc.listforwards(status='failed')['forwards']) == 1 + assert len(l2.rpc.listforwards(status='settled')['forwards']) == 1 + assert len(l2.rpc.listforwards()['forwards']) == 2 + + # Clean failed ones. + l2.rpc.autoclean(subsystem='failedforwards', age=2) + wait_for(lambda: l2.rpc.listforwards(status='failed')['forwards'] == []) + + assert len(l2.rpc.listforwards(status='settled')['forwards']) == 1 + assert l2.rpc.autoclean_status()['autoclean']['failedforwards']['cleaned'] == 1 + assert l2.rpc.autoclean_status()['autoclean']['succeededforwards']['cleaned'] == 0 + + # Clean succeeded ones + l2.rpc.autoclean(subsystem='succeededforwards', age=2) + wait_for(lambda: l2.rpc.listforwards(status='settled')['forwards'] == []) + assert l2.rpc.listforwards() == {'forwards': []} + assert l2.rpc.autoclean_status()['autoclean']['failedforwards']['cleaned'] == 1 + assert l2.rpc.autoclean_status()['autoclean']['succeededforwards']['cleaned'] == 1 def test_block_added_notifications(node_factory, bitcoind):