diff --git a/CHANGELOG.md b/CHANGELOG.md index 14a711abaff6..f8d5af6ce769 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,10 +10,12 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ### Added - plugins: fully enabled, and ready for you to write some! +- plugins: `pay` is now a plugin. - lightning-cli: `help ` finds man pages even if `make install` not run. - JSON API: `waitsendpay` now has an `erring_direction` field. - JSON API: `listpeers` now has a `direction` field in `channels`. - JSON API: `listchannels` now takes a `source` option to filter by node id. +- JSON API: New command `paystatus` gives detailed information on `pay` commands. ### Changed diff --git a/Makefile b/Makefile index cac89c7ca759..fdbeff25035e 100644 --- a/Makefile +++ b/Makefile @@ -216,6 +216,7 @@ include lightningd/Makefile include cli/Makefile include doc/Makefile include devtools/Makefile +include plugins/Makefile # Git doesn't maintain timestamps, so we only regen if git says we should. CHANGED_FROM_GIT = [ x"`git log $@ | head -n1`" != x"`git log $< | head -n1`" -o x"`git diff $<`" != x"" ] diff --git a/common/daemon.c b/common/daemon.c index 67fc16e39638..cf8f46ae96e3 100644 --- a/common/daemon.c +++ b/common/daemon.c @@ -152,10 +152,10 @@ void daemon_shutdown(void) wally_cleanup(0); } -void daemon_maybe_debug(int argc, char *argv[]) +void daemon_maybe_debug(char *argv[]) { #if DEVELOPER - for (int i = 1; i < argc; i++) { + for (int i = 1; argv[i]; i++) { if (!streq(argv[i], "--debugger")) continue; diff --git a/common/daemon.h b/common/daemon.h index d252dff3a5de..5a0a01fb0b0f 100644 --- a/common/daemon.h +++ b/common/daemon.h @@ -15,7 +15,7 @@ int daemon_poll(struct pollfd *fds, nfds_t nfds, int timeout); void daemon_shutdown(void); /* Kick in a debugger if they set --debugger */ -void daemon_maybe_debug(int argc, char *argv[]); +void daemon_maybe_debug(char *argv[]); struct backtrace_state *backtrace_state; diff --git a/common/status.c b/common/status.c index c22ffffab462..3eef72264409 100644 --- a/common/status.c +++ b/common/status.c @@ -73,9 +73,9 @@ void status_send(const u8 *msg TAKES) { report_logging_io("SIGUSR1"); if (status_fd >= 0) { - int type =fromwire_peektype(msg); if (!wire_sync_write(status_fd, msg)) - err(1, "Writing out status %i", type); + /* No point printing error if lightningd is dead. */ + exit(1); } else { daemon_conn_send(status_conn, msg); } diff --git a/common/subdaemon.c b/common/subdaemon.c index 58349f343b80..f54ec168afaf 100644 --- a/common/subdaemon.c +++ b/common/subdaemon.c @@ -37,7 +37,7 @@ void subdaemon_setup(int argc, char *argv[]) logging_io = true; } - daemon_maybe_debug(argc, argv); + daemon_maybe_debug(argv); #if DEVELOPER for (int i = 1; i < argc; i++) { diff --git a/gossipd/routing.c b/gossipd/routing.c index 3d3d0b0dd5b7..c1384b963cce 100644 --- a/gossipd/routing.c +++ b/gossipd/routing.c @@ -1561,6 +1561,7 @@ struct route_hop *get_route(const tal_t *ctx, struct routing_state *rstate, hops[i].nodeid = n->id; hops[i].amount = total_amount; hops[i].delay = total_delay; + hops[i].direction = idx; total_amount += connection_fee(c, total_amount); total_delay += c->delay; n = other_node(n, route[i]); diff --git a/gossipd/routing.h b/gossipd/routing.h index 1cb9ee237156..73c2fd660872 100644 --- a/gossipd/routing.h +++ b/gossipd/routing.h @@ -207,6 +207,7 @@ get_channel(const struct routing_state *rstate, struct route_hop { struct short_channel_id channel_id; + int direction; struct pubkey nodeid; u64 amount; u32 delay; diff --git a/lightningd/Makefile b/lightningd/Makefile index eaf3a5e1e3a5..854c91674cef 100644 --- a/lightningd/Makefile +++ b/lightningd/Makefile @@ -79,7 +79,6 @@ LIGHTNINGD_SRC := \ lightningd/opening_control.c \ lightningd/options.c \ lightningd/pay.c \ - lightningd/payalgo.c \ lightningd/peer_control.c \ lightningd/peer_htlcs.c \ lightningd/ping.c \ diff --git a/lightningd/gossip_msg.c b/lightningd/gossip_msg.c index 20d84af9bf6a..39155fbeafa6 100644 --- a/lightningd/gossip_msg.c +++ b/lightningd/gossip_msg.c @@ -64,13 +64,16 @@ void fromwire_route_hop(const u8 **pptr, size_t *max, struct route_hop *entry) { fromwire_pubkey(pptr, max, &entry->nodeid); fromwire_short_channel_id(pptr, max, &entry->channel_id); + entry->direction = fromwire_u8(pptr, max); entry->amount = fromwire_u64(pptr, max); entry->delay = fromwire_u32(pptr, max); } + void towire_route_hop(u8 **pptr, const struct route_hop *entry) { towire_pubkey(pptr, &entry->nodeid); towire_short_channel_id(pptr, &entry->channel_id); + towire_u8(pptr, entry->direction); towire_u64(pptr, entry->amount); towire_u32(pptr, entry->delay); } diff --git a/lightningd/json.c b/lightningd/json.c index 88614747ebdd..cee09ff01797 100644 --- a/lightningd/json.c +++ b/lightningd/json.c @@ -30,6 +30,7 @@ json_add_route_hop(struct json_stream *r, char const *n, json_add_pubkey(r, "id", &h->nodeid); json_add_short_channel_id(r, "channel", &h->channel_id); + json_add_num(r, "direction", h->direction); json_add_u64(r, "msatoshi", h->amount); json_add_num(r, "delay", h->delay); json_object_end(r); diff --git a/lightningd/pay.c b/lightningd/pay.c index aed9206f65f7..53e5decec277 100644 --- a/lightningd/pay.c +++ b/lightningd/pay.c @@ -1004,13 +1004,14 @@ static struct command_result *json_sendpay(struct command *cmd, u64 *amount; struct pubkey *id; struct short_channel_id *channel; - unsigned *delay; + unsigned *delay, *direction; if (!param(cmd, buffer, t, p_req("msatoshi", param_u64, &amount), p_req("id", param_pubkey, &id), p_req("delay", param_number, &delay), p_req("channel", param_short_channel_id, &channel), + p_opt("direction", param_number, &direction), NULL)) return command_param_failed(); @@ -1020,6 +1021,8 @@ static struct command_result *json_sendpay(struct command *cmd, route[n_hops].nodeid = *id; route[n_hops].delay = *delay; route[n_hops].channel_id = *channel; + /* FIXME: Actually ignored by sending code! */ + route[n_hops].direction = direction ? *direction : 0; n_hops++; } diff --git a/lightningd/payalgo.c b/lightningd/payalgo.c deleted file mode 100644 index e00936b82a8c..000000000000 --- a/lightningd/payalgo.c +++ /dev/null @@ -1,702 +0,0 @@ -#include "pay.h" -#include "payalgo.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -/* Record of failures. */ -enum pay_failure_type { - FAIL_UNPARSEABLE_ONION, - FAIL_PAYMENT_REPLY -}; -/* A payment failure. */ -struct pay_failure { - /* Part of pay_failures list of struct pay */ - struct list_node list; - /* The type of payment failure */ - enum pay_failure_type type; - /* A tal_arr of route hops, whose parent is - * this struct */ - struct route_hop *route; - /* The raw onion reply, if TYPE_UNPARSEABLE_ONION, a - * tal_arr whose parent is this struct */ - const u8 *onionreply; - /* The routing failure, if TYPE_PAYMENT_REPLY, a tal - * object whose parent is this struct */ - struct routing_failure *routing_failure; - /* The detail of the routing failure. A tal_arr - * string whose parent is this struct. */ - char *details; -}; - -/* Output a pay failure */ -static void -json_add_failure(struct json_stream *r, char const *n, - const struct pay_failure *f) -{ - struct routing_failure *rf; - json_object_start(r, n); - json_add_string(r, "message", f->details); - switch (f->type) { - case FAIL_UNPARSEABLE_ONION: - json_add_string(r, "type", "FAIL_UNPARSEABLE_ONION"); - json_add_hex_talarr(r, "onionreply", f->onionreply); - break; - - case FAIL_PAYMENT_REPLY: - rf = f->routing_failure; - json_add_string(r, "type", "FAIL_PAYMENT_REPLY"); - json_add_num(r, "erring_index", rf->erring_index); - json_add_num(r, "failcode", (unsigned) rf->failcode); - json_add_pubkey(r, "erring_node", &rf->erring_node); - json_add_short_channel_id(r, "erring_channel", - &rf->erring_channel); - if (rf->channel_update) - json_add_hex_talarr(r, "channel_update", - rf->channel_update); - break; - } - json_add_route(r, "route", f->route, tal_count(f->route)); - json_object_end(r); -} - -/* Output an array of payment failures. */ -static void -json_add_failures(struct json_stream *r, char const *n, - const struct list_head *fs) -{ - struct pay_failure *f; - json_array_start(r, n); - list_for_each(fs, f, list) { - json_add_failure(r, NULL, f); - } - json_array_end(r); -} - -/* Pay command */ -struct pay { - /* Parent command. */ - struct command *cmd; - - /* Bolt11 details */ - struct sha256 payment_hash; - struct pubkey receiver_id; - struct timeabs expiry; - u32 min_final_cltv_expiry; - - /* Command details */ - u64 msatoshi; - double riskfactor; - double maxfeepercent; - u32 maxdelay; - - /* Number of getroute and sendpay tries */ - unsigned int getroute_tries; - unsigned int sendpay_tries; - - /* Current fuzz we pass into getroute. */ - double fuzz; - - /* Parent of the current pay attempt. This object is - * freed, then allocated at the start of each pay - * attempt to ensure no leaks across long pay attempts */ - char *try_parent; - - /* Current route being attempted. */ - struct route_hop *route; - - /* List of failures to pay. */ - struct list_head pay_failures; - - /* Whether we are attempting payment or not. */ - bool in_sendpay; - - /* Maximum fee that is exempted from the maxfeepercent computation. This - * is mainly useful for tiny transfers for which the leveraged fee would - * be dominated by the forwarding fee. */ - u64 exemptfee; - - /* The description from the bolt11 string */ - const char *description; -}; - -static struct routing_failure * -dup_routing_failure(const tal_t *ctx, const struct routing_failure *fail) -{ - struct routing_failure *nobj = tal(ctx, struct routing_failure); - - nobj->erring_index = fail->erring_index; - nobj->failcode = fail->failcode; - nobj->erring_node = fail->erring_node; - nobj->erring_channel = fail->erring_channel; - if (fail->channel_update) - nobj->channel_update = tal_dup_arr(nobj, u8, - fail->channel_update, - tal_count(fail->channel_update), - 0); - else - nobj->channel_update = NULL; - - return nobj; -} - -/* Add a pay_failure from a sendpay_result */ -static void -add_pay_failure(struct pay *pay, - const struct sendpay_result *r) -{ - struct pay_failure *f = tal(pay, struct pay_failure); - - /* Append to tail */ - list_add_tail(&pay->pay_failures, &f->list); - - switch (r->errorcode) { - case PAY_UNPARSEABLE_ONION: - f->type = FAIL_UNPARSEABLE_ONION; - f->onionreply = tal_dup_arr(f, u8, r->onionreply, - tal_count(r->onionreply), 0); - break; - - case PAY_TRY_OTHER_ROUTE: - f->type = FAIL_PAYMENT_REPLY; - f->routing_failure = dup_routing_failure(f, - r->routing_failure); - break; - - /* All other errors are disallowed */ - default: - abort(); - } - f->details = tal_strdup(f, r->details); - /* Grab the route */ - f->route = tal_steal(f, pay->route); - pay->route = NULL; -} - -static void -json_pay_success(struct pay *pay, - const struct sendpay_result *r) -{ - struct command *cmd = pay->cmd; - struct json_stream *response; - - response = json_stream_success(cmd); - json_object_start(response, NULL); - json_add_payment_fields(response, r->payment); - json_add_num(response, "getroute_tries", pay->getroute_tries); - json_add_num(response, "sendpay_tries", pay->sendpay_tries); - json_add_route(response, "route", - pay->route, tal_count(pay->route)); - json_add_failures(response, "failures", &pay->pay_failures); - json_object_end(response); - was_pending(command_success(cmd, response)); -} - -static void json_pay_failure(struct pay *pay, - const struct sendpay_result *r) -{ - struct json_stream *data; - struct routing_failure *fail; - - assert(!r->succeeded); - - switch (r->errorcode) { - case PAY_IN_PROGRESS: - data = json_stream_fail(pay->cmd, r->errorcode, r->details); - json_object_start(data, NULL); - json_add_num(data, "getroute_tries", pay->getroute_tries); - json_add_num(data, "sendpay_tries", pay->sendpay_tries); - json_add_payment_fields(data, r->payment); - json_add_failures(data, "failures", &pay->pay_failures); - json_object_end(data); - was_pending(command_failed(pay->cmd, data)); - return; - - case PAY_RHASH_ALREADY_USED: - case PAY_STOPPED_RETRYING: - data = json_stream_fail(pay->cmd, r->errorcode, r->details); - json_object_start(data, NULL); - json_add_num(data, "getroute_tries", pay->getroute_tries); - json_add_num(data, "sendpay_tries", pay->sendpay_tries); - json_add_failures(data, "failures", &pay->pay_failures); - json_object_end(data); - was_pending(command_failed(pay->cmd, data)); - return; - - case PAY_UNPARSEABLE_ONION: - /* Impossible case */ - abort(); - - case PAY_DESTINATION_PERM_FAIL: - fail = r->routing_failure; - assert(r->details != NULL); - - data = json_stream_fail(pay->cmd, - r->errorcode, - tal_fmt(tmpctx, "failed: %s (%s)", - onion_type_name(fail->failcode), - r->details)); - json_object_start(data, NULL); - json_add_num(data, "erring_index", - fail->erring_index); - json_add_num(data, "failcode", - (unsigned) fail->failcode); - json_add_pubkey(data, "erring_node", &fail->erring_node); - json_add_short_channel_id(data, "erring_channel", - &fail->erring_channel); - if (fail->channel_update) - json_add_hex_talarr(data, "channel_update", - fail->channel_update); - json_add_failures(data, "failures", &pay->pay_failures); - json_object_end(data); - was_pending(command_failed(pay->cmd, data)); - return; - - case PAY_TRY_OTHER_ROUTE: - /* Impossible case */ - abort(); - return; - } - abort(); -} - -/* Determine if we should delay before retrying. Return a reason - * string, or NULL if we will not retry */ -static const char *should_delay_retry(const tal_t *ctx, - const struct sendpay_result *r) -{ - /* The routing failures WIRE_EXPIRY_TOO_FAR, WIRE_EXPIRY_TOO_SOON, - * and WIRE_FINAL_EXPIRY_TOO_SOON may arise due to disagreement - * between the peers about what the block heights are. So - * delay for those before retrying. */ - if (!r->succeeded && r->errorcode == PAY_TRY_OTHER_ROUTE) { - switch (r->routing_failure->failcode) { - case WIRE_EXPIRY_TOO_FAR: - case WIRE_EXPIRY_TOO_SOON: - case WIRE_FINAL_EXPIRY_TOO_SOON: - return tal_fmt(ctx, - "Possible blockheight disagreement " - "(%s from peer)", - onion_type_name(r->routing_failure->failcode)); - - default: - /* Do nothing */ ; - } - } - - return NULL; -} - -/* Start a payment attempt. */ -static struct command_result *json_pay_try(struct pay *pay); - -/* Used when delaying. */ -static void do_pay_try(struct pay *pay) -{ - log_info(pay->cmd->ld->log, "pay(%p): Try another route", pay); - json_pay_try(pay); -} - -/* Call when sendpay returns to us. */ -static void json_pay_sendpay_resolve(const struct sendpay_result *r, - void *vpay) -{ - struct pay *pay = (struct pay *) vpay; - char const *why; - - pay->in_sendpay = false; - - /* If we succeed, hurray */ - if (r->succeeded) { - log_info(pay->cmd->ld->log, "pay(%p): Success", pay); - json_pay_success(pay, r); - return; - } - - /* We can retry only if it is one of the retryable errors - * below. If it is not, fail now. */ - if (r->errorcode != PAY_UNPARSEABLE_ONION && - r->errorcode != PAY_TRY_OTHER_ROUTE) { - log_info(pay->cmd->ld->log, "pay(%p): Failed, reporting to caller", pay); - json_pay_failure(pay, r); - return; - } - - add_pay_failure(pay, r); - - /* Should retry here, question is whether to retry now or later */ - - why = should_delay_retry(pay->try_parent, r); - if (why) { - /* We have some reason to delay retrying. */ - - log_info(pay->cmd->ld->log, - "pay(%p): Delay before retry: %s", pay, why); - - /* Clear previous try memory. */ - pay->try_parent = tal_free(pay->try_parent); - pay->try_parent = tal(pay, char); - - /* Delay for 3 seconds if needed. FIXME: random - * exponential backoff */ - new_reltimer(&pay->cmd->ld->timers, pay->try_parent, - time_from_sec(3), - &do_pay_try, pay); - } else - do_pay_try(pay); -} - -/* Generates a string describing the route. Route should be a - * tal_arr */ -static char const *stringify_route(const tal_t *ctx, struct route_hop *route) -{ - size_t i; - char *rv = tal_strdup(ctx, "us"); - for (i = 0; i < tal_count(route); ++i) - tal_append_fmt(&rv, " -> %s (%"PRIu64"msat, %"PRIu32"blk) -> %s", - type_to_string(ctx, struct short_channel_id, &route[i].channel_id), - route[i].amount, route[i].delay, - type_to_string(ctx, struct pubkey, &route[i].nodeid)); - return rv; -} - -static void log_route(struct pay *pay, struct route_hop *route) -{ - log_info(pay->cmd->ld->log, "pay(%p): sendpay via route: %s", - pay, stringify_route(tmpctx, route)); -} - -static void json_pay_sendpay_resume(const struct sendpay_result *r, - void *vpay) -{ - struct pay *pay = (struct pay *) vpay; - bool completed = r->succeeded || r->errorcode != PAY_IN_PROGRESS; - - if (completed) - /* Already completed. */ - json_pay_sendpay_resolve(r, pay); - else { - /* Clear previous try memory. */ - pay->try_parent = tal_free(pay->try_parent); - pay->try_parent = tal(pay, char); - - /* Not yet complete? Wait for it. */ - wait_payment(pay->try_parent, pay->cmd->ld, &pay->payment_hash, - json_pay_sendpay_resolve, pay); - } -} - -static void json_pay_getroute_reply(struct subd *gossip UNUSED, - const u8 *reply, const int *fds UNUSED, - struct pay *pay) -{ - struct route_hop *route; - u64 msatoshi_sent; - u64 fee; - double feepercent; - bool fee_too_high; - bool delay_too_high; - struct json_stream *data; - char const *err; - - fromwire_gossip_getroute_reply(reply, reply, &route); - - if (tal_count(route) == 0) { - data = json_stream_fail(pay->cmd, PAY_ROUTE_NOT_FOUND, - "Could not find a route"); - json_object_start(data, NULL); - json_add_num(data, "getroute_tries", pay->getroute_tries); - json_add_num(data, "sendpay_tries", pay->sendpay_tries); - json_add_failures(data, "failures", &pay->pay_failures); - json_object_end(data); - was_pending(command_failed(pay->cmd, data)); - return; - } - - msatoshi_sent = route[0].amount; - fee = msatoshi_sent - pay->msatoshi; - /* Casting u64 to double will lose some precision. The loss of precision - * in feepercent will be like 3.0000..(some dots)..1 % - 3.0 %. - * That loss will not be representable in double. So, it's Okay to - * cast u64 to double for feepercent calculation. */ - feepercent = ((double) fee) * 100.0 / ((double) pay->msatoshi); - fee_too_high = (fee > pay->exemptfee && feepercent > pay->maxfeepercent); - delay_too_high = (route[0].delay > pay->maxdelay); - /* compare fuzz to range */ - if ((fee_too_high || delay_too_high) && pay->fuzz < 0.01) { - err = ""; - if (fee_too_high) - err = tal_fmt(pay, - "Fee %"PRIu64" is %f%% " - "of payment %"PRIu64"; " - "max fee requested is %f%%.", - fee, feepercent, - pay->msatoshi, - pay->maxfeepercent); - if (fee_too_high && delay_too_high) - err = tal_fmt(pay, "%s ", err); - if (delay_too_high) - err = tal_fmt(pay, - "%s" - "Delay (locktime) is %"PRIu32" blocks; " - "max delay requested is %u.", - err, route[0].delay, pay->maxdelay); - - data = json_stream_fail(pay->cmd, PAY_ROUTE_TOO_EXPENSIVE, err); - json_object_start(data, NULL); - json_add_u64(data, "msatoshi", pay->msatoshi); - json_add_u64(data, "fee", fee); - json_add_double(data, "feepercent", feepercent); - json_add_double(data, "maxfeepercent", pay->maxfeepercent); - json_add_u64(data, "delay", (u64) route[0].delay); - json_add_num(data, "maxdelay", pay->maxdelay); - json_add_num(data, "getroute_tries", pay->getroute_tries); - json_add_num(data, "sendpay_tries", pay->sendpay_tries); - json_add_route(data, "route", - route, tal_count(route)); - json_add_failures(data, "failures", &pay->pay_failures); - json_object_end(data); - - was_pending(command_failed(pay->cmd, data)); - return; - } - if (fee_too_high || delay_too_high) { - /* Retry with lower fuzz */ - pay->fuzz -= 0.15; - if (pay->fuzz <= 0.0) - pay->fuzz = 0.0; - json_pay_try(pay); - return; - } - - ++pay->sendpay_tries; - - log_route(pay, route); - assert(!pay->route); - pay->route = tal_dup_arr(pay, struct route_hop, route, - tal_count(route), 0); - - pay->in_sendpay = true; - send_payment(pay->try_parent, - pay->cmd->ld, &pay->payment_hash, route, - pay->msatoshi, - pay->description, - &json_pay_sendpay_resume, pay); -} - -/* Start a payment attempt. Return NULL if deferred, otherwise - * command_failed(). */ -static struct command_result *json_pay_try(struct pay *pay) -{ - u8 *req; - struct command *cmd = pay->cmd; - struct timeabs now = time_now(); - u64 maxoverpayment; - u64 overpayment; - - /* If too late anyway, fail now. */ - if (time_after(now, pay->expiry)) { - struct json_stream *data - = json_stream_fail(cmd, PAY_INVOICE_EXPIRED, - "Invoice expired"); - json_object_start(data, NULL); - json_add_num(data, "now", now.ts.tv_sec); - json_add_num(data, "expiry", pay->expiry.ts.tv_sec); - json_add_num(data, "getroute_tries", pay->getroute_tries); - json_add_num(data, "sendpay_tries", pay->sendpay_tries); - json_add_failures(data, "failures", &pay->pay_failures); - json_object_end(data); - return command_failed(cmd, data); - } - - /* Clear previous try memory. */ - pay->try_parent = tal_free(pay->try_parent); - pay->try_parent = tal(pay, char); - - /* Clear route */ - pay->route = tal_free(pay->route); - - /* Generate an overpayment, from fuzz * maxfee. */ - /* Now normally the use of double for money is very bad. - * Note however that a later stage will ensure that - * we do not end up paying more than maxfeepercent - * of the msatoshi we intend to pay. */ - maxoverpayment = ((double) pay->msatoshi * pay->fuzz * pay->maxfeepercent) - / 100.0; - if (maxoverpayment > 0) { - /* We will never generate the maximum computed - * overpayment this way. Maybe OK for most - * purposes. */ - overpayment = pseudorand(maxoverpayment); - } else - overpayment = 0; - - ++pay->getroute_tries; - - /* FIXME: use b11->routes */ - req = towire_gossip_getroute_request(pay->try_parent, - &cmd->ld->id, - &pay->receiver_id, - pay->msatoshi + overpayment, - pay->riskfactor, - pay->min_final_cltv_expiry, - &pay->fuzz, NULL, - ROUTING_MAX_HOPS); - subd_req(pay->try_parent, cmd->ld->gossip, req, -1, 0, json_pay_getroute_reply, pay); - - return NULL; -} - -static void json_pay_stop_retrying(struct pay *pay) -{ - struct sendpay_result *sr; - - sr = tal(pay, struct sendpay_result); - sr->succeeded = false; - if (pay->in_sendpay) { - /* Still in sendpay. Return with PAY_IN_PROGRESS */ - sr->errorcode = PAY_IN_PROGRESS; - sr->payment = wallet_payment_by_hash(sr, - pay->cmd->ld->wallet, - &pay->payment_hash); - sr->details = "Stopped retrying during payment attempt; " - "continue monitoring with " - "pay or listpayments"; - } else { - /* Outside sendpay, no ongoing payment */ - sr->errorcode = PAY_STOPPED_RETRYING; - sr->details = "Stopped retrying, no ongoing payment"; - } - json_pay_failure(pay, sr); -} - -static struct command_result *json_pay(struct command *cmd, - const char *buffer, - const jsmntok_t *obj UNNEEDED, - const jsmntok_t *params) -{ - double *riskfactor; - double *maxfeepercent; - u64 *msatoshi; - struct pay *pay = tal(cmd, struct pay); - struct bolt11 *b11; - const char *b11str, *desc; - char *fail; - unsigned int *retryfor; - unsigned int *maxdelay; - unsigned int *exemptfee; - struct command_result *res; - - if (!param(cmd, buffer, params, - p_req("bolt11", param_string, &b11str), - p_opt("msatoshi", param_u64, &msatoshi), - p_opt("description", param_string, &desc), - p_opt_def("riskfactor", param_double, &riskfactor, 1.0), - p_opt_def("maxfeepercent", param_percent, &maxfeepercent, 0.5), - p_opt_def("retry_for", param_number, &retryfor, 60), - p_opt_def("maxdelay", param_number, &maxdelay, - cmd->ld->config.locktime_max), - p_opt_def("exemptfee", param_number, &exemptfee, 5000), - NULL)) - return command_param_failed(); - - b11 = bolt11_decode(pay, b11str, desc, &fail); - if (!b11) { - return command_fail(cmd, JSONRPC2_INVALID_PARAMS, - "Invalid bolt11: %s", fail); - } - - pay->cmd = cmd; - pay->payment_hash = b11->payment_hash; - pay->receiver_id = b11->receiver_id; - memset(&pay->expiry, 0, sizeof(pay->expiry)); - pay->expiry.ts.tv_sec = b11->timestamp + b11->expiry; - pay->min_final_cltv_expiry = b11->min_final_cltv_expiry; - pay->exemptfee = *exemptfee; - - if (b11->msatoshi) { - if (msatoshi) { - return command_fail(cmd, JSONRPC2_INVALID_PARAMS, - "msatoshi parameter unnecessary"); - } - msatoshi = b11->msatoshi; - } else { - if (!msatoshi) { - return command_fail(cmd, JSONRPC2_INVALID_PARAMS, - "msatoshi parameter required"); - } - } - pay->msatoshi = *msatoshi; - pay->riskfactor = *riskfactor * 1000; - pay->maxfeepercent = *maxfeepercent; - - if (*maxdelay < pay->min_final_cltv_expiry) { - return command_fail(cmd, JSONRPC2_INVALID_PARAMS, - "maxdelay (%u) must be greater than " - "min_final_cltv_expiry (%"PRIu32") of " - "invoice", - *maxdelay, pay->min_final_cltv_expiry); - } - pay->maxdelay = *maxdelay; - - pay->getroute_tries = 0; - pay->sendpay_tries = 0; - /* Higher fuzz increases the potential fees we will pay, since - * higher fuzz makes it more likely that high-fee paths get - * selected. We start with very high fuzz, but if the - * returned route is too expensive for the given - * `maxfeepercent` or `maxdelay` we reduce the fuzz. - * Starting with high - * fuzz means, if the user allows high fee/locktime, we can take - * advantage of that to increase randomization and - * improve privacy somewhat. */ - pay->fuzz = 0.75; - pay->try_parent = NULL; - /* Start with no route */ - pay->route = NULL; - /* Start with no failures */ - list_head_init(&pay->pay_failures); - pay->in_sendpay = false; - pay->description = b11->description; - - /* Initiate payment */ - res = json_pay_try(pay); - if (res) - return res; - - /* Set up timeout. */ - new_reltimer(&cmd->ld->timers, pay, time_from_sec(*retryfor), - &json_pay_stop_retrying, pay); - return command_still_pending(cmd); -} - -static const struct json_command pay_command = { - "pay", - json_pay, - "Send payment specified by {bolt11} with {msatoshi} " - "(ignored if {bolt11} has an amount), " - "{description} (required if {bolt11} uses description hash), " - "{riskfactor} (default 1.0), " - "{maxfeepercent} (default 0.5) the maximum acceptable fee as a percentage (e.g. 0.5 => 0.5%), " - "{exemptfee} (default 5000 msat) disables the maxfeepercent check for fees below the threshold, " - "{retry_for} (default 60) the integer number of seconds before we stop retrying, and " - "{maxdelay} (default 500) the maximum number of blocks we allow the funds to possibly get locked" -}; -AUTODATA(json_command, &pay_command); diff --git a/lightningd/payalgo.h b/lightningd/payalgo.h deleted file mode 100644 index 971b80ba29e2..000000000000 --- a/lightningd/payalgo.h +++ /dev/null @@ -1,5 +0,0 @@ -#ifndef LIGHTNING_LIGHTNINGD_PAYALGO_H -#define LIGHTNING_LIGHTNINGD_PAYALGO_H -#include "config.h" - -#endif /* LIGHTNING_LIGHTNINGD_PAYALGO_H */ diff --git a/plugins/.gitignore b/plugins/.gitignore new file mode 100644 index 000000000000..65847d8a1fb4 --- /dev/null +++ b/plugins/.gitignore @@ -0,0 +1 @@ +pay diff --git a/plugins/Makefile b/plugins/Makefile new file mode 100644 index 000000000000..1a128bd24e7f --- /dev/null +++ b/plugins/Makefile @@ -0,0 +1,51 @@ +PLUGIN_PAY_SRC := plugins/pay.c +PLUGIN_PAY_OBJS := $(PLUGIN_PAY_SRC:.c=.o) + +PLUGIN_LIB_SRC := plugins/libplugin.c +PLUGIN_LIB_HEADER := plugins/libplugin.h +PLUGIN_LIB_OBJS := $(PLUGIN_LIB_SRC:.c=.o) + +PLUGIN_COMMON_OBJS := \ + bitcoin/chainparams.o \ + bitcoin/pubkey.o \ + bitcoin/pullpush.o \ + bitcoin/script.o \ + bitcoin/shadouble.o \ + bitcoin/short_channel_id.o \ + bitcoin/signature.o \ + bitcoin/tx.o \ + bitcoin/varint.o \ + common/bech32.o \ + common/bech32_util.o \ + common/bolt11.o \ + common/daemon.o \ + common/hash_u5.o \ + common/json.o \ + common/json_escaped.o \ + common/json_helpers.o \ + common/json_tok.o \ + common/memleak.o \ + common/param.o \ + common/pseudorand.o \ + common/type_to_string.o \ + common/utils.o \ + common/version.o \ + wire/fromwire.o \ + wire/towire.o + +plugins/pay: $(PLUGIN_PAY_OBJS) $(PLUGIN_LIB_OBJS) $(PLUGIN_COMMON_OBJS) $(JSMN_OBJS) $(CCAN_OBJS) + +$(PLUGIN_PAY_OBJS) $(PLUGIN_LIB_OBJS): $(PLUGIN_LIB_HEADER) + +# Make sure these depend on everything. +ALL_PROGRAMS += plugins/pay +ALL_OBJS += $(PLUGIN_PAY_OBJS) $(PLUGIN_LIB_OBJS) + +check-source: $(PLUGIN_PAY_SRC:%=check-src-include-order/%) +check-source-bolt: $(PLUGIN_PAY_SRC:%=bolt-check/%) +check-whitespace: $(PLUGIN_PAY_SRC:%=check-whitespace/%) + +clean: plugin-clean + +plugin-clean: + $(RM) $(PLUGIN_PAY_OBJS) $(PLUGIN_LIB_OBJS) diff --git a/plugins/libplugin.c b/plugins/libplugin.c new file mode 100644 index 000000000000..c1edd49dfcf5 --- /dev/null +++ b/plugins/libplugin.c @@ -0,0 +1,576 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define READ_CHUNKSIZE 4096 + +/* Tracking requests */ +static UINTMAP(struct out_req *) out_reqs; +static u64 next_outreq_id; + +struct plugin_conn { + int fd; + MEMBUF(char) mb; +}; + +struct command { + u64 id; + struct plugin_conn *rpc; +}; + +struct out_req { + /* The unique id of this request. */ + u64 id; + /* The command which is why we're calling this rpc. */ + struct command *cmd; + /* The callback when we get a response. */ + struct command_result *(*cb)(struct command *command, + const char *buf, + const jsmntok_t *result, + void *arg); + /* The callback when we get an error. */ + struct command_result *(*errcb)(struct command *command, + const char *buf, + const jsmntok_t *error, + void *arg); + void *arg; +}; + +/* command_result is mainly used as a compile-time check to encourage you + * to return as soon as you get one (and not risk use-after-free of command). + * Here we use two values: complete (cmd freed) an pending (still going) */ +struct command_result { + char c; +}; +static struct command_result complete, pending; + +struct command_result *command_param_failed(void) +{ + return &complete; +} + +void NORETURN plugin_err(const char *fmt, ...) +{ + va_list ap; + + /* FIXME: log */ + va_start(ap, fmt); + errx(1, "%s", tal_vfmt(NULL, fmt, ap)); + va_end(ap); +} + +/* Realloc helper for tal membufs */ +static void *membuf_tal_realloc(struct membuf *mb, void *rawelems, + size_t newsize) +{ + char *p = rawelems; + + tal_resize(&p, newsize); + return p; +} + +static int read_json(struct plugin_conn *conn) +{ + char *end; + + /* We rely on the double-\n marker which only terminates JSON top + * levels. Thanks lightningd! */ + while ((end = memmem(membuf_elems(&conn->mb), + membuf_num_elems(&conn->mb), "\n\n", 2)) + == NULL) { + ssize_t r; + + /* Make sure we've room for at least READ_CHUNKSIZE. */ + membuf_prepare_space(&conn->mb, READ_CHUNKSIZE); + r = read(conn->fd, membuf_space(&conn->mb), + membuf_num_space(&conn->mb)); + /* lightningd goes away, we go away. */ + if (r == 0) + exit(0); + if (r < 0) + plugin_err("Reading JSON input: %s", strerror(errno)); + membuf_added(&conn->mb, r); + } + + return end + 2 - membuf_elems(&conn->mb); +} + +static struct command *read_json_request(const tal_t *ctx, + struct plugin_conn *conn, + struct plugin_conn *rpc, + const jsmntok_t **method, + const jsmntok_t **params, + int *reqlen) +{ + const jsmntok_t *toks, *id; + bool valid; + struct command *cmd = tal(ctx, struct command); + + *reqlen = read_json(conn); + toks = json_parse_input(cmd, membuf_elems(&conn->mb), *reqlen, &valid); + if (!valid) + plugin_err("Malformed JSON input '%.*s'", + *reqlen, membuf_elems(&conn->mb)); + + if (toks[0].type != JSMN_OBJECT) + plugin_err("Malformed JSON command '%*.s' is not an object", + *reqlen, membuf_elems(&conn->mb)); + + *method = json_get_member(membuf_elems(&conn->mb), toks, "method"); + *params = json_get_member(membuf_elems(&conn->mb), toks, "params"); + /* FIXME: Notifications don't have id! */ + id = json_get_member(membuf_elems(&conn->mb), toks, "id"); + if (!json_to_u64(membuf_elems(&conn->mb), id, &cmd->id)) + plugin_err("JSON id '%*.s' is not a number", + id->end - id->start, + membuf_elems(&conn->mb) + id->start); + /* Putting this in cmd avoids a global, or direct exposure to users */ + cmd->rpc = rpc; + + return cmd; +} + +/* I stole this trick from @wythe (Mark Beckwith); its ugliness is beautiful */ +static void vprintf_json(int fd, const char *fmt_single_ticks, va_list ap) +{ + char *json, *p; + size_t n; + + json = tal_vfmt(NULL, fmt_single_ticks, ap); + + for (n = 0, p = strchr(json, '\''); p; p = strchr(json, '\'')) { + *p = '"'; + n++; + } + /* Don't put stray single-ticks in like this comment does! */ + assert(n % 2 == 0); + write_all(fd, json, strlen(json)); + tal_free(json); +} + +static PRINTF_FMT(2,3) void printf_json(int fd, + const char *fmt_single_ticks, ...) +{ + va_list ap; + + va_start(ap, fmt_single_ticks); + vprintf_json(fd, fmt_single_ticks, ap); + va_end(ap); +} + +/* param.c is insistant on functions returning 'struct command_result'; we + * just always return NULL. */ +static struct command_result *WARN_UNUSED_RESULT end_cmd(struct command *cmd) +{ + tal_free(cmd); + return &complete; +} + +static struct command_result *WARN_UNUSED_RESULT +command_done_ok(struct command *cmd, const char *result) +{ + printf_json(STDOUT_FILENO, + "{ 'jsonrpc': '2.0', " + "'id': %"PRIu64", " + "'result': { %s } }\n\n", + cmd->id, result); + return end_cmd(cmd); +} + +struct command_result *command_done_err(struct command *cmd, + int code, + const char *errmsg, + const char *data) +{ + printf_json(STDOUT_FILENO, + "{ 'jsonrpc': '2.0', " + "'id': %"PRIu64", " + " 'error' : " + " { 'code' : %d," + " 'message' : '%s'", + cmd->id, code, errmsg); + if (data) + printf_json(STDOUT_FILENO, + ", 'data': %s", data); + printf_json(STDOUT_FILENO, " } }\n\n"); + return end_cmd(cmd); +} + +static struct command_result *WARN_UNUSED_RESULT +command_done_raw(struct command *cmd, + const char *label, + const char *str, int size) +{ + printf_json(STDOUT_FILENO, + "{ 'jsonrpc': '2.0', " + "'id': %"PRIu64", " + " '%s' : %.*s }\n\n", + cmd->id, label, size, str); + return end_cmd(cmd); +} + +struct command_result *command_success(struct command *cmd, const char *result) +{ + return command_done_raw(cmd, "result", result, strlen(result)); +} + +struct command_result *forward_error(struct command *cmd, + const char *buf, + const jsmntok_t *error, + void *arg UNNEEDED) +{ + /* Push through any errors. */ + return command_done_raw(cmd, "error", + buf + error->start, error->end - error->start); +} + +struct command_result *forward_result(struct command *cmd, + const char *buf, + const jsmntok_t *result, + void *arg UNNEEDED) +{ + /* Push through the result. */ + return command_done_raw(cmd, "result", + buf + result->start, result->end - result->start); +} + +/* Called by param() directly if it's malformed. */ +struct command_result *command_fail(struct command *cmd, + int code, const char *fmt, ...) +{ + va_list ap; + struct command_result *res; + + va_start(ap, fmt); + res = command_done_err(cmd, code, tal_vfmt(cmd, fmt, ap), NULL); + va_end(ap); + return res; +} + +/* We never invoke param for usage. */ +bool command_usage_only(const struct command *cmd) +{ + return false; +} + +bool command_check_only(const struct command *cmd) +{ + return false; +} + +void command_set_usage(struct command *cmd, const char *usage) +{ + abort(); +} + +/* Reads rpc reply and returns tokens, setting contents to 'error' or + * 'result' (depending on *error). */ +static const jsmntok_t *read_rpc_reply(const tal_t *ctx, + struct plugin_conn *rpc, + const jsmntok_t **contents, + bool *error, + int *reqlen) +{ + const jsmntok_t *toks; + bool valid; + + *reqlen = read_json(rpc); + + toks = json_parse_input(ctx, membuf_elems(&rpc->mb), *reqlen, &valid); + if (!valid) + plugin_err("Malformed JSON reply '%.*s'", + *reqlen, membuf_elems(&rpc->mb)); + + *contents = json_get_member(membuf_elems(&rpc->mb), toks, "error"); + if (*contents) + *error = true; + else { + *contents = json_get_member(membuf_elems(&rpc->mb), toks, + "result"); + if (!*contents) + plugin_err("JSON reply with no 'result' nor 'error'? '%.*s'", + *reqlen, membuf_elems(&rpc->mb)); + *error = false; + } + return toks; +} + +/* Synchronous routine to send command and extract single field from response */ +const char *rpc_delve(const tal_t *ctx, + const char *method, const char *params, + struct plugin_conn *rpc, const char *guide) +{ + bool error; + const jsmntok_t *contents, *t; + int reqlen; + const char *ret; + + printf_json(rpc->fd, + "{ 'method': '%s', 'id': 0, 'params': { %s } }", + method, params); + + read_rpc_reply(tmpctx, rpc, &contents, &error, &reqlen); + if (error) + plugin_err("Got error reply to %s: '%.*s'", + method, reqlen, membuf_elems(&rpc->mb)); + + t = json_delve(membuf_elems(&rpc->mb), contents, guide); + if (!t) + plugin_err("Could not find %s in reply to %s: '%.*s'", + guide, method, reqlen, membuf_elems(&rpc->mb)); + + ret = json_strdup(ctx, membuf_elems(&rpc->mb), t); + membuf_consume(&rpc->mb, reqlen); + return ret; +} + +static void handle_rpc_reply(struct plugin_conn *rpc) +{ + int reqlen; + const jsmntok_t *toks, *contents, *t; + struct out_req *out; + struct command_result *res; + u64 id; + bool error; + + toks = read_rpc_reply(tmpctx, rpc, &contents, &error, &reqlen); + + t = json_get_member(membuf_elems(&rpc->mb), toks, "id"); + if (!t) + plugin_err("JSON reply without id '%.*s'", + reqlen, membuf_elems(&rpc->mb)); + if (!json_to_u64(membuf_elems(&rpc->mb), t, &id)) + plugin_err("JSON reply without numeric id '%.*s'", + reqlen, membuf_elems(&rpc->mb)); + out = uintmap_get(&out_reqs, id); + if (!out) + plugin_err("JSON reply with unknown id '%.*s' (%"PRIu64")", + reqlen, membuf_elems(&rpc->mb), id); + + /* We want to free this if callback doesn't. */ + tal_steal(tmpctx, out); + uintmap_del(&out_reqs, out->id); + + if (error) + res = out->errcb(out->cmd, membuf_elems(&rpc->mb), contents, + out->arg); + else + res = out->cb(out->cmd, membuf_elems(&rpc->mb), contents, + out->arg); + + assert(res == &pending || res == &complete); + membuf_consume(&rpc->mb, reqlen); +} + +struct command_result * +send_outreq_(struct command *cmd, + const char *method, + struct command_result *(*cb)(struct command *command, + const char *buf, + const jsmntok_t *result, + void *arg), + struct command_result *(*errcb)(struct command *command, + const char *buf, + const jsmntok_t *result, + void *arg), + void *arg, + const char *paramfmt_single_ticks, ...) +{ + va_list ap; + struct out_req *out = tal(cmd, struct out_req); + out->id = next_outreq_id++; + out->cmd = cmd; + out->cb = cb; + out->errcb = errcb; + out->arg = arg; + uintmap_add(&out_reqs, out->id, out); + + printf_json(cmd->rpc->fd, + "{ 'method': '%s', 'id': %"PRIu64", 'params': {", + method, out->id); + va_start(ap, paramfmt_single_ticks); + vprintf_json(cmd->rpc->fd, paramfmt_single_ticks, ap); + va_end(ap); + printf_json(cmd->rpc->fd, "} }"); + return &pending; +} + +static struct command_result * +handle_getmanifest(struct command *getmanifest_cmd, + const struct plugin_command *commands, + size_t num_commands) +{ + char *params = tal_strdup(getmanifest_cmd, + "'options': [],\n" + "'rpcmethods': [ "); + for (size_t i = 0; i < num_commands; i++) { + tal_append_fmt(¶ms, "{ 'name': '%s'," + " 'description': '%s'", + commands[i].name, + commands[i].description); + if (commands[i].long_description) + tal_append_fmt(¶ms, + " 'long_description': '%s'", + commands[i].long_description); + tal_append_fmt(¶ms, + "}%s", i == num_commands - 1 ? "" : ",\n"); + } + tal_append_fmt(¶ms, " ]"); + return command_done_ok(getmanifest_cmd, params); +} + +static struct command_result *handle_init(struct command *init_cmd, + const char *buf, + const jsmntok_t *params, + void (*init)(struct plugin_conn *)) +{ + const jsmntok_t *rpctok, *dirtok; + struct sockaddr_un addr; + char *dir; + + /* Move into lightning directory: other files are relative */ + dirtok = json_delve(buf, params, ".configuration.lightning-dir"); + dir = json_strdup(tmpctx, buf, dirtok); + if (chdir(dir) != 0) + plugin_err("chdir to %s: %s", dir, strerror(errno)); + + rpctok = json_delve(buf, params, ".configuration.rpc-file"); + init_cmd->rpc->fd = socket(AF_UNIX, SOCK_STREAM, 0); + if (rpctok->end - rpctok->start + 1 > sizeof(addr.sun_path)) + plugin_err("rpc filename '%.*s' too long", + rpctok->end - rpctok->start, + buf + rpctok->start); + memcpy(addr.sun_path, buf + rpctok->start, rpctok->end - rpctok->start); + addr.sun_path[rpctok->end - rpctok->start] = '\0'; + addr.sun_family = AF_UNIX; + + if (connect(init_cmd->rpc->fd, (struct sockaddr *)&addr, sizeof(addr)) != 0) + plugin_err("Connecting to '%.*s': %s", + rpctok->end - rpctok->start, buf + rpctok->start, + strerror(errno)); + + if (init) + init(init_cmd->rpc); + + return command_done_ok(init_cmd, ""); +} + +static void handle_new_command(const tal_t *ctx, + struct plugin_conn *request_conn, + struct plugin_conn *rpc_conn, + const struct plugin_command *commands, + size_t num_commands) +{ + struct command *cmd; + const jsmntok_t *method, *params; + int reqlen; + + cmd = read_json_request(ctx, request_conn, rpc_conn, + &method, ¶ms, &reqlen); + for (size_t i = 0; i < num_commands; i++) { + if (json_tok_streq(membuf_elems(&request_conn->mb), method, + commands[i].name)) { + commands[i].handle(cmd, membuf_elems(&request_conn->mb), + params); + membuf_consume(&request_conn->mb, reqlen); + return; + } + } + + plugin_err("Unknown command '%.*s'", + method->end - method->start, + membuf_elems(&request_conn->mb) + method->start); +} + +void plugin_main(char *argv[], + void (*init)(struct plugin_conn *rpc), + const struct plugin_command *commands, + size_t num_commands) +{ + struct plugin_conn request_conn, rpc_conn; + const tal_t *ctx = tal(NULL, char); + struct command *cmd; + const jsmntok_t *method, *params; + int reqlen; + struct pollfd fds[2]; + + setup_locale(); + + daemon_maybe_debug(argv); + + /* Note this already prints to stderr, which is enough for now */ + daemon_setup(argv[0], NULL, NULL); + + membuf_init(&rpc_conn.mb, + tal_arr(ctx, char, READ_CHUNKSIZE), READ_CHUNKSIZE, + membuf_tal_realloc); + request_conn.fd = STDIN_FILENO; + membuf_init(&request_conn.mb, + tal_arr(ctx, char, READ_CHUNKSIZE), READ_CHUNKSIZE, + membuf_tal_realloc); + uintmap_init(&out_reqs); + + cmd = read_json_request(tmpctx, &request_conn, NULL, + &method, ¶ms, &reqlen); + if (!json_tok_streq(membuf_elems(&request_conn.mb), method, + "getmanifest")) { + plugin_err("Expected getmanifest not '%.*s'", + method->end - method->start, + membuf_elems(&request_conn.mb) + method->start); + } + membuf_consume(&request_conn.mb, reqlen); + handle_getmanifest(cmd, commands, num_commands); + + cmd = read_json_request(tmpctx, &request_conn, &rpc_conn, + &method, ¶ms, &reqlen); + if (!json_tok_streq(membuf_elems(&request_conn.mb), method, "init")) { + plugin_err("Expected init not '%.*s'", + method->end - method->start, + membuf_elems(&request_conn.mb) + method->start); + } + handle_init(cmd, membuf_elems(&request_conn.mb), + params, init); + membuf_consume(&request_conn.mb, reqlen); + + /* Set up fds for poll. */ + fds[0].fd = STDIN_FILENO; + fds[0].events = POLLIN; + fds[1].fd = rpc_conn.fd; + fds[1].events = POLLIN; + + for (;;) { + clean_tmpctx(); + + /* If we already have some input, process now. */ + if (membuf_num_elems(&request_conn.mb) != 0) { + handle_new_command(ctx, &request_conn, &rpc_conn, + commands, num_commands); + continue; + } + if (membuf_num_elems(&rpc_conn.mb) != 0) { + handle_rpc_reply(&rpc_conn); + continue; + } + + /* Otherwise, we poll. */ + poll(fds, 2, -1); + + if (fds[0].revents & POLLIN) + handle_new_command(ctx, &request_conn, &rpc_conn, + commands, num_commands); + if (fds[1].revents & POLLIN) + handle_rpc_reply(&rpc_conn); + } +} diff --git a/plugins/libplugin.h b/plugins/libplugin.h new file mode 100644 index 000000000000..b38c7aa1fa2c --- /dev/null +++ b/plugins/libplugin.h @@ -0,0 +1,94 @@ +/* Helper library for C plugins. */ +#ifndef LIGHTNING_PLUGINS_LIBPLUGIN_H +#define LIGHTNING_PLUGINS_LIBPLUGIN_H +#include "config.h" + +#include +#include +#include +#include +#include + +struct command; +struct plugin_conn; + +/* Create an array of these, one for each command you support. */ +struct plugin_command { + const char *name; + const char *description; + const char *long_description; + struct command_result *(*handle)(struct command *cmd, + const char *buf, + const jsmntok_t *params); +}; + +/* Return this iff the param() call failed in your handler. */ +struct command_result *command_param_failed(void); + +/* Call this on fatal error. */ +void NORETURN plugin_err(const char *fmt, ...); + +/* This command is finished, here's a detailed error. data can be NULL. */ +struct command_result *WARN_UNUSED_RESULT +command_done_err(struct command *cmd, + int code, + const char *errmsg, + const char *data); + +/* This command is finished, here's the success msg. */ +struct command_result *WARN_UNUSED_RESULT +command_success(struct command *cmd, const char *result); + +/* Synchronous helper to send command and extract single field from + * response; can only be used in init callback. */ +const char *rpc_delve(const tal_t *ctx, + const char *method, const char *params, + struct plugin_conn *rpc, const char *guide); + +/* Async rpc request. For convenience, and single ' are turned into ". */ +PRINTF_FMT(6,7) struct command_result * +send_outreq_(struct command *cmd, + const char *method, + struct command_result *(*cb)(struct command *command, + const char *buf, + const jsmntok_t *result, + void *arg), + struct command_result *(*errcb)(struct command *command, + const char *buf, + const jsmntok_t *result, + void *arg), + void *arg, + const char *paramfmt_single_ticks, ...); + +#define send_outreq(cmd, method, cb, errcb, arg, ...) \ + send_outreq_((cmd), (method), \ + typesafe_cb_preargs(struct command_result *, void *, \ + (cb), (arg), \ + struct command *command, \ + const char *buf, \ + const jsmntok_t *result), \ + typesafe_cb_preargs(struct command_result *, void *, \ + (errcb), (arg), \ + struct command *command, \ + const char *buf, \ + const jsmntok_t *result), \ + (arg), __VA_ARGS__) + +/* Callback to just forward error and close request. */ +struct command_result *forward_error(struct command *cmd, + const char *buf, + const jsmntok_t *error, + void *arg); + +/* Callback to just forward result and close request. */ +struct command_result *forward_result(struct command *cmd, + const char *buf, + const jsmntok_t *result, + void *arg); + +/* The main plugin runner. */ +void NORETURN plugin_main(char *argv[], + void (*init)(struct plugin_conn *rpc), + const struct plugin_command *commands, + size_t num_commands); +#endif /* LIGHTNING_PLUGINS_LIBPLUGIN_H */ diff --git a/plugins/pay.c b/plugins/pay.c new file mode 100644 index 000000000000..f39ec20ac23e --- /dev/null +++ b/plugins/pay.c @@ -0,0 +1,1048 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* Public key of this node. */ +static struct pubkey my_id; +static unsigned int maxdelay_default; +static LIST_HEAD(pay_status); + +struct pay_attempt { + /* What we changed when starting this attempt. */ + const char *why; + /* Time we started & finished attempt */ + struct timeabs start, end; + /* Route hint we were using (if any) */ + struct route_info *routehint; + /* Channels we excluded when doing route lookup. */ + const char **excludes; + /* Route we got (NULL == route lookup fail). */ + const char *route; + /* The failure result (NULL on success) */ + const char *failure; + /* The non-failure result (NULL on failure) */ + const char *result; +}; + +struct pay_status { + /* Destination, as text */ + const char *dest; + + /* We're in 'pay_status' global list. */ + struct list_node list; + + /* Description user provided (if any) */ + const char *desc; + /* Amount they wanted to pay. */ + u64 msatoshi; + /* CLTV delay required by destination. */ + u32 final_cltv; + /* Bolt11 invoice. */ + const char *bolt11; + + /* What we did about routehints (if anything) */ + const char *routehint_modifications; + + /* Details of shadow route we chose (if any) */ + char *shadow; + + /* Details of initial exclusions (if any) */ + const char *exclusions; + + /* Array of payment attempts. */ + struct pay_attempt *attempts; +}; + +struct pay_command { + /* Destination, as text */ + const char *dest; + + /* How much we're paying, and what riskfactor for routing. */ + u64 msatoshi; + double riskfactor; + unsigned int final_cltv; + + /* Limits on what routes we'll accept. */ + double maxfeepercent; + unsigned int maxdelay; + u64 exemptfee; + + /* Payment hash, as text. */ + const char *payment_hash; + + /* Description, if any. */ + const char *desc; + + /* Chatty description of attempts. */ + struct pay_status *ps; + + /* Error to use if getroute says it can't find route. */ + const char *expensive_route; + + /* Time to stop retrying. */ + struct timeabs stoptime; + + /* Channels which have failed us. */ + const char **excludes; + + /* Any routehints to use. */ + struct route_info **routehints; + + /* Current node during shadow route calculation. */ + const char *shadow_dest; +}; + +static struct pay_attempt *current_attempt(struct pay_command *pc) +{ + return &pc->ps->attempts[tal_count(pc->ps->attempts)-1]; +} + +PRINTF_FMT(2,3) static void attempt_failed_fmt(struct pay_command *pc, const char *fmt, ...) +{ + struct pay_attempt *attempt = current_attempt(pc); + va_list ap; + + va_start(ap,fmt); + attempt->failure = tal_vfmt(pc->ps->attempts, fmt, ap); + attempt->end = time_now(); + va_end(ap); +} + +static void attempt_failed_tok(struct pay_command *pc, const char *method, + const char *buf, const jsmntok_t *errtok) +{ + const jsmntok_t *msg = json_get_member(buf, errtok, "message"); + + if (msg) + attempt_failed_fmt(pc, "%.*sCall to %s:%.*s", + msg->start - errtok->start, + buf + errtok->start, + method, + errtok->end - msg->start, + buf + msg->start); + else + attempt_failed_fmt(pc, + "{ 'message': 'Call to %s failed', %.*s", + method, + errtok->end - errtok->start - 1, + buf + errtok->start + 1); +} + +static struct command_result *start_pay_attempt(struct command *cmd, + struct pay_command *pc, + const char *fmt, ...); + +/* Is this (erring) channel within the routehint itself? */ +static bool channel_in_routehint(const struct route_info *routehint, + const char *buf, const jsmntok_t *scidtok) +{ + struct short_channel_id scid; + + if (!json_to_short_channel_id(buf, scidtok, &scid)) + plugin_err("bad erring_channel '%.*s'", + scidtok->end - scidtok->start, buf + scidtok->start); + + for (size_t i = 0; i < tal_count(routehint); i++) + if (short_channel_id_eq(&scid, &routehint[i].short_channel_id)) + return true; + + return false; +} + +static struct command_result *waitsendpay_expired(struct command *cmd, + struct pay_command *pc) +{ + char *errmsg, *data; + + errmsg = tal_fmt(pc, "Gave up after %zu attempts", + tal_count(pc->ps->attempts)); + data = tal_strdup(pc, "'attempts': [ "); + for (size_t i = 0; i < tal_count(pc->ps->attempts); i++) { + if (pc->ps->attempts[i].route) + tal_append_fmt(&data, "%s { 'route': %s,\n 'failure': %s\n }", + i == 0 ? "" : ",", + pc->ps->attempts[i].route, + pc->ps->attempts[i].failure); + else + tal_append_fmt(&data, "%s { 'failure': %s\n }", + i == 0 ? "" : ",", + pc->ps->attempts[i].failure); + } + tal_append_fmt(&data, "]"); + return command_done_err(cmd, PAY_STOPPED_RETRYING, errmsg, data); +} + +/* Try again with the next routehint (or none if that was the last) */ +static struct command_result *next_routehint(struct command *cmd, + struct pay_command *pc) +{ + tal_arr_remove(&pc->routehints, 0); + return start_pay_attempt(cmd, pc, "Removed route hint"); +} + +static struct command_result *waitsendpay_error(struct command *cmd, + const char *buf, + const jsmntok_t *error, + struct pay_command *pc) +{ + const jsmntok_t *codetok, *scidtok, *dirtok; + int code; + + attempt_failed_tok(pc, "waitsendpay", buf, error); + + codetok = json_get_member(buf, error, "code"); + if (!json_to_int(buf, codetok, &code)) + plugin_err("waitsendpay error gave no 'code'? '%.*s'", + error->end - error->start, buf + error->start); + + /* FIXME: Handle PAY_UNPARSEABLE_ONION! */ + + /* Many error codes are final. */ + if (code != PAY_TRY_OTHER_ROUTE) { + return forward_error(cmd, buf, error, pc); + } + + scidtok = json_delve(buf, error, ".data.erring_channel"); + if (!scidtok) + plugin_err("waitsendpay error no erring_channel '%.*s'", + error->end - error->start, buf + error->start); + dirtok = json_delve(buf, error, ".data.erring_direction"); + if (!dirtok) + plugin_err("waitsendpay error no erring_direction '%.*s'", + error->end - error->start, buf + error->start); + + if (time_after(time_now(), pc->stoptime)) { + return waitsendpay_expired(cmd, pc); + } + + /* If failure is in routehint part, eliminate that */ + if (tal_count(pc->routehints) != 0 + && channel_in_routehint(pc->routehints[0], buf, scidtok)) + return next_routehint(cmd, pc); + + /* Otherwise, add erring channel to exclusion list. */ + tal_arr_expand(&pc->excludes, + tal_fmt(pc->excludes, "%.*s/%c", + scidtok->end - scidtok->start, + buf + scidtok->start, + buf[dirtok->start])); + /* Try again. */ + return start_pay_attempt(cmd, pc, "Excluded channel %s", + pc->excludes[tal_count(pc->excludes)-1]); +} + +static struct command_result *waitsendpay_done(struct command *cmd, + const char *buf, + const jsmntok_t *result, + struct pay_command *pc) +{ + struct pay_attempt *attempt = current_attempt(pc); + + attempt->result = json_strdup(pc->ps->attempts, buf, result); + attempt->end = time_now(); + + return forward_result(cmd, buf, result, pc); +} + +static struct command_result *sendpay_done(struct command *cmd, + const char *buf, + const jsmntok_t *result, + struct pay_command *pc) +{ + return send_outreq(cmd, "waitsendpay", + waitsendpay_done, waitsendpay_error, pc, + "'payment_hash': '%s', 'timeout': 60", + pc->payment_hash); +} + +/* Calculate how many millisatoshi we need at the start of this route + * to get msatoshi to the end. */ +static u64 route_msatoshi(u64 msatoshi, + const struct route_info *route, size_t num_route) +{ + for (ssize_t i = num_route - 1; i >= 0; i--) { + u64 fee; + + fee = route[i].fee_base_msat; + fee += (route[i].fee_proportional_millionths * msatoshi) / 1000000; + msatoshi += fee; + } + return msatoshi; +} + +/* Calculate cltv we need at the start of this route to get cltv at the end. */ +static u32 route_cltv(u32 cltv, + const struct route_info *route, size_t num_route) +{ + for (size_t i = 0; i < num_route; i++) + cltv += route[i].cltv_expiry_delta; + return cltv; +} + +/* The pubkey to use is the destination of this routehint. */ +static const char *route_pubkey(const tal_t *ctx, + const struct pay_command *pc, + const struct route_info *routehint, + size_t n) +{ + if (n == tal_count(routehint)) + return pc->dest; + return type_to_string(ctx, struct pubkey, &routehint[n].pubkey); +} + +static const char *join_routehint(const tal_t *ctx, + const char *buf, + const jsmntok_t *route, + const struct pay_command *pc, + const struct route_info *routehint) +{ + char *ret; + + /* Truncate closing ] from route */ + ret = tal_strndup(ctx, buf + route->start, route->end - route->start - 1); + for (size_t i = 0; i < tal_count(routehint); i++) { + tal_append_fmt(&ret, ", {" + " 'id': '%s'," + " 'channel': '%s'," + " 'msatoshi': %"PRIu64"," + " 'delay': %u }", + /* pubkey of *destination* */ + route_pubkey(tmpctx, pc, routehint, i + 1), + type_to_string(tmpctx, struct short_channel_id, + &routehint[i].short_channel_id), + /* amount to be received by *destination* */ + route_msatoshi(pc->msatoshi, routehint + i + 1, + tal_count(routehint) - i - 1), + /* cltv for *destination* */ + route_cltv(pc->final_cltv, routehint + i + 1, + tal_count(routehint) - i - 1)); + } + /* Put ] back */ + tal_append_fmt(&ret, "]"); + return ret; +} + +static struct command_result *sendpay_error(struct command *cmd, + const char *buf, + const jsmntok_t *error, + struct pay_command *pc) +{ + attempt_failed_tok(pc, "sendpay", buf, error); + + return forward_error(cmd, buf, error, pc); +} + +static const jsmntok_t *find_worst_channel(const char *buf, + const jsmntok_t *route, + const char *fieldname, + u64 final) +{ + u64 prev = final, worstval = 0; + const jsmntok_t *worst = NULL, *end; + + end = json_next(route); + for (route = route + 1; route < end; route = json_next(route)) { + u64 val; + + json_to_u64(buf, json_get_member(buf, route, fieldname), &val); + if (worst == NULL || val - prev > worstval) { + worst = route; + worstval = val - prev; + } + prev = val; + } + + return worst; +} + +/* Can't exclude if it's in routehint itself. */ +static bool maybe_exclude(struct pay_command *pc, + const char *buf, const jsmntok_t *route) +{ + const jsmntok_t *scid, *dir; + + scid = json_get_member(buf, route, "channel"); + + if (tal_count(pc->routehints) != 0 + && channel_in_routehint(pc->routehints[0], buf, scid)) + return false; + + dir = json_get_member(buf, route, "direction"); + tal_arr_expand(&pc->excludes, + tal_fmt(pc->excludes, "%.*s/%c", + scid->end - scid->start, + buf + scid->start, + buf[dir->start])); + return true; +} + +static struct command_result *getroute_done(struct command *cmd, + const char *buf, + const jsmntok_t *result, + struct pay_command *pc) +{ + struct pay_attempt *attempt = current_attempt(pc); + const jsmntok_t *t = json_get_member(buf, result, "route"); + char *json_desc; + u64 fee; + u32 delay; + double feepercent; + + if (!t) + plugin_err("getroute gave no 'route'? '%.*s'", + result->end - result->start, buf); + + if (tal_count(pc->routehints)) + attempt->route = join_routehint(pc->ps->attempts, buf, t, + pc, pc->routehints[0]); + else + attempt->route = json_strdup(pc->ps->attempts, buf, t); + + if (!json_to_u64(buf, json_delve(buf, t, "[0].msatoshi"), &fee)) + plugin_err("getroute with invalid msatoshi? '%.*s'", + result->end - result->start, buf); + fee -= pc->msatoshi; + + if (!json_to_number(buf, json_delve(buf, t, "[0].delay"), &delay)) + plugin_err("getroute with invalid delay? '%.*s'", + result->end - result->start, buf); + + /* Casting u64 to double will lose some precision. The loss of precision + * in feepercent will be like 3.0000..(some dots)..1 % - 3.0 %. + * That loss will not be representable in double. So, it's Okay to + * cast u64 to double for feepercent calculation. */ + feepercent = ((double)fee) * 100.0 / ((double) pc->msatoshi); + + if (fee > pc->exemptfee && feepercent > pc->maxfeepercent) { + const jsmntok_t *charger; + + attempt_failed_fmt(pc, "{ 'message': 'Route wanted fee of %"PRIu64" msatoshis' }", fee); + + /* Remember this if we eliminating this causes us to have no + * routes at all! */ + if (!pc->expensive_route) + pc->expensive_route + = tal_fmt(pc, "Route wanted fee of %"PRIu64 + " msatoshis", fee); + + /* Try excluding most fee-charging channel (unless it's in + * routeboost). */ + charger = find_worst_channel(buf, t, "msatoshi", pc->msatoshi); + if (maybe_exclude(pc, buf, charger)) { + return start_pay_attempt(cmd, pc, + "Excluded expensive channel %s", + pc->excludes[tal_count(pc->excludes)-1]); + } + + if (tal_count(pc->routehints) != 0) + return next_routehint(cmd, pc); + + return command_fail(cmd, PAY_ROUTE_TOO_EXPENSIVE, + "%s", pc->expensive_route); + } + + if (delay > pc->maxdelay) { + const jsmntok_t *delayer; + + attempt_failed_fmt(pc, + "{ 'message': 'Route wanted delay of %u blocks' }", + delay); + + /* Remember this if we eliminating this causes us to have no + * routes at all! */ + if (!pc->expensive_route) + pc->expensive_route + = tal_fmt(pc, "Route wanted delay of %u blocks", + delay); + + delayer = find_worst_channel(buf, t, "delay", pc->final_cltv); + + /* Try excluding most delaying channel (unless it's in + * routeboost). */ + if (maybe_exclude(pc, buf, delayer)) { + return start_pay_attempt(cmd, pc, + "Excluded delaying channel %s", + pc->excludes[tal_count(pc->excludes)-1]); + } + + if (tal_count(pc->routehints) != 0) + return next_routehint(cmd, pc); + + return command_fail(cmd, PAY_ROUTE_TOO_EXPENSIVE, + "%s", pc->expensive_route); + } + + if (pc->desc) + json_desc = tal_fmt(pc, ", 'description': '%s'", pc->desc); + else + json_desc = ""; + + return send_outreq(cmd, "sendpay", sendpay_done, sendpay_error, pc, + "'route': %s, 'payment_hash': '%s'%s", + attempt->route, + pc->payment_hash, + json_desc); + +} + +static struct command_result *getroute_error(struct command *cmd, + const char *buf, + const jsmntok_t *error, + struct pay_command *pc) +{ + attempt_failed_tok(pc, "getroute", buf, error); + + /* If we were trying to use a routehint, remove and try again. */ + if (tal_count(pc->routehints) != 0) + return next_routehint(cmd, pc); + + /* If we've run out of routes, there might be a good reason. */ + if (pc->expensive_route) + return command_fail(cmd, PAY_ROUTE_TOO_EXPENSIVE, + "%s", pc->expensive_route); + + return forward_error(cmd, buf, error, pc); +} + +/* Deep copy of excludes array. */ +static const char **dup_excludes(const tal_t *ctx, const char **excludes) +{ + const char **ret = tal_dup_arr(ctx, const char *, + excludes, tal_count(excludes), 0); + for (size_t i = 0; i < tal_count(ret); i++) + ret[i] = tal_strdup(ret, excludes[i]); + return ret; +} + +static struct command_result *start_pay_attempt(struct command *cmd, + struct pay_command *pc, + const char *fmt, ...) +{ + char *exclude; + u64 amount; + const char *dest; + size_t max_hops = ROUTING_MAX_HOPS; + u32 cltv; + struct pay_attempt attempt; + va_list ap; + + va_start(ap, fmt); + attempt.start = time_now(); + /* Mark it unfinished */ + attempt.end.ts.tv_sec = -1; + attempt.excludes = dup_excludes(pc->ps, pc->excludes); + attempt.route = NULL; + attempt.failure = NULL; + attempt.result = NULL; + attempt.why = tal_vfmt(pc->ps, fmt, ap); + va_end(ap); + + /* routehint set below. */ + + if (tal_count(pc->excludes) != 0) { + exclude = tal_strdup(tmpctx, ",'exclude': ["); + for (size_t i = 0; i < tal_count(pc->excludes); i++) + /* JSON.org grammar doesn't allow trailing , */ + tal_append_fmt(&exclude, "%s %s", + i == 0 ? "" : ",", + pc->excludes[i]); + tal_append_fmt(&exclude, "]"); + } else + exclude = ""; + + /* If we have a routehint, try that first; we need to do extra + * checks that it meets our criteria though. */ + if (tal_count(pc->routehints)) { + amount = route_msatoshi(pc->msatoshi, + pc->routehints[0], + tal_count(pc->routehints[0])); + dest = type_to_string(tmpctx, struct pubkey, + &pc->routehints[0][0].pubkey); + max_hops -= tal_count(pc->routehints[0]); + cltv = route_cltv(pc->final_cltv, + pc->routehints[0], + tal_count(pc->routehints[0])); + attempt.routehint = tal_steal(pc->ps, pc->routehints[0]); + } else { + amount = pc->msatoshi; + dest = pc->dest; + cltv = pc->final_cltv; + attempt.routehint = NULL; + } + + tal_arr_expand(&pc->ps->attempts, attempt); + + /* OK, ask for route to destination */ + return send_outreq(cmd, "getroute", getroute_done, getroute_error, pc, + "'id': '%s'," + "'msatoshi': %"PRIu64"," + "'cltv': %u," + "'maxhops': %zu," + "'riskfactor': %f%s", + dest, amount, cltv, max_hops, pc->riskfactor, exclude); +} + +/* BOLT #7: + * + * If a route is computed by simply routing to the intended recipient and + * summing the `cltv_expiry_delta`s, then it's possible for intermediate nodes + * to guess their position in the route. Knowing the CLTV of the HTLC, the + * surrounding network topology, and the `cltv_expiry_delta`s gives an + * attacker a way to guess the intended recipient. Therefore, it's highly + * desirable to add a random offset to the CLTV that the intended recipient + * will receive, which bumps all CLTVs along the route. + * + * In order to create a plausible offset, the origin node MAY start a limited + * random walk on the graph, starting from the intended recipient and summing + * the `cltv_expiry_delta`s, and use the resulting sum as the offset. This + * effectively creates a _shadow route extension_ to the actual route and + * provides better protection against this attack vector than simply picking a + * random offset would. + */ +static struct command_result *shadow_route(struct command *cmd, + struct pay_command *pc); + +static struct command_result *add_shadow_route(struct command *cmd, + const char *buf, + const jsmntok_t *result, + struct pay_command *pc) +{ + /* Use reservoir sampling across the capable channels. */ + const jsmntok_t *channels = json_get_member(buf, result, "channels"); + const jsmntok_t *chan, *end, *best = NULL; + u64 sample; + u32 cltv, best_cltv; + + end = json_next(channels); + for (chan = channels + 1; chan < end; chan = json_next(chan)) { + u64 sats, v; + + json_to_u64(buf, json_get_member(buf, chan, "satoshis"), &sats); + if (sats * 1000 < pc->msatoshi) + continue; + + /* Don't use if total would exceed 1/4 of our time allowance. */ + json_to_number(buf, json_get_member(buf, chan, "delay"), &cltv); + if ((pc->final_cltv + cltv) * 4 > pc->maxdelay) + continue; + + v = pseudorand(UINT64_MAX); + if (!best || v > sample) { + best = chan; + best_cltv = cltv; + sample = v; + } + } + + if (!best) { + tal_append_fmt(&pc->ps->shadow, + "No suitable channels found to %s. ", + pc->shadow_dest); + return start_pay_attempt(cmd, pc, "Initial attempt"); + } + + pc->final_cltv += best_cltv; + pc->shadow_dest = json_strdup(pc, buf, + json_get_member(buf, best, "destination")); + tal_append_fmt(&pc->ps->shadow, + "Added %u cltv delay for shadow to %s. ", + best_cltv, pc->shadow_dest); + return shadow_route(cmd, pc); +} + +static struct command_result *shadow_route(struct command *cmd, + struct pay_command *pc) +{ + if (pseudorand(2) == 0) + return start_pay_attempt(cmd, pc, "Initial attempt"); + + return send_outreq(cmd, "listchannels", + add_shadow_route, forward_error, pc, + "'source' : '%s'", pc->shadow_dest); +} + +/* gossipd doesn't know much about the current state of channels; here we + * manually exclude peers which are disconnected and channels which lack + * current capacity (it will eliminate those without total capacity). */ +static struct command_result *listpeers_done(struct command *cmd, + const char *buf, + const jsmntok_t *result, + struct pay_command *pc) +{ + const jsmntok_t *peer, *peers_end; + char *mods = tal_strdup(tmpctx, ""); + + peer = json_get_member(buf, result, "peers"); + if (!peer) + plugin_err("listpeers gave no 'peers'? '%.*s'", + result->end - result->start, buf); + + peers_end = json_next(peer); + for (peer = peer + 1; peer < peers_end; peer = json_next(peer)) { + const jsmntok_t *chan, *chans_end; + bool connected; + + json_to_bool(buf, json_get_member(buf, peer, "connected"), + &connected); + chan = json_get_member(buf, peer, "channels"); + chans_end = json_next(chan); + for (chan = chan + 1; chan < chans_end; chan = json_next(chan)) { + const jsmntok_t *state, *scid, *dir; + u64 spendable; + + /* gossipd will only consider things in state NORMAL + * anyway; we don't need to exclude others. */ + state = json_get_member(buf, chan, "state"); + if (!json_tok_streq(buf, state, "CHANNELD_NORMAL")) + continue; + + json_to_u64(buf, + json_get_member(buf, chan, + "spendable_msatoshi"), + &spendable); + + if (connected && spendable >= pc->msatoshi) + continue; + + /* Exclude this disconnected or low-capacity channel */ + scid = json_get_member(buf, chan, "short_channel_id"); + dir = json_get_member(buf, chan, "direction"); + tal_arr_expand(&pc->excludes, + tal_fmt(pc->excludes, "%.*s/%c", + scid->end - scid->start, + buf + scid->start, + buf[dir->start])); + + tal_append_fmt(&mods, + "Excluded channel %s (%"PRIu64" msat, %s). ", + pc->excludes[tal_count(pc->excludes)-1], + spendable, + connected ? "connected" : "disconnected"); + } + } + + if (!streq(mods, "")) + pc->ps->exclusions = tal_steal(pc->ps, mods); + + pc->ps->shadow = tal_strdup(pc->ps, ""); + return shadow_route(cmd, pc); +} + +/* Trim route to this length by taking from the *front* of route + * (end points to destination, so we need that bit!) */ +static void trim_route(struct route_info **route, size_t n) +{ + size_t remove = tal_count(*route) - n; + memmove(*route, *route + remove, sizeof(**route) * n); + tal_resize(route, n); +} + +/* Make sure routehints are reasonable length, and (since we assume we + * can append), not directly to us. Note: untrusted data! */ +static struct route_info **filter_routehints(struct pay_command *pc, + struct route_info **hints) +{ + char *mods = tal_strdup(tmpctx, ""); + + for (size_t i = 0; i < tal_count(hints); i++) { + /* Trim any routehint > 10 hops */ + size_t max_hops = ROUTING_MAX_HOPS / 2; + if (tal_count(hints[i]) > max_hops) { + tal_append_fmt(&mods, + "Trimmed routehint %zu (%zu hops) to %zu. ", + i, tal_count(hints[i]), max_hops); + trim_route(&hints[i], max_hops); + } + + /* If we are first hop, trim. */ + if (tal_count(hints[i]) > 0 + && pubkey_eq(&hints[i][0].pubkey, &my_id)) { + tal_append_fmt(&mods, + "Removed ourselves from routehint %zu. ", + i); + trim_route(&hints[i], tal_count(hints[i])-1); + } + + /* If route is empty, remove altogether. */ + if (tal_count(hints[i]) == 0) { + tal_append_fmt(&mods, + "Removed empty routehint %zu. ", i); + tal_arr_remove(&hints, i); + i--; + } + } + + if (!streq(mods, "")) + pc->ps->routehint_modifications = tal_steal(pc->ps, mods); + + return tal_steal(pc, hints); +} + +static struct pay_status *add_pay_status(struct pay_command *pc, + const char *b11str) +{ + struct pay_status *ps = tal(NULL, struct pay_status); + + /* The pay_status outlives the pc, so it simply takes field ownership */ + ps->dest = tal_steal(ps, pc->dest); + ps->desc = tal_steal(ps, pc->desc); + ps->msatoshi = pc->msatoshi; + ps->final_cltv = pc->final_cltv; + ps->bolt11 = tal_steal(ps, b11str); + ps->routehint_modifications = NULL; + ps->shadow = NULL; + ps->exclusions = NULL; + ps->attempts = tal_arr(ps, struct pay_attempt, 0); + + list_add_tail(&pay_status, &ps->list); + return ps; +} + +static struct command_result *handle_pay(struct command *cmd, + const char *buf, + const jsmntok_t *params) +{ + u64 *msatoshi; + struct bolt11 *b11; + const char *b11str; + char *fail; + double *riskfactor; + unsigned int *retryfor; + struct pay_command *pc = tal(cmd, struct pay_command); + double *maxfeepercent; + unsigned int *maxdelay; + u64 *exemptfee; + + setup_locale(); + + if (!param(cmd, buf, params, + p_req("bolt11", param_string, &b11str), + p_opt("msatoshi", param_u64, &msatoshi), + p_opt("description", param_string, &pc->desc), + p_opt_def("riskfactor", param_double, &riskfactor, 1.0), + p_opt_def("maxfeepercent", param_percent, &maxfeepercent, 0.5), + p_opt_def("retry_for", param_number, &retryfor, 60), + p_opt_def("maxdelay", param_number, &maxdelay, + maxdelay_default), + p_opt_def("exemptfee", param_u64, &exemptfee, 5000), + NULL)) + return NULL; + + b11 = bolt11_decode(cmd, b11str, pc->desc, &fail); + if (!b11) { + return command_fail(cmd, JSONRPC2_INVALID_PARAMS, + "Invalid bolt11: %s", fail); + } + + if (time_now().ts.tv_sec > b11->timestamp + b11->expiry) { + return command_fail(cmd, PAY_INVOICE_EXPIRED, "Invoice expired"); + } + + if (b11->msatoshi) { + if (msatoshi) { + return command_fail(cmd, JSONRPC2_INVALID_PARAMS, + "msatoshi parameter unnecessary"); + } + pc->msatoshi = *b11->msatoshi; + } else { + if (!msatoshi) { + return command_fail(cmd, JSONRPC2_INVALID_PARAMS, + "msatoshi parameter required"); + } + pc->msatoshi = *msatoshi; + } + + pc->maxfeepercent = *maxfeepercent; + pc->maxdelay = *maxdelay; + pc->exemptfee = *exemptfee; + pc->riskfactor = *riskfactor; + pc->final_cltv = b11->min_final_cltv_expiry; + pc->dest = type_to_string(cmd, struct pubkey, &b11->receiver_id); + pc->shadow_dest = tal_strdup(pc, pc->dest); + pc->payment_hash = type_to_string(pc, struct sha256, + &b11->payment_hash); + pc->stoptime = timeabs_add(time_now(), time_from_sec(*retryfor)); + pc->excludes = tal_arr(cmd, const char *, 0); + pc->ps = add_pay_status(pc, b11str); + pc->routehints = filter_routehints(pc, b11->routes); + pc->expensive_route = NULL; + + /* Get capacities of local channels. */ + return send_outreq(cmd, "listpeers", listpeers_done, forward_error, pc, + /* gcc doesn't like zero-length format strings! */ + " "); +} + +/* FIXME: Add this to ccan/time? */ +#define UTC_TIMELEN (sizeof("YYYY-mm-ddTHH:MM:SS.nnnZ")) +static void utc_timestring(const struct timeabs *time, char str[UTC_TIMELEN]) +{ + char iso8601_msec_fmt[sizeof("YYYY-mm-ddTHH:MM:SS.%03dZ")]; + + strftime(iso8601_msec_fmt, sizeof(iso8601_msec_fmt), "%FT%T.%%03dZ", + gmtime(&time->ts.tv_sec)); + snprintf(str, UTC_TIMELEN, iso8601_msec_fmt, + (int) time->ts.tv_nsec / 1000000); +} + +static void add_attempt(char **ret, + const struct pay_status *ps, + const struct pay_attempt *attempt) +{ + char timestr[UTC_TIMELEN]; + + utc_timestring(&attempt->start, timestr); + + tal_append_fmt(ret, "{ 'strategy': '%s'," + " 'start_time': '%s'," + " 'age_in_seconds': %"PRIu64, + attempt->why, + timestr, + time_to_sec(time_between(time_now(), attempt->start))); + if (attempt->result || attempt->failure) { + utc_timestring(&attempt->end, timestr); + tal_append_fmt(ret, ", 'end_time': '%s'" + ", 'duration_in_seconds': %"PRIu64, + timestr, + time_to_sec(time_between(attempt->end, + attempt->start))); + } + if (tal_count(attempt->routehint)) { + tal_append_fmt(ret, ", 'routehint': ["); + for (size_t i = 0; i < tal_count(attempt->routehint); i++) { + tal_append_fmt(ret, "%s{" + " 'id': '%s'," + " 'channel': '%s'," + " 'msatoshi': %"PRIu64"," + " 'delay': %u }", + i == 0 ? "" : ", ", + type_to_string(tmpctx, struct pubkey, + &attempt->routehint[i].pubkey), + type_to_string(tmpctx, + struct short_channel_id, + &attempt->routehint[i].short_channel_id), + route_msatoshi(ps->msatoshi, + attempt->routehint + i, + tal_count(attempt->routehint) - i), + route_cltv(ps->final_cltv, + attempt->routehint + i, + tal_count(attempt->routehint) - i)); + } + tal_append_fmt(ret, "]"); + } + if (tal_count(attempt->excludes)) { + for (size_t i = 0; i < tal_count(attempt->excludes); i++) { + if (i == 0) + tal_append_fmt(ret, ", 'excluded_channels': ["); + else + tal_append_fmt(ret, ", "); + tal_append_fmt(ret, "'%s'", attempt->excludes[i]); + } + tal_append_fmt(ret, "]"); + } + + if (attempt->route) + tal_append_fmt(ret, ", 'route': %s", attempt->route); + + if (attempt->failure) + tal_append_fmt(ret, ", 'failure': %s", attempt->failure); + + if (attempt->result) + tal_append_fmt(ret, ", 'success': %s", attempt->result); + + tal_append_fmt(ret, "}"); +} + +static struct command_result *handle_paystatus(struct command *cmd, + const char *buf, + const jsmntok_t *params) +{ + struct pay_status *ps; + const char *b11str; + char *ret; + bool some = false; + + if (!param(cmd, buf, params, + p_opt("bolt11", param_string, &b11str), + NULL)) + return NULL; + + ret = tal_fmt(cmd, "{ 'pay': ["); + /* FIXME: Index by bolt11 string! */ + list_for_each(&pay_status, ps, list) { + if (b11str && !streq(b11str, ps->bolt11)) + continue; + + if (some) + tal_append_fmt(&ret, ",\n"); + some = true; + + tal_append_fmt(&ret, "{ 'bolt11': '%s'," + " 'msatoshi': %"PRIu64", " + " 'destination': '%s'", + ps->bolt11, ps->msatoshi, ps->dest); + if (ps->desc) + tal_append_fmt(&ret, ", 'description': '%s'", ps->desc); + if (ps->routehint_modifications) + tal_append_fmt(&ret, ", 'routehint_modifications': '%s'", + ps->routehint_modifications); + if (ps->shadow && !streq(ps->shadow, "")) + tal_append_fmt(&ret, ", 'shadow': '%s'", ps->shadow); + if (ps->exclusions) + tal_append_fmt(&ret, ", 'local_exclusions': '%s'", + ps->exclusions); + + assert(tal_count(ps->attempts)); + for (size_t i = 0; i < tal_count(ps->attempts); i++) { + if (i == 0) + tal_append_fmt(&ret, ", 'attempts': ["); + else + tal_append_fmt(&ret, ","); + + add_attempt(&ret, ps, &ps->attempts[i]); + } + tal_append_fmt(&ret, "] }"); + } + tal_append_fmt(&ret, "] }"); + + return command_success(cmd, ret); +} + +static void init(struct plugin_conn *rpc) +{ + const char *field; + + field = rpc_delve(tmpctx, "getinfo", "", rpc, ".id"); + if (!pubkey_from_hexstr(field, strlen(field), &my_id)) + plugin_err("getinfo didn't contain valid id: '%s'", field); + + field = rpc_delve(tmpctx, "listconfigs", + "'config': 'max-locktime-blocks'", + rpc, ".max-locktime-blocks"); + maxdelay_default = atoi(field); +} + +static const struct plugin_command commands[] = { { + "pay", + "Send payment specified by {bolt11} with {msatoshi}", + "Try to send a payment, retrying {retry_for} seconds before giving up", + handle_pay + }, { + "paystatus", + "Detail status of attempts to pay {bolt11}, or all", + "Covers both old payments and current ones.", + handle_paystatus + } +}; + +int main(int argc, char *argv[]) +{ + plugin_main(argv, init, commands, ARRAY_SIZE(commands)); +} diff --git a/tests/test_misc.py b/tests/test_misc.py index 33fbdd0349fe..ccae4fd89697 100644 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -234,7 +234,15 @@ def test_htlc_out_timeout(node_factory, bitcoind, executor): l1.daemon.wait_for_log('dev_disconnect: @WIRE_REVOKE_AND_ACK') # Takes 6 blocks to timeout (cltv-final + 1), but we also give grace period of 1 block. - bitcoind.generate_block(5 + 1) + # shadow route can add extra blocks! + status = only_one(l1.rpc.call('paystatus')['pay']) + if 'shadow' in status: + shadowlen = 6 * status['shadow'].count('Added 6 cltv delay for shadow') + else: + shadowlen = 0 + + bitcoind.generate_block(5 + 1 + shadowlen) + time.sleep(3) assert not l1.daemon.is_in_log('hit deadline') bitcoind.generate_block(1) @@ -293,7 +301,13 @@ def test_htlc_in_timeout(node_factory, bitcoind, executor): l1.daemon.wait_for_log('dev_disconnect: -WIRE_REVOKE_AND_ACK') # Deadline HTLC expiry minus 1/2 cltv-expiry delta (rounded up) (== cltv - 3). cltv is 5+1. - bitcoind.generate_block(2) + # shadow route can add extra blocks! + status = only_one(l1.rpc.call('paystatus')['pay']) + if 'shadow' in status: + shadowlen = 6 * status['shadow'].count('Added 6 cltv delay for shadow') + else: + shadowlen = 0 + bitcoind.generate_block(2 + shadowlen) assert not l2.daemon.is_in_log('hit deadline') bitcoind.generate_block(1) @@ -303,7 +317,7 @@ def test_htlc_in_timeout(node_factory, bitcoind, executor): l2.daemon.wait_for_log(' to ONCHAIN') l1.daemon.wait_for_log(' to ONCHAIN') - # L2 will collect HTLC + # L2 will collect HTLC (iff no shadow route) l2.daemon.wait_for_log('Propose handling OUR_UNILATERAL/THEIR_HTLC by OUR_HTLC_SUCCESS_TX .* after 0 blocks') l2.daemon.wait_for_log('sendrawtx exit 0') bitcoind.generate_block(1) @@ -905,11 +919,16 @@ def test_htlc_send_timeout(node_factory, bitcoind): err = excinfo.value # Complaints it couldn't find route. - assert err.error['code'] == 205 + # FIXME: include in pylightning + PAY_ROUTE_NOT_FOUND = 205 + assert err.error['code'] == PAY_ROUTE_NOT_FOUND + + status = only_one(l1.rpc.call('paystatus')['pay']) + # Temporary channel failure - assert only_one(err.error['data']['failures'])['failcode'] == 0x1007 - assert only_one(err.error['data']['failures'])['erring_node'] == l2.info['id'] - assert only_one(err.error['data']['failures'])['erring_channel'] == chanid2 + assert status['attempts'][0]['failure']['data']['failcode'] == 0x1007 + assert status['attempts'][0]['failure']['data']['erring_node'] == l2.info['id'] + assert status['attempts'][0]['failure']['data']['erring_channel'] == chanid2 # L2 should send ping, but never receive pong so never send commitment. l2.daemon.wait_for_log(r'channeld.*:\[OUT\] 0012') diff --git a/tests/test_pay.py b/tests/test_pay.py index dd2085b6f9fc..0ed7c10dc841 100644 --- a/tests/test_pay.py +++ b/tests/test_pay.py @@ -59,6 +59,47 @@ def test_pay(node_factory): assert len(payments) == 1 and payments[0]['payment_preimage'] == preimage +def test_pay_limits(node_factory): + """Test that we enforce fee max percentage and max delay""" + l1, l2, l3 = node_factory.line_graph(3, wait_for_announce=True) + + # FIXME: pylightning should define these! + PAY_ROUTE_TOO_EXPENSIVE = 206 + + inv = l3.rpc.invoice("any", "any", 'description') + + # Fee too high. + with pytest.raises(RpcError, match=r'Route wanted fee of .* msatoshis') as err: + l1.rpc.call('pay', {'bolt11': inv['bolt11'], 'msatoshi': 100000, 'maxfeepercent': 0.0001, 'exemptfee': 0}) + + assert err.value.error['code'] == PAY_ROUTE_TOO_EXPENSIVE + + # It should have retried (once without routehint, too) + status = l1.rpc.call('paystatus', {'bolt11': inv['bolt11']})['pay'][0]['attempts'] + assert len(status) == 3 + assert status[0]['strategy'] == "Initial attempt" + assert status[1]['strategy'].startswith("Excluded expensive channel ") + assert status[2]['strategy'] == "Removed route hint" + + # Delay too high. + with pytest.raises(RpcError, match=r'Route wanted delay of .* blocks') as err: + l1.rpc.call('pay', {'bolt11': inv['bolt11'], 'msatoshi': 100000, 'maxdelay': 0}) + + assert err.value.error['code'] == PAY_ROUTE_TOO_EXPENSIVE + # Should also have retried. + status = l1.rpc.call('paystatus', {'bolt11': inv['bolt11']})['pay'][1]['attempts'] + assert len(status) == 3 + assert status[0]['strategy'] == "Initial attempt" + assert status[1]['strategy'].startswith("Excluded delaying channel ") + assert status[2]['strategy'] == "Removed route hint" + + # This works, because fee is less than exemptfee. + l1.rpc.call('pay', {'bolt11': inv['bolt11'], 'msatoshi': 100000, 'maxfeepercent': 0.0001, 'exemptfee': 2000}) + status = l1.rpc.call('paystatus', {'bolt11': inv['bolt11']})['pay'][2]['attempts'] + assert len(status) == 1 + assert status[0]['strategy'] == "Initial attempt" + + def test_pay0(node_factory): """Test paying 0 amount """ @@ -1119,3 +1160,200 @@ def test_pay_variants(node_factory): b11 = 'LIGHTNING:' + l2.rpc.invoice(123000, 'test_pay_variants upper with prefix', 'description')['bolt11'].upper() l1.rpc.decodepay(b11) l1.rpc.pay(b11) + + +def test_pay_retry(node_factory, bitcoind): + """Make sure pay command retries properly. """ + def exhaust_channel(funder, fundee, scid, already_spent=0): + """Spend all available capacity (10^6 - 1%) of channel""" + maxpay = (10**6 - 10**6 // 100 - 13440) * 1000 - already_spent + inv = fundee.rpc.invoice(maxpay, + ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(20)), + "exhaust_channel") + routestep = { + 'msatoshi': maxpay, + 'id': fundee.info['id'], + 'delay': 10, + 'channel': scid + } + funder.rpc.sendpay([routestep], inv['payment_hash']) + funder.rpc.waitsendpay(inv['payment_hash']) + + # We connect every node to l5; in a line and individually. + # Keep fixed fees so we can easily calculate exhaustion + l1, l2, l3, l4, l5 = node_factory.line_graph(5, fundchannel=False, + opts={'feerates': (7500, 7500, 7500)}) + + # scid12 + l1.fund_channel(l2, 10**6, wait_for_active=False) + # scid23 + l2.fund_channel(l3, 10**6, wait_for_active=False) + # scid34 + l3.fund_channel(l4, 10**6, wait_for_active=False) + scid45 = l4.fund_channel(l5, 10**6, wait_for_active=False) + + l1.rpc.connect(l5.info['id'], 'localhost', l5.port) + scid15 = l1.fund_channel(l5, 10**6, wait_for_active=False) + l2.rpc.connect(l5.info['id'], 'localhost', l5.port) + scid25 = l2.fund_channel(l5, 10**6, wait_for_active=False) + l3.rpc.connect(l5.info['id'], 'localhost', l5.port) + scid35 = l3.fund_channel(l5, 10**6, wait_for_active=False) + + # Make sure l1 sees all 7 channels + bitcoind.generate_block(5) + wait_for(lambda: len(l1.rpc.listchannels()['channels']) == 14) + + # Exhaust shortcut channels one at a time, to force retries. + exhaust_channel(l1, l5, scid15) + exhaust_channel(l2, l5, scid25) + exhaust_channel(l3, l5, scid35) + + # Pay l1->l5 should succeed via straight line (eventually) + l1.rpc.pay(l5.rpc.invoice(10**8, 'test_retry', 'test_retry')['bolt11']) + + # This should make it fail. + exhaust_channel(l4, l5, scid45, 10**8) + + with pytest.raises(RpcError): + l1.rpc.pay(l5.rpc.invoice(10**8, 'test_retry2', 'test_retry2')['bolt11']) + + +@unittest.skipIf(not DEVELOPER, "needs DEVELOPER=1 otherwise gossip takes 5 minutes!") +def test_pay_routeboost(node_factory, bitcoind): + """Make sure we can use routeboost information. """ + # l1->l2->l3--private-->l4 + l1, l2 = node_factory.line_graph(2, announce_channels=True, wait_for_announce=True) + l3, l4, l5 = node_factory.line_graph(3, announce_channels=False, wait_for_announce=False) + l2.rpc.connect(l3.info['id'], 'localhost', l3.port) + scidl2l3 = l2.fund_channel(l3, 10**6) + + # Make sure l1 knows about the 2->3 channel. + bitcoind.generate_block(5) + l1.daemon.wait_for_logs([r'update for channel {}/0 now ACTIVE' + .format(scidl2l3), + r'update for channel {}/1 now ACTIVE' + .format(scidl2l3)]) + # Make sure l4 knows about 2->3 channel too so it's not a dead-end. + l4.daemon.wait_for_logs([r'update for channel {}/0 now ACTIVE' + .format(scidl2l3), + r'update for channel {}/1 now ACTIVE' + .format(scidl2l3)]) + + # Get an l4 invoice; it should put the private channel in routeboost. + inv = l4.rpc.invoice(10**5, 'test_pay_routeboost', 'test_pay_routeboost', + exposeprivatechannels=True) + assert 'warning_capacity' not in inv + assert 'warning_offline' not in inv + assert only_one(only_one(l1.rpc.decodepay(inv['bolt11'])['routes'])) + + # Now we should be able to pay it. + start = time.time() + l1.rpc.pay(inv['bolt11']) + end = time.time() + + # Status should show all the gory details. + status = l1.rpc.call('paystatus', [inv['bolt11']]) + assert only_one(status['pay'])['bolt11'] == inv['bolt11'] + assert only_one(status['pay'])['msatoshi'] == 10**5 + assert only_one(status['pay'])['destination'] == l4.info['id'] + assert 'description' not in only_one(status['pay']) + assert 'routehint_modifications' not in only_one(status['pay']) + assert 'local_exclusions' not in only_one(status['pay']) + attempt = only_one(only_one(status['pay'])['attempts']) + assert attempt['age_in_seconds'] <= time.time() - start + assert attempt['duration_in_seconds'] <= end - start + assert only_one(attempt['routehint']) + assert only_one(attempt['routehint'])['id'] == l3.info['id'] + assert only_one(attempt['routehint'])['msatoshi'] == 10**5 + 1 + 10**5 // 100000 + assert only_one(attempt['routehint'])['delay'] == 5 + 6 + + # With dev-route option we can test longer routehints. + if DEVELOPER: + scid34 = only_one(l3.rpc.listpeers(l4.info['id'])['peers'])['channels'][0]['short_channel_id'] + scid45 = only_one(l4.rpc.listpeers(l5.info['id'])['peers'])['channels'][0]['short_channel_id'] + routel3l4l5 = [{'id': l3.info['id'], + 'short_channel_id': scid34, + 'fee_base_msat': 1000, + 'fee_proportional_millionths': 10, + 'cltv_expiry_delta': 6}, + {'id': l4.info['id'], + 'short_channel_id': scid45, + 'fee_base_msat': 1000, + 'fee_proportional_millionths': 10, + 'cltv_expiry_delta': 6}] + inv = l5.rpc.call('invoice', {'msatoshi': 10**5, + 'label': 'test_pay_routeboost2', + 'description': 'test_pay_routeboost2', + 'dev-routes': [routel3l4l5]}) + l1.rpc.pay(inv['bolt11']) + status = l1.rpc.call('paystatus', [inv['bolt11']]) + assert len(only_one(status['pay'])['attempts']) == 1 + assert 'failure' not in only_one(status['pay'])['attempts'][0] + assert 'success' in only_one(status['pay'])['attempts'][0] + + # Now test that it falls back correctly to not using routeboost + # if it can't route to the node mentioned + routel4l3 = [{'id': l4.info['id'], + 'short_channel_id': scid34, + 'fee_base_msat': 1000, + 'fee_proportional_millionths': 10, + 'cltv_expiry_delta': 6}] + inv = l3.rpc.call('invoice', {'msatoshi': 10**5, + 'label': 'test_pay_routeboost3', + 'description': 'test_pay_routeboost3', + 'dev-routes': [routel4l3]}) + l1.rpc.pay(inv['bolt11']) + status = l1.rpc.call('paystatus', [inv['bolt11']]) + assert len(only_one(status['pay'])['attempts']) == 2 + assert 'failure' in only_one(status['pay'])['attempts'][0] + assert 'success' not in only_one(status['pay'])['attempts'][0] + routehint = only_one(status['pay'])['attempts'][0]['routehint'] + assert [h['channel'] for h in routehint] == [r['short_channel_id'] for r in routel4l3] + assert 'failure' not in only_one(status['pay'])['attempts'][1] + assert 'success' in only_one(status['pay'])['attempts'][1] + assert 'routehint' not in only_one(status['pay'])['attempts'][1] + + # Similarly if it can route, but payment fails. + routel2bad = [{'id': l2.info['id'], + 'short_channel_id': scid34, # Invalid scid + 'fee_base_msat': 1000, + 'fee_proportional_millionths': 10, + 'cltv_expiry_delta': 6}] + inv = l3.rpc.call('invoice', {'msatoshi': 10**5, + 'label': 'test_pay_routeboost4', + 'description': 'test_pay_routeboost4', + 'dev-routes': [routel2bad]}) + l1.rpc.pay(inv['bolt11']) + + # Finally, it should fall back to second routehint if first fails. + # (Note, this is not public because it's not 6 deep) + l3.rpc.connect(l5.info['id'], 'localhost', l5.port) + scid35 = l3.fund_channel(l5, 10**6) + l4.stop() + routel3l5 = [{'id': l3.info['id'], + 'short_channel_id': scid35, + 'fee_base_msat': 1000, + 'fee_proportional_millionths': 10, + 'cltv_expiry_delta': 6}] + inv = l5.rpc.call('invoice', {'msatoshi': 10**5, + 'label': 'test_pay_routeboost5', + 'description': 'test_pay_routeboost5', + 'dev-routes': [routel3l4l5, routel3l5]}) + l1.rpc.pay(inv['bolt11'], description="paying test_pay_routeboost5") + + status = l1.rpc.call('paystatus', [inv['bolt11']]) + assert only_one(status['pay'])['bolt11'] == inv['bolt11'] + assert only_one(status['pay'])['msatoshi'] == 10**5 + assert only_one(status['pay'])['destination'] == l5.info['id'] + assert only_one(status['pay'])['description'] == "paying test_pay_routeboost5" + assert 'routehint_modifications' not in only_one(status['pay']) + assert 'local_exclusions' not in only_one(status['pay']) + attempts = only_one(status['pay'])['attempts'] + + # First failed, second succeeded. + assert len(attempts) == 2 + assert 'success' not in attempts[0] + assert 'success' in attempts[1] + + assert [h['channel'] for h in attempts[0]['routehint']] == [r['short_channel_id'] for r in routel3l4l5] + assert [h['channel'] for h in attempts[1]['routehint']] == [r['short_channel_id'] for r in routel3l5] diff --git a/tests/test_plugin.py b/tests/test_plugin.py index d76a563aba67..dcd3165a1a60 100644 --- a/tests/test_plugin.py +++ b/tests/test_plugin.py @@ -108,3 +108,11 @@ def test_failing_plugins(): '--plugin={}'.format(p), '--help', ]) + + +def test_pay_plugin(node_factory): + l1, l2 = node_factory.line_graph(2) + inv = l2.rpc.invoice(123000, 'label', 'description', 3700) + + res = l1.rpc.pay(bolt11=inv['bolt11']) + assert res['status'] == 'complete' diff --git a/tests/utils.py b/tests/utils.py index 87dacddd3f28..ea7c7a0c5529 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -697,6 +697,7 @@ def split_options(self, opts): 'may_reconnect', 'random_hsm', 'log_all_io', + 'feerates', ] node_opts = {k: v for k, v in opts.items() if k in node_opt_keys} cli_opts = {k: v for k, v in opts.items() if k not in node_opt_keys}