From 04c9c5e0d10f8ad9d08d39cc7416fc1e7f77b740 Mon Sep 17 00:00:00 2001 From: Christian Decker Date: Mon, 18 May 2020 14:44:45 +0200 Subject: [PATCH] paymod: Collect and return results of a tree of partial payments The status of what started as a simple JSON-RPC call is now spread across an entire tree of partial payments and payment attempts. So we collect the status in a single struct in order to report back success of failure. --- plugins/libplugin-pay.c | 137 ++++++++++++++++++++++++++++++++++++++-- tests/test_pay.py | 8 ++- 2 files changed, 138 insertions(+), 7 deletions(-) diff --git a/plugins/libplugin-pay.c b/plugins/libplugin-pay.c index 131534dcc..06fd91a75 100644 --- a/plugins/libplugin-pay.c +++ b/plugins/libplugin-pay.c @@ -1,3 +1,4 @@ +#include "common/type_to_string.h" #include #include @@ -6,6 +7,23 @@ #include #include +/* Just a container to collect a subtree result so we can summarize all + * sub-payments and return a reasonable result to the caller of `pay` */ +struct payment_tree_result { + /* OR of all the leafs in the subtree. */ + enum payment_step leafstates; + + /* OR of all the inner nodes and leaf nodes. */ + enum payment_step treestates; + + struct amount_msat sent; + + /* Preimage if any of the attempts succeeded. */ + struct preimage *preimage; + + u32 attempts; +}; + struct payment *payment_new(tal_t *ctx, struct command *cmd, struct payment *parent, struct payment_modifier **mods) @@ -57,6 +75,51 @@ static struct command_result *payment_rpc_failure(struct command *cmd, return command_still_pending(cmd); } +static struct payment_tree_result payment_collect_result(struct payment *p) +{ + struct payment_tree_result res; + size_t numchildren = tal_count(p->children); + res.sent = AMOUNT_MSAT(0); + /* If we didn't have a route, we didn't attempt. */ + res.attempts = p->route == NULL ? 0 : 1; + res.treestates = p->step; + res.leafstates = 0; + res.preimage = NULL; + + if (numchildren == 0) { + res.leafstates |= p->step; + if (p->result && p->result->state == PAYMENT_COMPLETE) { + res.sent = p->result->amount_sent; + res.preimage = p->result->payment_preimage; + } + } + + for (size_t i = 0; i < numchildren; i++) { + struct payment_tree_result cres = + payment_collect_result(p->children[i]); + + /* Some of our subpayments have succeeded, aggregate how much + * we sent in total. */ + if (!amount_msat_add(&res.sent, res.sent, cres.sent)) + plugin_err( + p->cmd->plugin, + "Number overflow summing partial payments: %s + %s", + type_to_string(tmpctx, struct amount_msat, + &res.sent), + type_to_string(tmpctx, struct amount_msat, + &cres.sent)); + + /* Bubble up the first preimage we see. */ + if (res.preimage == NULL && cres.preimage != NULL) + res.preimage = cres.preimage; + + res.leafstates |= cres.leafstates; + res.treestates |= cres.treestates; + res.attempts += cres.attempts; + } + return res; +} + static struct command_result *payment_getinfo_success(struct command *cmd, const char *buffer, const jsmntok_t *toks, @@ -455,6 +518,23 @@ static bool payment_is_finished(const struct payment *p) } } +static enum payment_step payment_aggregate_states(struct payment *p) +{ + enum payment_step agg = p->step; + + for (size_t i=0; ichildren); i++) + agg |= payment_aggregate_states(p->children[i]); + + return agg; +} + +/* A payment is finished if a) it is in a final state, of b) it's in a + * child-spawning state and all of its children are in a final state. */ +static bool payment_is_success(struct payment *p) +{ + return (payment_aggregate_states(p) & PAYMENT_STEP_SUCCESS) != 0; +} + /* Function to bubble up completions to the root, which actually holds on to * the command that initiated the flow. */ static void payment_child_finished(struct payment *p, @@ -473,10 +553,59 @@ static void payment_child_finished(struct payment *p, * traversal, i.e., all children are finished before the parent is called. */ static void payment_finished(struct payment *p) { - if (p->parent == NULL) - return command_fail(p->cmd, JSONRPC2_INVALID_REQUEST, "Not functional yet"); - else - return payment_child_finished(p->parent, p); + struct payment_tree_result result = payment_collect_result(p); + struct json_stream *ret; + struct command *cmd = p->cmd; + + p->end_time = time_now(); + + /* Either none of the leaf attempts succeeded yet, or we have a + * preimage. */ + assert((result.leafstates & PAYMENT_STEP_SUCCESS) == 0 || + result.preimage != NULL); + + if (p->parent == NULL) { + assert(p->cmd != NULL); + if (payment_is_success(p)) { + assert(result.treestates & PAYMENT_STEP_SUCCESS); + assert(result.leafstates & PAYMENT_STEP_SUCCESS); + assert(result.preimage != NULL); + + ret = jsonrpc_stream_success(p->cmd); + json_add_sha256(ret, "payment_hash", p->payment_hash); + json_add_num(ret, "parts", result.attempts); + + json_add_amount_msat_compat(ret, p->amount, "msatoshi", + "amount_msat"); + json_add_amount_msat_compat(ret, result.sent, + "msatoshi_sent", + "amount_sent_msat"); + + if (result.leafstates != PAYMENT_STEP_SUCCESS) + json_add_string( + ret, "warning", + "Some parts of the payment are not yet " + "completed, but we have the confirmation " + "from the recipient."); + json_add_preimage(ret, "payment_preimage", result.preimage); + + json_add_string(ret, "status", "complete"); + + /* Unset the pointer to the cmd so we don't attempt to + * return a response twice. */ + p->cmd = NULL; + if (command_finished(cmd, ret)) {/* Ignore result. */} + return; + } else { + if (command_fail(p->cmd, JSONRPC2_INVALID_REQUEST, + "Not functional yet")) {/* Ignore result. */} + + return; + } + } else { + payment_child_finished(p->parent, p); + return; + } } void payment_continue(struct payment *p) diff --git a/tests/test_pay.py b/tests/test_pay.py index a824239d6..dba536784 100644 --- a/tests/test_pay.py +++ b/tests/test_pay.py @@ -1,7 +1,8 @@ -from binascii import hexlify +from binascii import hexlify, unhexlify from fixtures import * # noqa: F401,F403 from fixtures import TEST_NETWORK from flaky import flaky # noqa: F401 +from hashlib import sha256 from pyln.client import RpcError, Millisatoshi from pyln.proto.onion import TlvPayload from utils import ( @@ -3055,5 +3056,6 @@ def test_pay_modifiers(node_factory): assert(hlp['command'] == 'paymod bolt11 [dummy]') inv = l2.rpc.invoice(123, 'lbl', 'desc')['bolt11'] - with pytest.raises(RpcError, match="Not functional yet"): - l1.rpc.paymod(inv) + r = l1.rpc.paymod(inv) + assert(r['status'] == 'complete') + assert(sha256(unhexlify(r['payment_preimage'])).hexdigest() == r['payment_hash'])