Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

EXPERIMENTAL: MPP send and receive support (lowlevel) #3309

Merged
merged 22 commits into from
Dec 12, 2019
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
178892b
db: add partid, total_msat fields to payment entries.
rustyrussell Dec 11, 2019
ad4ed97
db: add partid field to htlc_out.
rustyrussell Dec 11, 2019
2a03434
htlcs: remove origin_htlc_id from htlc_out.
rustyrussell Dec 11, 2019
c654478
lightningd: share more code between sendpay and sendonion.
rustyrussell Dec 11, 2019
04a46f0
lightningd: change amount-in-flight check to be more nuanced.
rustyrussell Dec 11, 2019
c61227b
sendpay/sendonion: add optional partid arg, finesse msatoshi argument.
rustyrussell Dec 11, 2019
15fa972
configure: make partid payments only available with EXPERIMENTAL_FEAT…
rustyrussell Dec 11, 2019
3bc4636
waitsendpay: add partid arg.
rustyrussell Dec 11, 2019
94d3897
pytest: Add tests to make sure received onion is as expected.
rustyrussell Dec 11, 2019
2e4416e
doc: update experimental bolt version quotes.
rustyrussell Dec 11, 2019
d94ae31
lightningd: cleanup redundant args from handle_localpay
rustyrussell Dec 11, 2019
3c6e33a
lightningd: split invoice check into separate function.
rustyrussell Dec 11, 2019
555b217
lightningd: implement htlc sets.
rustyrussell Dec 11, 2019
73bf9e0
lightningd: wrap htlc replay in a database transaction.
rustyrussell Dec 11, 2019
1839483
lightningd: sew in htlc set.
rustyrussell Dec 11, 2019
8cee375
plugins: listpays ignores pre-0.7.0 or manual sendpay payments w/ no …
rustyrussell Dec 11, 2019
84a2753
plugins: listpays will now consolidate multi-part payments.
rustyrussell Dec 11, 2019
c6bbb41
common: offer option_basic_mpp for EXPERIMENTAL_FEATURES.
rustyrussell Dec 11, 2019
cbfc84f
pytest: add more multi-part-payment tests.
rustyrussell Dec 11, 2019
2b4ca09
lightningd: require payment_secret for MPP.
rustyrussell Dec 11, 2019
207ae69
lightningd: fix spurious "more than twice final" error.
rustyrussell Dec 12, 2019
e6edb76
lightningd: fix failure message in waitsendpay with multi-part payments.
rustyrussell Dec 12, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 20 additions & 14 deletions lightningd/pay.c
Original file line number Diff line number Diff line change
Expand Up @@ -280,11 +280,11 @@ void payment_succeeded(struct lightningd *ld, struct htlc_out *hout,
struct wallet_payment *payment;

wallet_payment_set_status(ld->wallet, &hout->payment_hash,
/* FIXME: Set partid! */ 0,
hout->partid,
PAYMENT_COMPLETE, rval);
payment = wallet_payment_by_hash(tmpctx, ld->wallet,
&hout->payment_hash,
/* FIXME: Set partid! */ 0);
hout->partid);
assert(payment);

tell_waiters_success(ld, &hout->payment_hash, payment);
Expand Down Expand Up @@ -982,7 +982,7 @@ send_payment(struct lightningd *ld,
final_tlv,
route[i].amount,
base_expiry + route[i].delay,
route[i].amount, payment_secret);
total_msat, payment_secret);
if (!onion) {
return command_fail(cmd, PAY_DESTINATION_PERM_FAIL,
"Destination does not support"
Expand Down Expand Up @@ -1094,13 +1094,15 @@ static struct command_result *json_sendonion(struct command *cmd,
struct lightningd *ld = cmd->ld;
const char *label;
struct secret *path_secrets;
u64 *partid;

if (!param(cmd, buffer, params,
p_req("onion", param_bin_from_hex, &onion),
p_req("first_hop", param_route_hop, &first_hop),
p_req("payment_hash", param_sha256, &payment_hash),
p_opt("label", param_escaped_string, &label),
p_opt("shared_secrets", param_secrets_array, &path_secrets),
p_opt_def("partid", param_u64, &partid, 0),
NULL))
return command_param_failed();

Expand All @@ -1112,7 +1114,7 @@ static struct command_result *json_sendonion(struct command *cmd,
"with failcode=%d",
failcode);

return send_payment_core(ld, cmd, payment_hash, /* FIXME: Set partid! */0,
return send_payment_core(ld, cmd, payment_hash, *partid,
first_hop, AMOUNT_MSAT(0), AMOUNT_MSAT(0),
cdecker marked this conversation as resolved.
Show resolved Hide resolved
label, NULL, &packet, NULL, NULL, NULL,
path_secrets);
Expand Down Expand Up @@ -1163,6 +1165,7 @@ static struct command_result *json_sendpay(struct command *cmd,
struct route_hop *route;
struct amount_msat *msat;
const char *b11str, *label = NULL;
u64 *partid;
struct secret *payment_secret;

/* For generating help, give new-style. */
Expand All @@ -1175,6 +1178,7 @@ static struct command_result *json_sendpay(struct command *cmd,
p_opt("bolt11", param_string, &b11str),
p_opt("payment_secret", param_secret,
&payment_secret),
p_opt_def("partid", param_u64, &partid, 0),
NULL))
return command_param_failed();
} else if (params->type == JSMN_ARRAY) {
Expand All @@ -1186,6 +1190,7 @@ static struct command_result *json_sendpay(struct command *cmd,
p_opt("bolt11", param_string, &b11str),
p_opt("payment_secret", param_secret,
&payment_secret),
p_opt_def("partid", param_u64, &partid, 0),
NULL))
return command_param_failed();
} else {
Expand All @@ -1199,6 +1204,7 @@ static struct command_result *json_sendpay(struct command *cmd,
p_opt("bolt11", param_string, &b11str),
p_opt("payment_secret", param_secret,
&payment_secret),
p_opt_def("partid", param_u64, &partid, 0),
NULL))
return command_param_failed();

Expand Down Expand Up @@ -1265,20 +1271,21 @@ static struct command_result *json_sendpay(struct command *cmd,
* requesting. The final hop amount is what we actually give, which can
* be from the msatoshi to twice msatoshi. */

/* if not: msatoshi <= finalhop.amount <= 2 * msatoshi, fail. */
if (*partid && !msat)
return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
"Must specify msatoshi with partid");

/* if not: finalhop.amount <= 2 * msatoshi, fail. */
if (msat) {
struct amount_msat limit = route[routetok->size-1].amount;

if (amount_msat_less(*msat, limit))
if (!amount_msat_add(&limit, limit, limit))
return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
"msatoshi %s less than final %s",
type_to_string(tmpctx,
struct amount_msat,
msat),
"Unbelievable final amount %s",
type_to_string(tmpctx,
struct amount_msat,
&route[routetok->size-1].amount));
limit.millisatoshis *= 2; /* Raw: sanity check */

if (amount_msat_greater(*msat, limit))
return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
"msatoshi %s more than twice final %s",
Expand All @@ -1299,10 +1306,9 @@ static struct command_result *json_sendpay(struct command *cmd,
}
#endif

return send_payment(cmd->ld, cmd, rhash, /* FIXME: Set partid! */ 0,
return send_payment(cmd->ld, cmd, rhash, *partid,
route,
msat ? *msat : route[routetok->size-1].amount,
/* FIXME: Set total_msat! */
route[routetok->size-1].amount,
msat ? *msat : route[routetok->size-1].amount,
label, b11str, payment_secret);
}
Expand Down
58 changes: 58 additions & 0 deletions tests/test_pay.py
Original file line number Diff line number Diff line change
Expand Up @@ -2567,3 +2567,61 @@ def serialize_payload(n):
except RpcError as e:
assert(e.error['code'] == 204)
assert(e.error['data']['raw_message'] == "400f00000000000003e80000006c")


@unittest.skipIf(not DEVELOPER, "needs DEVELOPER=1")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It'd also require EXPERIMENTAL_FEATURES. Merging #3335 also fixes this.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also fixed in the next request, but breaks bisectability.

def test_partial_payment(node_factory, bitcoind, executor):
# We want to test two payments at the same time, before we send commit
l1, l2, l3, l4 = node_factory.get_nodes(4, [{}] + [{'disconnect': ['=WIRE_UPDATE_ADD_HTLC-nocommit']}] * 2 + [{}])

# Two routes to l4: one via l2, and one via l3.
l1.rpc.connect(l2.info['id'], 'localhost', l2.port)
l1.fund_channel(l2, 100000)
l1.rpc.connect(l3.info['id'], 'localhost', l3.port)
l1.fund_channel(l3, 100000)
l2.rpc.connect(l4.info['id'], 'localhost', l4.port)
scid24 = l2.fund_channel(l4, 100000)
l3.rpc.connect(l4.info['id'], 'localhost', l4.port)
scid34 = l3.fund_channel(l4, 100000)
bitcoind.generate_block(5)

# Wait until l1 knows about all channels.
wait_for(lambda: len(l1.rpc.listchannels()['channels']) == 8)

inv = l4.rpc.invoice(1000, 'inv', 'inv')
paysecret = l4.rpc.decodepay(inv['bolt11'])['payment_secret']

# Separate routes for each part of the payment.
r134 = l1.rpc.getroute(l4.info['id'], 500, 1, exclude=[scid24 + '/0', scid24 + '/1'])['route']
r124 = l1.rpc.getroute(l4.info['id'], 500, 1, exclude=[scid34 + '/0', scid34 + '/1'])['route']

# These can happen in parallel.
l1.rpc.call('sendpay', [r134, inv['payment_hash'], None, 1000, inv['bolt11'], paysecret, 1])

# Can't mix non-parallel payment!
with pytest.raises(RpcError, match=r'Already have parallel payment in progress'):
l1.rpc.call('sendpay', {'route': r124,
'payment_hash': inv['payment_hash'],
'msatoshi': 1000,
'payment_secret': paysecret})

# It will not allow a parallel with different msatoshi!
with pytest.raises(RpcError, match=r'msatoshi was previously 1000msat, now 999msat'):
l1.rpc.call('sendpay', [r124, inv['payment_hash'], None, 999, inv['bolt11'], paysecret, 2])

# This will work fine.
l1.rpc.call('sendpay', [r124, inv['payment_hash'], None, 1000, inv['bolt11'], paysecret, 2])

# Any more would exceed total payment
with pytest.raises(RpcError, match=r'Already have 1000msat of 1000msat payments in progress'):
l1.rpc.call('sendpay', [r124, inv['payment_hash'], None, 1000, inv['bolt11'], paysecret, 3])

# But repeat is a NOOP.
l1.rpc.call('sendpay', [r124, inv['payment_hash'], None, 1000, inv['bolt11'], paysecret, 1])
l1.rpc.call('sendpay', [r134, inv['payment_hash'], None, 1000, inv['bolt11'], paysecret, 2])

# Now continue, payments will fail because receiver doesn't do MPP.
l2.rpc.dev_reenable_commit(l4.info['id'])
l3.rpc.dev_reenable_commit(l4.info['id'])

# FIXME: waitsendpay needs a 'partid' field.