From 130eac771e439a98eb04d38fca9eb42b977b57ba Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Tue, 22 Oct 2024 13:29:47 +1030 Subject: [PATCH] lightningd: new command injectpaymentonion. This is like `sendonion` but unwraps the onion as the first hop, avoiding nasty special cases for blinded paths which start with this node, and also self-pay. Tests split into multiple ones after Christian's review. Changelog-Added: JSON-RPC: `injectpaymentonion` for initiating an HTLC like a peer would do. Signed-off-by: Rusty Russell --- common/jsonrpc_errors.h | 1 + contrib/msggen/msggen/schema.json | 125 +++++ doc/Makefile | 1 + doc/index.rst | 1 + doc/schemas/lightning-injectpaymentonion.json | 125 +++++ lightningd/pay.c | 391 ++++++++++++- lightningd/peer_htlcs.c | 16 +- lightningd/peer_htlcs.h | 12 + tests/test_pay.py | 517 +++++++++++++++++- wallet/test/run-wallet.c | 8 + 10 files changed, 1184 insertions(+), 13 deletions(-) create mode 100644 doc/schemas/lightning-injectpaymentonion.json diff --git a/common/jsonrpc_errors.h b/common/jsonrpc_errors.h index ca69a9bfc0d9..96a09da2365b 100644 --- a/common/jsonrpc_errors.h +++ b/common/jsonrpc_errors.h @@ -51,6 +51,7 @@ enum jsonrpc_errcode { PAY_INSUFFICIENT_FUNDS = 215, PAY_UNREACHABLE = 216, PAY_USER_ERROR = 217, + PAY_INJECTPAYMENTONION_FAILED = 218, /* `fundchannel` or `withdraw` errors */ FUND_MAX_EXCEEDED = 300, diff --git a/contrib/msggen/msggen/schema.json b/contrib/msggen/msggen/schema.json index d0f25cdab27c..e255f365d5eb 100644 --- a/contrib/msggen/msggen/schema.json +++ b/contrib/msggen/msggen/schema.json @@ -14979,6 +14979,131 @@ } ] }, + "lightning-injectpaymentonion.json": { + "$schema": "../rpc-schema-draft.json", + "type": "object", + "additionalProperties": false, + "rpc": "injectpaymentonion", + "title": "Send a payment with a custom onion packet", + "description": [ + "The **injectpaymentonion** RPC command causes the node to receive a payment attempt similar to the way it would receive one from a peer. The onion packet is unwrapped, then handled normally: either as a local payment, or forwarded to the next peer.", + "Compared to lightning-sendonion(7): the handling of blinded paths and self-payments is trivial, and the interface blocks until the payment succeeds or fails." + ], + "request": { + "required": [ + "onion", + "payment_hash", + "amount_msat", + "cltv_expiry", + "partid", + "groupid" + ], + "properties": { + "onion": { + "type": "hex", + "description": [ + "Hex-encoded 1366 bytes long blob that was returned by either of the tools that can generate onions. It contains the payloads destined for each hop and some metadata. Please refer to [BOLT 04][bolt04] for further details. If is specific to the route that is being used and the *payment_hash* used to construct, and therefore cannot be reused for other payments or to attempt a separate route. The custom onion can generally be created using the `devtools/onion` CLI tool, or the **createonion** RPC command." + ] + }, + "payment_hash": { + "type": "hash", + "description": [ + "Specifies the 32 byte hex-encoded hash to use as a challenge to the HTLC that we are sending. It is specific to the onion and has to match the one the onion was created with." + ] + }, + "amount_msat": { + "type": "msat", + "description": [ + "The amount for the first HTLC in millisatoshis. This is also the amount which will be forwarded to the first peer (if any) as we do not charge fees on our own payments." + ] + }, + "cltv_expiry": { + "type": "u16", + "description": [ + "The cltv_expiry for the first HTLC in blocks. This must be greater than the current blockheight." + ] + }, + "partid": { + "type": "u64", + "description": [ + "The non-zero identifier for multiple parallel partial payments with the same *payment_hash*." + ] + }, + "groupid": { + "type": "u64", + "description": [ + "Grouping key to disambiguate multiple attempts to pay the same *payment_hash*. All payments in other groups must be completed before starting a new group." + ] + }, + "label": { + "type": "string", + "description": [ + "Can be used to provide a human readable reference to retrieve the payment at a later time." + ] + }, + "invstring": { + "type": "string", + "description": [ + "Usually a bolt11 or bolt12 string, which, it will be returned in *waitsendpay* and *listsendpays* results." + ] + }, + "localinvreqid": { + "type": "hash", + "description": [ + "`localinvreqid` is used by offers to link a payment attempt to a local `invoice_request` offer created by lightningd-invoicerequest(7)." + ] + } + } + }, + "response": { + "required": [ + "created_index", + "created_at", + "completed_at", + "payment_preimage" + ], + "properties": { + "created_at": { + "type": "u64", + "description": [ + "The UNIX timestamp showing when this payment was initiated." + ] + }, + "completed_at": { + "type": "u64", + "description": [ + "The UNIX timestamp showing when this payment was completed." + ] + }, + "created_index": { + "type": "u64", + "description": [ + "1-based index indicating order this payment was created in." + ] + } + } + }, + "errors": [ + "The following error codes may occur:", + "", + "- 218: injectpaymentonion failed", + "", + "The *onionreply* is returned in the error details, which can be unwrapped to discover the error" + ], + "author": [ + "Rusty Russell <> is mainly responsible." + ], + "see_also": [ + "lightning-createonion(7)", + "lightning-sendonion(7)", + "lightning-listsendpays(7)" + ], + "resources": [ + "Main web site: ", + "", + "[bolt04]: https://github.com/lightning/bolts/blob/master/04-onion-routing.md" + ] + }, "lightning-invoice.json": { "$schema": "../rpc-schema-draft.json", "type": "object", diff --git a/doc/Makefile b/doc/Makefile index 42556572da38..50c54fd1152d 100644 --- a/doc/Makefile +++ b/doc/Makefile @@ -66,6 +66,7 @@ GENERATE_MARKDOWN := doc/lightning-addgossip.7 \ doc/lightning-getroute.7 \ doc/lightning-getroutes.7 \ doc/lightning-help.7 \ + doc/lightning-injectpaymentonion.7 \ doc/lightning-invoice.7 \ doc/lightning-invoicerequest.7 \ doc/lightning-keysend.7 \ diff --git a/doc/index.rst b/doc/index.rst index bb48bce62bb6..d45018c6d768 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -76,6 +76,7 @@ Core Lightning Documentation lightning-getroutes lightning-help lightning-hsmtool + lightning-injectpaymentonion lightning-invoice lightning-invoicerequest lightning-keysend diff --git a/doc/schemas/lightning-injectpaymentonion.json b/doc/schemas/lightning-injectpaymentonion.json new file mode 100644 index 000000000000..3965b0e4a6ae --- /dev/null +++ b/doc/schemas/lightning-injectpaymentonion.json @@ -0,0 +1,125 @@ +{ + "$schema": "../rpc-schema-draft.json", + "type": "object", + "additionalProperties": false, + "rpc": "injectpaymentonion", + "title": "Send a payment with a custom onion packet", + "description": [ + "The **injectpaymentonion** RPC command causes the node to receive a payment attempt similar to the way it would receive one from a peer. The onion packet is unwrapped, then handled normally: either as a local payment, or forwarded to the next peer.", + "Compared to lightning-sendonion(7): the handling of blinded paths and self-payments is trivial, and the interface blocks until the payment succeeds or fails." + ], + "request": { + "required": [ + "onion", + "payment_hash", + "amount_msat", + "cltv_expiry", + "partid", + "groupid" + ], + "properties": { + "onion": { + "type": "hex", + "description": [ + "Hex-encoded 1366 bytes long blob that was returned by either of the tools that can generate onions. It contains the payloads destined for each hop and some metadata. Please refer to [BOLT 04][bolt04] for further details. If is specific to the route that is being used and the *payment_hash* used to construct, and therefore cannot be reused for other payments or to attempt a separate route. The custom onion can generally be created using the `devtools/onion` CLI tool, or the **createonion** RPC command." + ] + }, + "payment_hash": { + "type": "hash", + "description": [ + "Specifies the 32 byte hex-encoded hash to use as a challenge to the HTLC that we are sending. It is specific to the onion and has to match the one the onion was created with." + ] + }, + "amount_msat": { + "type": "msat", + "description": [ + "The amount for the first HTLC in millisatoshis. This is also the amount which will be forwarded to the first peer (if any) as we do not charge fees on our own payments." + ] + }, + "cltv_expiry": { + "type": "u16", + "description": [ + "The cltv_expiry for the first HTLC in blocks. This must be greater than the current blockheight." + ] + }, + "partid": { + "type": "u64", + "description": [ + "The non-zero identifier for multiple parallel partial payments with the same *payment_hash*." + ] + }, + "groupid": { + "type": "u64", + "description": [ + "Grouping key to disambiguate multiple attempts to pay the same *payment_hash*. All payments in other groups must be completed before starting a new group." + ] + }, + "label": { + "type": "string", + "description": [ + "Can be used to provide a human readable reference to retrieve the payment at a later time." + ] + }, + "invstring": { + "type": "string", + "description": [ + "Usually a bolt11 or bolt12 string, which, it will be returned in *waitsendpay* and *listsendpays* results." + ] + }, + "localinvreqid": { + "type": "hash", + "description": [ + "`localinvreqid` is used by offers to link a payment attempt to a local `invoice_request` offer created by lightningd-invoicerequest(7)." + ] + } + } + }, + "response": { + "required": [ + "created_index", + "created_at", + "completed_at", + "payment_preimage" + ], + "properties": { + "created_at": { + "type": "u64", + "description": [ + "The UNIX timestamp showing when this payment was initiated." + ] + }, + "completed_at": { + "type": "u64", + "description": [ + "The UNIX timestamp showing when this payment was completed." + ] + }, + "created_index": { + "type": "u64", + "description": [ + "1-based index indicating order this payment was created in." + ] + } + } + }, + "errors": [ + "The following error codes may occur:", + "", + "- 218: injectpaymentonion failed", + "", + "The *onionreply* is returned in the error details, which can be unwrapped to discover the error" + ], + "author": [ + "Rusty Russell <> is mainly responsible." + ], + "see_also": [ + "lightning-createonion(7)", + "lightning-sendonion(7)", + "lightning-listsendpays(7)" + ], + "resources": [ + "Main web site: ", + "", + "[bolt04]: https://github.com/lightning/bolts/blob/master/04-onion-routing.md" + ] +} diff --git a/lightningd/pay.c b/lightningd/pay.c index 129bc38737e7..b1f961e7aa7d 100644 --- a/lightningd/pay.c +++ b/lightningd/pay.c @@ -2,10 +2,12 @@ #include #include #include +#include #include #include #include #include +#include #include #include #include @@ -587,11 +589,9 @@ void payment_failed(struct lightningd *ld, failstr = localfail; pay_errcode = PAY_TRY_OTHER_ROUTE; } else if (payment->path_secrets == NULL) { - /* This was a payment initiated with `sendonion`, we therefore + /* This was a payment initiated with `sendonion`/`injectonionmessage`, we therefore * don't have the path secrets and cannot decode the error - * onion. Let's store it and hope whatever called `sendonion` - * knows how to deal with these. */ - + * onion. We hand it to the user. */ pay_errcode = PAY_UNPARSEABLE_ONION; fail = NULL; failstr = NULL; @@ -1672,6 +1672,389 @@ static const struct json_command waitsendpay_command = { }; AUTODATA(json_command, &waitsendpay_command); +static struct command_result * +injectonion_fail(struct command *cmd, + const struct wallet_payment *payment, + enum jsonrpc_errcode pay_errcode, + const struct onionreply *onionreply, + const struct routing_failure *fail, + const char *errmsg, + struct secret *shared_secret) +{ + struct json_stream *js; + + /* Turn local errors into onion reply. */ + if (!onionreply) + onionreply = create_onionreply(tmpctx, shared_secret, fail->msg); + + js = json_stream_fail(cmd, PAY_INJECTPAYMENTONION_FAILED, errmsg); + /* We wrap the onion reply, as it expects. */ + json_add_hex_talarr(js, "onionreply", + wrap_onionreply(tmpctx, shared_secret, onionreply) + ->contents); + + json_object_end(js); + return command_failed(cmd, js); +} + +static struct command_result * +injectonion_succeed(struct command *cmd, + const struct wallet_payment *payment, + void *unused) +{ + struct json_stream *response = json_stream_success(cmd); + + assert(payment->status == PAYMENT_COMPLETE); + + json_add_u64(response, "created_index", payment->id); + json_add_u32(response, "created_at", payment->timestamp); + json_add_u32(response, "completed_at", *payment->completed_at); + json_add_preimage(response, "payment_preimage", payment->payment_preimage); + return command_success(cmd, response); +} + +struct selfpay { + struct command *cmd; + struct secret shared_secret; + u64 partid, groupid; + struct sha256 payment_hash; +}; + +/* FIXME: Map errors better using payment_failed? */ +static void selfpay_mpp_fail(struct selfpay *selfpay, const u8 *failmsg TAKES) +{ + struct onionreply *reply = create_onionreply(tmpctx, &selfpay->shared_secret, failmsg); + + if (taken(failmsg)) + tal_steal(selfpay->cmd, failmsg); + + payment_failed(selfpay->cmd->ld, + selfpay->cmd->ld->log, + &selfpay->payment_hash, + selfpay->partid, + selfpay->groupid, + reply, + NULL, + NULL); +} + +static void selfpay_mpp_succeeded(struct selfpay *selfpay, + const struct preimage *preimage) +{ + payment_succeeded(selfpay->cmd->ld, + &selfpay->payment_hash, + selfpay->partid, + selfpay->groupid, + preimage); +} + +static struct command_result *param_u64_nonzero(struct command *cmd, + const char *name, + const char *buffer, + const jsmntok_t *tok, + u64 **val) +{ + struct command_result *res = param_u64(cmd, name, buffer, tok, val); + if (res == NULL && *val == 0) + res = command_fail_badparam(cmd, name, buffer, tok, + "Must be non-zero"); + return res; +} + +static void register_payment_and_waiter(struct command *cmd, + const struct sha256 *payment_hash, + u64 partid, u64 groupid, + struct amount_msat msat, + struct amount_msat msat_sent, + struct amount_msat total_msat, + const char *label, + const char *invstring, + struct sha256 *local_invreq_id, + const struct secret *shared_secret) +{ + wallet_add_payment(cmd, + cmd->ld->wallet, + time_now().ts.tv_sec, + NULL, + payment_hash, + partid, + groupid, + PAYMENT_PENDING, + NULL, + msat, + msat_sent, + total_msat, + NULL, + NULL, + NULL, + NULL, + invstring, + label, + NULL, + NULL, + local_invreq_id); + + /* Now we wait for htlc to resolve (it will need shared_secret!) */ + add_waitsendpay_waiter(cmd->ld, cmd, payment_hash, partid, groupid, + injectonion_succeed, injectonion_fail, + tal_dup(cmd, struct secret, shared_secret)); +} + +static struct command_result *json_injectpaymentonion(struct command *cmd, + const char *buffer, + const jsmntok_t *obj UNNEEDED, + const jsmntok_t *params) +{ + u8 *onion; + enum onion_wire failcode; + struct sha256 *payment_hash; + struct lightningd *ld = cmd->ld; + const char *label, *invstring; + struct pubkey *blinding, *next_path_key; + struct amount_msat *msat; + u32 *cltv; + u64 *partid, *groupid; + struct sha256 *local_invreq_id; + struct secret shared_secret; + struct onionpacket *op; + struct onion_payload *payload; + struct route_step *rs; + u64 failtlvtype; + size_t failtlvpos; + struct channel *next; + struct command_result *ret; + const u8 *failmsg; + struct htlc_out *hout; + + if (!param_check(cmd, buffer, params, + p_req("onion", param_bin_from_hex, &onion), + p_req("payment_hash", param_sha256, &payment_hash), + p_req("amount_msat", param_msat, &msat), + p_req("cltv_expiry", param_u32, &cltv), + p_req("partid", param_u64_nonzero, &partid), + p_req("groupid", param_u64, &groupid), + p_opt("blinding", param_pubkey, &blinding), + p_opt("label", param_escaped_string, &label), + p_opt("invstring", param_invstring, &invstring), + p_opt("localinvreqid", param_sha256, &local_invreq_id), + NULL)) + return command_param_failed(); + + /* Safety check: reconcile this with previous attempts, check + * partid/groupid uniqueness: we don't know amount or total. */ + ret = check_progress(cmd->ld, cmd, payment_hash, AMOUNT_MSAT(0), + AMOUNT_MSAT(0), + *partid, *groupid, NULL); + if (ret) + return ret; + + /* This checks we're not trying to pay our a locally-generated + * invoice_request more than once. */ + ret = check_invoice_request_usage(cmd, local_invreq_id); + if (ret) + return ret; + + if (tal_bytelen(onion) != TOTAL_PACKET_SIZE(ROUTING_INFO_SIZE)) { + return command_fail(cmd, JSONRPC2_INVALID_PARAMS, + "onion must be %u bytes long", + TOTAL_PACKET_SIZE(ROUTING_INFO_SIZE)); + } + + op = parse_onionpacket(tmpctx, onion, tal_bytelen(onion), + &failcode); + if (!op) { + return command_fail(cmd, JSONRPC2_INVALID_PARAMS, + "Could not parse onion: %s", + onion_wire_name(failcode)); + } + + if (!ecdh_maybe_blinding(&op->ephemeralkey, + blinding, + &shared_secret)) { + return command_fail(cmd, JSONRPC2_INVALID_PARAMS, + "Could not tweak ephemeral key"); + } + + rs = process_onionpacket(tmpctx, op, &shared_secret, + payment_hash->u.u8, + sizeof(payment_hash->u.u8)); + if (!rs) { + return command_fail(cmd, JSONRPC2_INVALID_PARAMS, + "Could not process onion"); + } + + payload = onion_decode(tmpctx, rs, blinding, + cmd->ld->accept_extra_tlv_types, + *msat, *cltv, + &failtlvtype, + &failtlvpos); + if (!payload) { + return command_fail(cmd, JSONRPC2_INVALID_PARAMS, + "Onion decode for %s failed at type %"PRIu64" offset %zu", + tal_hex(tmpctx, rs->raw_payload), + failtlvtype, failtlvpos); + } + + if (payload->final) { + struct selfpay *selfpay; + + if (command_check_only(cmd)) + return command_check_done(cmd); + + selfpay = tal(cmd, struct selfpay); + selfpay->cmd = cmd; + selfpay->shared_secret = shared_secret; + selfpay->partid = *partid; + selfpay->groupid = *groupid; + selfpay->payment_hash = *payment_hash; + + /* We actually *do* know msat delivered and total msat, but + * then check_progress will complain on the next part, because + * we don't know it then, so leave them 0 */ + register_payment_and_waiter(cmd, + payment_hash, + *partid, *groupid, + AMOUNT_MSAT(0), *msat, AMOUNT_MSAT(0), + label, invstring, local_invreq_id, + &shared_secret); + + /* Mark it pending now, though htlc_set_add might + * not resolve immediately */ + fixme_ignore(command_still_pending(cmd)); + htlc_set_add(cmd->ld, cmd->ld->log, *msat, *payload->total_msat, + payment_hash, payload->payment_secret, + selfpay_mpp_fail, selfpay_mpp_succeeded, + selfpay); + return command_its_complicated("htlc_set_add may have immediately succeeded or failed"); + } + + /* If they use scid, we use exactly the channel they tell us to here! */ + if (payload->forward_channel) { + next = any_channel_by_scid(cmd->ld, + *payload->forward_channel, + false); + if (!next) + return command_fail(cmd, JSONRPC2_INVALID_PARAMS, + "Unknown scid %s", + fmt_short_channel_id(tmpctx, + *payload->forward_channel)); + + if (!channel_state_can_add_htlc(next->state)) { + return command_fail(cmd, JSONRPC2_INVALID_PARAMS, + "channel %s in state %s", + fmt_short_channel_id(tmpctx, + *payload->forward_channel), + channel_state_str(next->state)); + } + } else { + struct node_id nid; + struct peer *next_peer; + + node_id_from_pubkey(&nid, payload->forward_node_id); + next_peer = peer_by_id(cmd->ld, &nid); + if (!next_peer) + return command_fail(cmd, JSONRPC2_INVALID_PARAMS, + "Unknown peer %s", + fmt_node_id(tmpctx, &nid)); + + next = best_channel(cmd->ld, next_peer, *msat, NULL); + if (!next) + return command_fail(cmd, JSONRPC2_INVALID_PARAMS, + "No available channel with peer %s", + fmt_node_id(tmpctx, &nid)); + } + + if (amount_msat_greater(*msat, next->htlc_maximum_msat) + || amount_msat_less(*msat, next->htlc_minimum_msat)) { + /* Are we in old-range grace-period? */ + if (!time_before(time_now(), next->old_feerate_timeout) + || amount_msat_less(*msat, next->old_htlc_minimum_msat) + || amount_msat_greater(*msat, next->old_htlc_maximum_msat)) { + return command_fail(cmd, JSONRPC2_INVALID_PARAMS, + "Amount %s not in htlc min/max range %s-%s", + fmt_amount_msat(tmpctx, *msat), + fmt_amount_msat(tmpctx, next->htlc_minimum_msat), + fmt_amount_msat(tmpctx, next->htlc_maximum_msat)); + } + log_info(next->log, + "Allowing payment using older htlc_minimum/maximum_msat"); + } + + /* BOLT #2: + * + * An offering node: + * - MUST estimate a timeout deadline for each HTLC it offers. + * - MUST NOT offer an HTLC with a timeout deadline before its + * `cltv_expiry`. + */ + /* In our case, G = 1, so we need to expire it one after it's expiration. + * But never offer an expired HTLC; that's dumb. */ + if (get_block_height(cmd->ld->topology) >= *cltv) { + return command_fail(cmd, JSONRPC2_INVALID_PARAMS, + "Expiry cltv %u too close to current %u", + *cltv, + get_block_height(ld->topology)); + } + + /* BOLT #4: + * + * - if the `cltv_expiry` is more than `max_htlc_cltv` in the future: + * - return an `expiry_too_far` error. + */ + if (get_block_height(ld->topology) + + ld->config.max_htlc_cltv < *cltv) { + return command_fail(cmd, JSONRPC2_INVALID_PARAMS, + "Expiry cltv %u too far from current %u + max %u", + *cltv, + get_block_height(ld->topology), + ld->config.max_htlc_cltv); + } + + /* We could have blinding from cmdline or from inside onion. */ + if (payload->path_key) { + struct sha256 sha; + blinding_hash_e_and_ss(payload->path_key, + &payload->blinding_ss, + &sha); + next_path_key = tal(tmpctx, struct pubkey); + blinding_next_path_key(payload->path_key, &sha, + next_path_key); + } else + next_path_key = NULL; + + if (command_check_only(cmd)) + return command_check_done(cmd); + + register_payment_and_waiter(cmd, + payment_hash, + *partid, *groupid, + AMOUNT_MSAT(0), *msat, AMOUNT_MSAT(0), + label, invstring, local_invreq_id, + &shared_secret); + + failmsg = send_htlc_out(tmpctx, next, *msat, + /* We set final_msat to the same, so fees == 0 + * (in fact, we don't know!) */ + *cltv, *msat, + payment_hash, + next_path_key, *partid, *groupid, + serialize_onionpacket(tmpctx, rs->next), + NULL, &hout); + if (failmsg) { + return command_fail(cmd, JSONRPC2_INVALID_PARAMS, + "Could not send to first peer: %s", + onion_wire_name(fromwire_peektype(failmsg))); + } + return command_still_pending(cmd); +} + +static const struct json_command injectpaymentonion_command = { + "injectpaymentonion", + json_injectpaymentonion, +}; +AUTODATA(json_command, &injectpaymentonion_command); + + static u64 sendpay_index_inc(struct lightningd *ld, const struct sha256 *payment_hash, u64 partid, diff --git a/lightningd/peer_htlcs.c b/lightningd/peer_htlcs.c index 018623f78137..dff7e495ebc2 100644 --- a/lightningd/peer_htlcs.c +++ b/lightningd/peer_htlcs.c @@ -739,13 +739,13 @@ const u8 *send_htlc_out(const tal_t *ctx, /* What's the best channel to this peer? * If @hint is set, channel must match that one. */ -static struct channel *best_channel(struct lightningd *ld, - const struct peer *next_peer, - struct amount_msat amt_to_forward, - struct channel *hint) +struct channel *best_channel(struct lightningd *ld, + const struct peer *next_peer, + struct amount_msat amt_to_forward, + const struct channel *hint) { struct amount_msat best_spendable = AMOUNT_MSAT(0); - struct channel *channel, *best = hint; + struct channel *channel, *best = cast_const(struct channel *, hint); /* Seek channel with largest spendable! */ list_for_each(&next_peer->channels, channel, list) { @@ -1202,9 +1202,9 @@ htlc_accepted_hook_final(struct htlc_accepted_hook_payload *request STEALS) } /* Apply tweak to ephemeral key if path_key is non-NULL, then do ECDH */ -static bool ecdh_maybe_blinding(const struct pubkey *ephemeral_key, - const struct pubkey *path_key, - struct secret *ss) +bool ecdh_maybe_blinding(const struct pubkey *ephemeral_key, + const struct pubkey *path_key, + struct secret *ss) { struct pubkey point = *ephemeral_key; diff --git a/lightningd/peer_htlcs.h b/lightningd/peer_htlcs.h index 3232c944187c..9b9e67255b74 100644 --- a/lightningd/peer_htlcs.h +++ b/lightningd/peer_htlcs.h @@ -54,6 +54,18 @@ void fixup_htlcs_out(struct lightningd *ld); void htlcs_resubmit(struct lightningd *ld, struct htlc_in_map *unconnected_htlcs_in STEALS); +/* Apply tweak to ephemeral key if path_key is non-NULL, then do ECDH */ +bool ecdh_maybe_blinding(const struct pubkey *ephemeral_key, + const struct pubkey *path_key, + struct secret *ss); + +/* Select best (highest capacity) to peer. If hint is set, must match that + * feerate */ +struct channel *best_channel(struct lightningd *ld, + const struct peer *next_peer, + struct amount_msat amt_to_forward, + const struct channel *hint); + /* For HTLCs which terminate here, invoice payment calls one of these. */ void fulfill_htlc(struct htlc_in *hin, const struct preimage *preimage); void local_fail_in_htlc(struct htlc_in *hin, const u8 *failmsg TAKES); diff --git a/tests/test_pay.py b/tests/test_pay.py index 76ad2a72758a..540ee1ea78f0 100644 --- a/tests/test_pay.py +++ b/tests/test_pay.py @@ -1,12 +1,14 @@ from fixtures import * # noqa: F401,F403 from fixtures import TEST_NETWORK +from hashlib import sha256 from pathlib import Path from pyln.client import RpcError, Millisatoshi from pyln.proto.onion import TlvPayload from pyln.testing.utils import EXPERIMENTAL_DUAL_FUND, FUNDAMOUNT, scid_to_int from utils import ( wait_for, only_one, sync_blockheight, TIMEOUT, - mine_funding_to_announce, first_scid, serialize_payload_tlv, serialize_payload_final_tlv + mine_funding_to_announce, first_scid, serialize_payload_tlv, serialize_payload_final_tlv, + tu64_encode ) import copy import os @@ -6065,3 +6067,516 @@ def test_pay_remember_hint(node_factory): # We should not have touched fw1, and should succeed after a single call p = sender.rpc.pay(inv) assert(p['parts'] == 1) + + +def test_injectpaymentonion_simple(node_factory, executor): + l1, l2 = node_factory.line_graph(2) + + blockheight = l1.rpc.getinfo()['blockheight'] + inv1 = l2.rpc.invoice(1000, "test_injectpaymentonion1", "test_injectpaymentonion1") + + # First hop for injectpaymentonion is self. + hops = [{'pubkey': l1.info['id'], + 'payload': serialize_payload_tlv(1000, 18 + 6, first_scid(l1, l2), blockheight).hex()}, + {'pubkey': l2.info['id'], + 'payload': serialize_payload_final_tlv(1000, 18, 1000, blockheight, inv1['payment_secret']).hex()}] + onion = l1.rpc.createonion(hops=hops, assocdata=inv1['payment_hash']) + + ret = l1.rpc.injectpaymentonion(onion=onion['onion'], + payment_hash=inv1['payment_hash'], + amount_msat=1000, + cltv_expiry=blockheight + 18 + 6, + partid=1, + groupid=0) + assert ret['completed_at'] >= ret['created_at'] + assert sha256(bytes.fromhex(ret['payment_preimage'])).hexdigest() == inv1['payment_hash'] + assert ret == {'payment_preimage': ret['payment_preimage'], + 'created_index': 1, + 'completed_at': ret['completed_at'], + 'created_at': ret['created_at']} + assert only_one(l2.rpc.listinvoices("test_injectpaymentonion1")['invoices'])['status'] == 'paid' + lsp = only_one(l1.rpc.listsendpays(inv1['bolt11'])['payments']) + assert lsp['groupid'] == 0 + assert lsp['partid'] == 1 + assert lsp['payment_hash'] == inv1['payment_hash'] + assert lsp['status'] == 'complete' + + +def test_injectpaymentonion_mpp(node_factory, executor): + l1, l2 = node_factory.line_graph(2) + + blockheight = l1.rpc.getinfo()['blockheight'] + inv2 = l2.rpc.invoice(3000, "test_injectpaymentonion2", "test_injectpaymentonion2") + + # First hop for injectpaymentonion is self. + hops1 = [{'pubkey': l1.info['id'], + 'payload': serialize_payload_tlv(1000, 18 + 6, first_scid(l1, l2), blockheight).hex()}, + {'pubkey': l2.info['id'], + 'payload': serialize_payload_final_tlv(1000, 18, 3000, blockheight, inv2['payment_secret']).hex()}] + onion1 = l1.rpc.createonion(hops=hops1, assocdata=inv2['payment_hash']) + hops2 = [{'pubkey': l1.info['id'], + 'payload': serialize_payload_tlv(2000, 18 + 6, first_scid(l1, l2), blockheight).hex()}, + {'pubkey': l2.info['id'], + 'payload': serialize_payload_final_tlv(2000, 18, 3000, blockheight, inv2['payment_secret']).hex()}] + onion2 = l1.rpc.createonion(hops=hops2, assocdata=inv2['payment_hash']) + + fut1 = executor.submit(l1.rpc.injectpaymentonion, + onion1['onion'], + inv2['payment_hash'], + 1000, + blockheight + 18 + 6, + 1, + 0) + fut2 = executor.submit(l1.rpc.injectpaymentonion, + onion2['onion'], + inv2['payment_hash'], + 2000, + blockheight + 18 + 6, + 2, + 0) + + # Now both should complete. + ret = fut1.result(TIMEOUT) + assert sha256(bytes.fromhex(ret['payment_preimage'])).hexdigest() == inv2['payment_hash'] + ret = fut2.result(TIMEOUT) + assert sha256(bytes.fromhex(ret['payment_preimage'])).hexdigest() == inv2['payment_hash'] + + assert only_one(l2.rpc.listinvoices("test_injectpaymentonion2")['invoices'])['status'] == 'paid' + lsps = l1.rpc.listsendpays(inv2['bolt11'])['payments'] + for lsp in lsps: + assert lsp['groupid'] == 0 + assert lsp['partid'] == 1 or lsp['partid'] == 2 + assert lsp['payment_hash'] == inv2['payment_hash'] + assert lsp['status'] == 'complete' + assert len(lsps) == 2 + + +def test_injectpaymentonion_3hop(node_factory, executor): + l1, l2, l3 = node_factory.line_graph(3, wait_for_announce=True) + + blockheight = l1.rpc.getinfo()['blockheight'] + inv3 = l3.rpc.invoice(1000, "test_injectpaymentonion3", "test_injectpaymentonion3") + + # First hop for injectpaymentonion is self. + hops = [{'pubkey': l1.info['id'], + 'payload': serialize_payload_tlv(1001, 18 + 6 + 6, first_scid(l1, l2), blockheight).hex()}, + {'pubkey': l2.info['id'], + 'payload': serialize_payload_tlv(1000, 18 + 6, first_scid(l3, l2), blockheight).hex()}, + {'pubkey': l3.info['id'], + 'payload': serialize_payload_final_tlv(1000, 18, 1000, blockheight, inv3['payment_secret']).hex()}] + onion = l1.rpc.createonion(hops=hops, assocdata=inv3['payment_hash']) + + ret = l1.rpc.injectpaymentonion(onion=onion['onion'], + payment_hash=inv3['payment_hash'], + amount_msat=1001, + cltv_expiry=blockheight + 18 + 6 + 6, + partid=1, + groupid=0) + assert sha256(bytes.fromhex(ret['payment_preimage'])).hexdigest() == inv3['payment_hash'] + assert only_one(l3.rpc.listinvoices("test_injectpaymentonion3")['invoices'])['status'] == 'paid' + lsp = only_one(l1.rpc.listsendpays(inv3['bolt11'])['payments']) + assert lsp['groupid'] == 0 + assert lsp['partid'] == 1 + assert lsp['payment_hash'] == inv3['payment_hash'] + assert lsp['status'] == 'complete' + + +def test_injectpaymentonion_selfpay(node_factory, executor): + l1, l2 = node_factory.line_graph(2, opts={'experimental-offers': None}) + + blockheight = l1.rpc.getinfo()['blockheight'] + + # Test simple self-pay. + inv4 = l1.rpc.invoice(1000, "test_injectpaymentonion4", "test_injectpaymentonion4") + + # First hop for injectpaymentonion is self. + hops = [{'pubkey': l1.info['id'], + 'payload': serialize_payload_final_tlv(1000, 18, 1000, blockheight, inv4['payment_secret']).hex()}] + onion = l1.rpc.createonion(hops=hops, assocdata=inv4['payment_hash']) + + ret = l1.rpc.injectpaymentonion(onion=onion['onion'], + payment_hash=inv4['payment_hash'], + amount_msat=1000, + cltv_expiry=blockheight + 18, + partid=1, + groupid=0) + assert sha256(bytes.fromhex(ret['payment_preimage'])).hexdigest() == inv4['payment_hash'] + assert only_one(l1.rpc.listinvoices("test_injectpaymentonion4")['invoices'])['status'] == 'paid' + lsp = only_one(l1.rpc.listsendpays(inv4['bolt11'])['payments']) + assert lsp['groupid'] == 0 + assert lsp['partid'] == 1 + assert lsp['payment_hash'] == inv4['payment_hash'] + assert lsp['status'] == 'complete' + + # Test self-pay with MPP. + inv5 = l1.rpc.invoice(1000, "test_injectpaymentonion5", "test_injectpaymentonion5") + + # First hop for injectpaymentonion is self. + hops1 = [{'pubkey': l1.info['id'], + 'payload': serialize_payload_final_tlv(333, 18, 1000, blockheight, inv5['payment_secret']).hex()}] + onion1 = l1.rpc.createonion(hops=hops1, assocdata=inv5['payment_hash']) + hops2 = [{'pubkey': l1.info['id'], + 'payload': serialize_payload_final_tlv(666, 18, 1000, blockheight, inv5['payment_secret']).hex()}] + onion2 = l1.rpc.createonion(hops=hops2, assocdata=inv5['payment_hash']) + + fut1 = executor.submit(l1.rpc.injectpaymentonion, + onion1['onion'], + inv5['payment_hash'], + 333, + blockheight + 18, + 1, + 0) + fut2 = executor.submit(l1.rpc.injectpaymentonion, + onion2['onion'], + inv5['payment_hash'], + 667, + blockheight + 18, + 2, + 0) + # Now both should complete. + ret = fut1.result(TIMEOUT) + assert sha256(bytes.fromhex(ret['payment_preimage'])).hexdigest() == inv5['payment_hash'] + + ret = fut2.result(TIMEOUT) + assert sha256(bytes.fromhex(ret['payment_preimage'])).hexdigest() == inv5['payment_hash'] + + assert only_one(l1.rpc.listinvoices("test_injectpaymentonion5")['invoices'])['status'] == 'paid' + lsps = l1.rpc.listsendpays(inv5['bolt11'])['payments'] + for lsp in lsps: + assert lsp['groupid'] == 0 + assert lsp['partid'] == 1 or lsp['partid'] == 2 + assert lsp['payment_hash'] == inv5['payment_hash'] + assert lsp['status'] == 'complete' + assert len(lsps) == 2 + + # Check listpays gives a reasonable result! + pays = only_one(l1.rpc.listpays(inv5['bolt11'])['pays']) + # Don't know these values + del pays['created_at'] + del pays['completed_at'] + del pays['preimage'] + assert pays == {'bolt11': inv5['bolt11'], + 'payment_hash': inv5['payment_hash'], + 'status': "complete", + 'amount_sent_msat': 1000, + 'number_of_parts': 2} + + # Test self-pay with MPP from non-selfpay. + inv6 = l2.rpc.invoice(3000, "test_injectpaymentonion6", "test_injectpaymentonion6") + + # First hop for injectpaymentonion is self. + hops1 = [{'pubkey': l1.info['id'], + 'payload': serialize_payload_tlv(1000, 18 + 6, first_scid(l1, l2), blockheight).hex()}, + {'pubkey': l2.info['id'], + 'payload': serialize_payload_final_tlv(1000, 18, 3000, blockheight, inv6['payment_secret']).hex()}] + onion1 = l1.rpc.createonion(hops=hops1, assocdata=inv6['payment_hash']) + hops2 = [{'pubkey': l2.info['id'], + 'payload': serialize_payload_final_tlv(2000, 18, 3000, blockheight, inv6['payment_secret']).hex()}] + onion2 = l1.rpc.createonion(hops=hops2, assocdata=inv6['payment_hash']) + + fut1 = executor.submit(l1.rpc.injectpaymentonion, + onion1['onion'], + inv6['payment_hash'], + 1000, + blockheight + 18 + 6, + 1, + 0) + fut2 = executor.submit(l2.rpc.injectpaymentonion, + onion2['onion'], + inv6['payment_hash'], + 2000, + blockheight + 18, + 2, + 1) + + # Now both should complete. + ret = fut1.result(TIMEOUT) + assert sha256(bytes.fromhex(ret['payment_preimage'])).hexdigest() == inv6['payment_hash'] + + ret = fut2.result(TIMEOUT) + assert sha256(bytes.fromhex(ret['payment_preimage'])).hexdigest() == inv6['payment_hash'] + + assert only_one(l2.rpc.listinvoices("test_injectpaymentonion6")['invoices'])['status'] == 'paid' + lsp = only_one(l1.rpc.listsendpays(inv6['bolt11'])['payments']) + assert lsp['groupid'] == 0 + assert lsp['partid'] == 1 + assert lsp['payment_hash'] == inv6['payment_hash'] + assert lsp['status'] == 'complete' + lsp = only_one(l2.rpc.listsendpays(inv6['bolt11'])['payments']) + assert lsp['groupid'] == 1 + assert lsp['partid'] == 2 + assert lsp['payment_hash'] == inv6['payment_hash'] + assert lsp['status'] == 'complete' + + # Test bolt12 self-pay. + offer = l1.rpc.offer('any') + inv10 = l1.rpc.fetchinvoice(offer['bolt12'], '1000msat') + decoded = l1.rpc.decode(inv10['invoice']) + + final_tlvs = TlvPayload() + final_tlvs.add_field(2, tu64_encode(1000)) + final_tlvs.add_field(4, tu64_encode(blockheight + 18)) + final_tlvs.add_field(10, bytes.fromhex(decoded['invoice_paths'][0]['path'][0]['encrypted_recipient_data'])) + final_tlvs.add_field(12, bytes.fromhex(decoded['invoice_paths'][0]['first_path_key'])) + final_tlvs.add_field(18, tu64_encode(1000)) + + hops = [{'pubkey': l1.info['id'], + 'payload': final_tlvs.to_bytes().hex()}] + onion = l1.rpc.createonion(hops=hops, assocdata=decoded['invoice_payment_hash']) + + ret = l1.rpc.injectpaymentonion(onion=onion['onion'], + payment_hash=decoded['invoice_payment_hash'], + amount_msat=1000, + cltv_expiry=blockheight + 18, + partid=1, + groupid=0) + assert sha256(bytes.fromhex(ret['payment_preimage'])).hexdigest() == decoded['invoice_payment_hash'] + # The label for the invoice is deterministic. + label = f"{decoded['offer_id']}-{decoded['invreq_payer_id']}-0" + assert only_one(l1.rpc.listinvoices(label)['invoices'])['status'] == 'paid' + lsp = only_one(l1.rpc.listsendpays(inv4['bolt11'])['payments']) + assert lsp['groupid'] == 0 + assert lsp['partid'] == 1 + assert lsp['payment_hash'] == inv4['payment_hash'] + assert lsp['status'] == 'complete' + + +def test_injectpaymentonion_blindedpath(node_factory, executor): + l1, l2 = node_factory.line_graph(2, + wait_for_announce=True, + opts={'experimental-offers': None}) + blockheight = l1.rpc.getinfo()['blockheight'] + + # Test bolt12, with stub blinded path. + offer = l2.rpc.offer('any') + inv7 = l1.rpc.fetchinvoice(offer['bolt12'], '1000msat') + + decoded = l1.rpc.decode(inv7['invoice']) + assert len(decoded['invoice_paths']) == 1 + path_key = decoded['invoice_paths'][0]['first_path_key'] + assert decoded['invoice_paths'][0]['first_node_id'] == l2.info['id'] + path = decoded['invoice_paths'][0]['path'] + assert len(path) == 1 + + # Manually encode the onion payload to include blinded info + # BOLT #4: + # - For every node inside a blinded route: + # - MUST include the `encrypted_recipient_data` provided by the recipient + # - For the first node in the blinded route: + # - MUST include the `path_key` provided by the recipient in `current_path_key` + # - If it is the final node: + # - MUST include `amt_to_forward`, `outgoing_cltv_value` and `total_amount_msat`. + # - The value set for `outgoing_cltv_value`: + # - MUST use the current block height as a baseline value. + # - if a [random offset](07-routing-gossip.md#recommendations-for-routing) was added to improve privacy: + # - SHOULD add the offset to the baseline value. + # - MUST NOT include any other tlv field. + final_tlvs = TlvPayload() + + # BOLT #4: + # 1. type: 2 (`amt_to_forward`) + # 2. data: + # * [`tu64`:`amt_to_forward`] + # 1. type: 4 (`outgoing_cltv_value`) + # 2. data: + # * [`tu32`:`outgoing_cltv_value`] + # ... + # 1. type: 10 (`encrypted_recipient_data`) + # 2. data: + # * [`...*byte`:`encrypted_recipient_data`] + # 1. type: 12 (`current_path_key`) + # 2. data: + # * [`point`:`path_key`] + # ... + # 1. type: 18 (`total_amount_msat`) + # 2. data: + # * [`tu64`:`total_msat`] + final_tlvs.add_field(2, tu64_encode(1000)) + final_tlvs.add_field(4, tu64_encode(blockheight + 18)) + final_tlvs.add_field(10, bytes.fromhex(path[0]['encrypted_recipient_data'])) + final_tlvs.add_field(12, bytes.fromhex(path_key)) + final_tlvs.add_field(18, tu64_encode(1000)) + + hops = [{'pubkey': l1.info['id'], + 'payload': serialize_payload_tlv(1000, 18 + 6, first_scid(l1, l2), blockheight).hex()}, + {'pubkey': l2.info['id'], + 'payload': final_tlvs.to_bytes().hex()}] + onion = l1.rpc.createonion(hops=hops, assocdata=decoded['invoice_payment_hash']) + + ret = l1.rpc.injectpaymentonion(onion=onion['onion'], + payment_hash=decoded['invoice_payment_hash'], + amount_msat=1000, + cltv_expiry=blockheight + 18 + 6, + partid=1, + groupid=0) + assert sha256(bytes.fromhex(ret['payment_preimage'])).hexdigest() == decoded['invoice_payment_hash'] + # The label for l2's invoice is deterministic. + label = f"{decoded['offer_id']}-{decoded['invreq_payer_id']}-0" + assert only_one(l2.rpc.listinvoices(label)['invoices'])['status'] == 'paid' + + lsp = only_one(l1.rpc.listsendpays(inv7['invoice'])['payments']) + assert lsp['groupid'] == 0 + assert lsp['partid'] == 1 + assert lsp['payment_hash'] == decoded['invoice_payment_hash'] + assert lsp['status'] == 'complete' + + # Now test bolt12 with real blinded path. + l4 = node_factory.get_node(options={'experimental-offers': None}) + # Private channel. + node_factory.join_nodes([l2, l4], announce_channels=False) + + # Make sure l4 knows about other nodes, so will add route hint. + wait_for(lambda: len(l4.rpc.listnodes()['nodes']) == 2) + offer = l4.rpc.offer('any') + inv8 = l1.rpc.fetchinvoice(offer['bolt12'], '1000msat') + + decoded = l1.rpc.decode(inv8['invoice']) + assert len(decoded['invoice_paths']) == 1 + path_key = decoded['invoice_paths'][0]['first_path_key'] + assert decoded['invoice_paths'][0]['first_node_id'] == l2.info['id'] + path = decoded['invoice_paths'][0]['path'] + assert len(path) == 2 + + mid_tlvs = TlvPayload() + mid_tlvs.add_field(10, bytes.fromhex(path[0]['encrypted_recipient_data'])) + mid_tlvs.add_field(12, bytes.fromhex(path_key)) + + final_tlvs = TlvPayload() + final_tlvs.add_field(2, tu64_encode(1000)) + final_tlvs.add_field(4, tu64_encode(blockheight + 18)) + final_tlvs.add_field(10, bytes.fromhex(path[1]['encrypted_recipient_data'])) + final_tlvs.add_field(18, tu64_encode(1000)) + + hops = [{'pubkey': l1.info['id'], + 'payload': serialize_payload_tlv(1001, 18 + 6 + 6, first_scid(l1, l2), blockheight).hex()}, + {'pubkey': l2.info['id'], + 'payload': mid_tlvs.to_bytes().hex()}, + {'pubkey': path[1]['blinded_node_id'], + 'payload': final_tlvs.to_bytes().hex()}] + onion = l1.rpc.createonion(hops=hops, assocdata=decoded['invoice_payment_hash']) + + ret = l1.rpc.injectpaymentonion(onion=onion['onion'], + payment_hash=decoded['invoice_payment_hash'], + amount_msat=1001, + cltv_expiry=blockheight + 18 + 6, + partid=1, + groupid=0) + assert sha256(bytes.fromhex(ret['payment_preimage'])).hexdigest() == decoded['invoice_payment_hash'] + # The label for l4's invoice is deterministic. + label = f"{decoded['offer_id']}-{decoded['invreq_payer_id']}-0" + assert only_one(l4.rpc.listinvoices(label)['invoices'])['status'] == 'paid' + lsp = only_one(l1.rpc.listsendpays(inv8['invoice'])['payments']) + assert lsp['groupid'] == 0 + assert lsp['partid'] == 1 + assert lsp['payment_hash'] == decoded['invoice_payment_hash'] + assert lsp['status'] == 'complete' + + # Finally, with blinded path which starts with us. + offer = l4.rpc.offer('any') + inv9 = l1.rpc.fetchinvoice(offer['bolt12'], '1000msat') + + decoded = l1.rpc.decode(inv9['invoice']) + assert len(decoded['invoice_paths']) == 1 + path_key = decoded['invoice_paths'][0]['first_path_key'] + assert decoded['invoice_paths'][0]['first_node_id'] == l2.info['id'] + path = decoded['invoice_paths'][0]['path'] + assert len(path) == 2 + + mid_tlvs = TlvPayload() + mid_tlvs.add_field(10, bytes.fromhex(path[0]['encrypted_recipient_data'])) + mid_tlvs.add_field(12, bytes.fromhex(path_key)) + + final_tlvs = TlvPayload() + final_tlvs.add_field(2, tu64_encode(1000)) + final_tlvs.add_field(4, tu64_encode(blockheight + 18)) + final_tlvs.add_field(10, bytes.fromhex(path[1]['encrypted_recipient_data'])) + final_tlvs.add_field(18, tu64_encode(1000)) + + hops = [{'pubkey': l2.info['id'], + 'payload': mid_tlvs.to_bytes().hex()}, + {'pubkey': path[1]['blinded_node_id'], + 'payload': final_tlvs.to_bytes().hex()}] + onion = l1.rpc.createonion(hops=hops, assocdata=decoded['invoice_payment_hash']) + + ret = l2.rpc.injectpaymentonion(onion=onion['onion'], + payment_hash=decoded['invoice_payment_hash'], + amount_msat=1001, + cltv_expiry=blockheight + 18 + 6, + partid=1, + groupid=0) + assert sha256(bytes.fromhex(ret['payment_preimage'])).hexdigest() == decoded['invoice_payment_hash'] + # The label for the invoice is deterministic. + label = f"{decoded['offer_id']}-{decoded['invreq_payer_id']}-0" + assert only_one(l4.rpc.listinvoices(label)['invoices'])['status'] == 'paid' + lsp = only_one(l2.rpc.listsendpays(inv9['invoice'])['payments']) + assert lsp['groupid'] == 0 + assert lsp['partid'] == 1 + assert lsp['payment_hash'] == decoded['invoice_payment_hash'] + assert lsp['status'] == 'complete' + + +def test_injectpaymentonion_failures(node_factory, executor): + l1, l2 = node_factory.line_graph(2, wait_for_announce=True) + blockheight = l1.rpc.getinfo()['blockheight'] + + # + # Failure cases should give an onion: + # Unknown invoice. + # Unknown invoice (selfpay) + # Cannot forward. + + # Unknown invoice + hops = [{'pubkey': l1.info['id'], + 'payload': serialize_payload_tlv(1000, 18 + 6, first_scid(l1, l2), blockheight).hex()}, + {'pubkey': l2.info['id'], + 'payload': serialize_payload_final_tlv(1000, 18, 1000, blockheight, '00' * 32).hex()}] + onion = l1.rpc.createonion(hops=hops, assocdata='00' * 32) + + with pytest.raises(RpcError) as err: + l1.rpc.injectpaymentonion(onion=onion['onion'], + payment_hash='00' * 32, + amount_msat=1000, + cltv_expiry=blockheight + 18 + 6, + partid=1, + groupid=0) + + # PAY_INJECTPAYMENTONION_FAILED + assert err.value.error['code'] == 218 + assert 'onionreply' in err.value.error['data'] + + # Self-pay (unknown payment_hash) + hops = [{'pubkey': l1.info['id'], + 'payload': serialize_payload_final_tlv(1000, 18, 1000, blockheight, '00' * 32).hex()}] + onion = l1.rpc.createonion(hops=hops, assocdata='00' * 32) + + with pytest.raises(RpcError) as err: + l1.rpc.injectpaymentonion(onion=onion['onion'], + payment_hash='00' * 32, + amount_msat=1000, + cltv_expiry=blockheight + 18 + 6, + partid=1, + groupid=1) + + # PAY_INJECTPAYMENTONION_FAILED + assert err.value.error['code'] == 218 + assert 'onionreply' in err.value.error['data'] + + # Insufficient funds (l2 can't pay to l1) + inv11 = l1.rpc.invoice(3000, "test_injectpaymentonion11", "test_injectpaymentonion11") + hops = [{'pubkey': l2.info['id'], + 'payload': serialize_payload_tlv(1000, 18 + 6, first_scid(l1, l2), blockheight).hex()}, + {'pubkey': l1.info['id'], + 'payload': serialize_payload_final_tlv(1000, 18, 1000, blockheight, inv11['payment_secret']).hex()}] + onion = l1.rpc.createonion(hops=hops, assocdata=inv11['payment_hash']) + + with pytest.raises(RpcError) as err: + l2.rpc.injectpaymentonion(onion=onion['onion'], + payment_hash=inv11['payment_hash'], + amount_msat=1000, + cltv_expiry=blockheight + 18 + 6, + partid=1, + groupid=0) + + # PAY_INJECTPAYMENTONION_FAILED + assert err.value.error['code'] == 218 + assert 'onionreply' in err.value.error['data'] diff --git a/wallet/test/run-wallet.c b/wallet/test/run-wallet.c index 6713382b4bac..af2196d3ef41 100644 --- a/wallet/test/run-wallet.c +++ b/wallet/test/run-wallet.c @@ -152,6 +152,9 @@ struct command_result *command_failed(struct command *cmd UNNEEDED, struct json_stream *result) { fprintf(stderr, "command_failed called!\n"); abort(); } +/* Generated stub for command_its_complicated */ +struct command_result *command_its_complicated(const char *why UNNEEDED) +{ fprintf(stderr, "command_its_complicated called!\n"); abort(); } /* Generated stub for command_param_failed */ struct command_result *command_param_failed(void) @@ -840,6 +843,11 @@ struct command_result *param_number(struct command *cmd UNNEEDED, const char *na const char *buffer UNNEEDED, const jsmntok_t *tok UNNEEDED, unsigned int **num UNNEEDED) { fprintf(stderr, "param_number called!\n"); abort(); } +/* Generated stub for param_pubkey */ +struct command_result *param_pubkey(struct command *cmd UNNEEDED, const char *name UNNEEDED, + const char *buffer UNNEEDED, const jsmntok_t *tok UNNEEDED, + struct pubkey **pubkey UNNEEDED) +{ fprintf(stderr, "param_pubkey called!\n"); abort(); } /* Generated stub for param_secret */ struct command_result *param_secret(struct command *cmd UNNEEDED, const char *name UNNEEDED, const char *buffer UNNEEDED, const jsmntok_t *tok UNNEEDED,