Browse Source

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.
paymod-01
Christian Decker 5 years ago
parent
commit
c59609c490
  1. 137
      plugins/libplugin-pay.c
  2. 8
      tests/test_pay.py

137
plugins/libplugin-pay.c

@ -1,3 +1,4 @@
#include "common/type_to_string.h"
#include <plugins/libplugin-pay.h> #include <plugins/libplugin-pay.h>
#include <stdio.h> #include <stdio.h>
@ -6,6 +7,23 @@
#include <ccan/tal/str/str.h> #include <ccan/tal/str/str.h>
#include <common/json_stream.h> #include <common/json_stream.h>
/* 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 *payment_new(tal_t *ctx, struct command *cmd,
struct payment *parent, struct payment *parent,
struct payment_modifier **mods) struct payment_modifier **mods)
@ -57,6 +75,51 @@ static struct command_result *payment_rpc_failure(struct command *cmd,
return command_still_pending(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, static struct command_result *payment_getinfo_success(struct command *cmd,
const char *buffer, const char *buffer,
const jsmntok_t *toks, 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; i<tal_count(p->children); 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 /* Function to bubble up completions to the root, which actually holds on to
* the command that initiated the flow. */ * the command that initiated the flow. */
static void payment_child_finished(struct payment *p, 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. */ * traversal, i.e., all children are finished before the parent is called. */
static void payment_finished(struct payment *p) static void payment_finished(struct payment *p)
{ {
if (p->parent == NULL) struct payment_tree_result result = payment_collect_result(p);
return command_fail(p->cmd, JSONRPC2_INVALID_REQUEST, "Not functional yet"); struct json_stream *ret;
else struct command *cmd = p->cmd;
return payment_child_finished(p->parent, p);
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) void payment_continue(struct payment *p)

8
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 * # noqa: F401,F403
from fixtures import TEST_NETWORK from fixtures import TEST_NETWORK
from flaky import flaky # noqa: F401 from flaky import flaky # noqa: F401
from hashlib import sha256
from pyln.client import RpcError, Millisatoshi from pyln.client import RpcError, Millisatoshi
from pyln.proto.onion import TlvPayload from pyln.proto.onion import TlvPayload
from utils import ( from utils import (
@ -3055,5 +3056,6 @@ def test_pay_modifiers(node_factory):
assert(hlp['command'] == 'paymod bolt11 [dummy]') assert(hlp['command'] == 'paymod bolt11 [dummy]')
inv = l2.rpc.invoice(123, 'lbl', 'desc')['bolt11'] inv = l2.rpc.invoice(123, 'lbl', 'desc')['bolt11']
with pytest.raises(RpcError, match="Not functional yet"): r = l1.rpc.paymod(inv)
l1.rpc.paymod(inv) assert(r['status'] == 'complete')
assert(sha256(unhexlify(r['payment_preimage'])).hexdigest() == r['payment_hash'])

Loading…
Cancel
Save