diff --git a/common/lease_rates.c b/common/lease_rates.c index cf0b40244..cd70734a0 100644 --- a/common/lease_rates.c +++ b/common/lease_rates.c @@ -1,5 +1,6 @@ #include "config.h" #include +#include #include #include @@ -15,3 +16,17 @@ bool lease_rates_empty(struct lease_rates *rates) && rates->lease_fee_base_sat == 0 && rates->lease_fee_basis == 0; } + +bool lease_rates_set_chan_fee_base_msat(struct lease_rates *rates, + struct amount_msat amt) +{ + rates->channel_fee_max_base_msat = amt.millisatoshis; /* Raw: conversion */ + return rates->channel_fee_max_base_msat == amt.millisatoshis; /* Raw: comparsion */ +} + +bool lease_rates_set_lease_fee_sat(struct lease_rates *rates, + struct amount_sat amt) +{ + rates->lease_fee_base_sat = amt.satoshis; /* Raw: conversion */ + return rates->lease_fee_base_sat == amt.satoshis; /* Raw: comparsion */ +} diff --git a/common/lease_rates.h b/common/lease_rates.h index dd10e79fb..2c4a82002 100644 --- a/common/lease_rates.h +++ b/common/lease_rates.h @@ -3,7 +3,13 @@ #include "config.h" #include +struct amount_msat; +struct amount_sat; struct lease_rates; bool lease_rates_empty(struct lease_rates *rates); + +WARN_UNUSED_RESULT bool lease_rates_set_chan_fee_base_msat(struct lease_rates *rates, struct amount_msat amt); + +WARN_UNUSED_RESULT bool lease_rates_set_lease_fee_sat(struct lease_rates *rates, struct amount_sat amt); #endif /* LIGHTNING_COMMON_LEASE_RATES_H */ diff --git a/doc/PLUGINS.md b/doc/PLUGINS.md index 4f204cb88..feb715484 100644 --- a/doc/PLUGINS.md +++ b/doc/PLUGINS.md @@ -1100,7 +1100,10 @@ the v2 protocol, and it has passed basic sanity checks: "max_accepted_htlcs": 483, "channel_flags": 1 "locktime": 2453, - "channel_max_msat": "16777215000msat" + "channel_max_msat": "16777215000msat", + "requested_lease_msat": "100000000msat", + "lease_blockheight_start": 683990, + "node_blockheight": 683990 } } ``` @@ -1108,6 +1111,10 @@ the v2 protocol, and it has passed basic sanity checks: There may be additional fields, such as `shutdown_scriptpubkey`. You can see the definitions of these fields in [BOLT 2's description of the open_channel message][bolt2-open-channel]. +`requested_lease_msat`, `lease_blockheight_start`, and `node_blockheight` are +only present if the opening peer has requested a funding lease, +per `option_will_fund`. + The returned result must contain a `result` member which is either the string `reject` or `continue`. If `reject` and there's a member `error_message`, that member is sent to the peer diff --git a/lightningd/Makefile b/lightningd/Makefile index 1a4a1c4db..117ccf225 100644 --- a/lightningd/Makefile +++ b/lightningd/Makefile @@ -100,6 +100,7 @@ LIGHTNINGD_COMMON_OBJS := \ common/json_helpers.o \ common/json_stream.o \ common/json_tok.o \ + common/lease_rates.o \ common/memleak.o \ common/msg_queue.o \ common/node_id.o \ diff --git a/lightningd/dual_open_control.c b/lightningd/dual_open_control.c index 7413812ad..0eeac2b52 100644 --- a/lightningd/dual_open_control.c +++ b/lightningd/dual_open_control.c @@ -18,6 +18,7 @@ #include #include #include +#include #include #include #include @@ -244,10 +245,15 @@ struct openchannel2_payload { /* What's the maximum amount of funding * this channel can hold */ struct amount_sat channel_max; + /* If they've requested funds, this is their request */ + struct amount_sat requested_lease_amt; + u32 lease_blockheight_start; + u32 node_blockheight; struct amount_sat accepter_funding; struct wally_psbt *psbt; const u8 *our_shutdown_scriptpubkey; + struct lease_rates *rates; char *err_msg; }; @@ -283,6 +289,14 @@ static void openchannel2_hook_serialize(struct openchannel2_payload *payload, payload->shutdown_scriptpubkey); json_add_amount_sat_only(stream, "channel_max_msat", payload->channel_max); + if (!amount_sat_zero(payload->requested_lease_amt)) { + json_add_amount_sat_only(stream, "requested_lease_msat", + payload->requested_lease_amt); + json_add_num(stream, "lease_blockheight_start", + payload->lease_blockheight_start); + json_add_num(stream, "node_blockheight", + payload->node_blockheight); + } json_object_end(stream); } @@ -699,6 +713,7 @@ openchannel2_hook_deserialize(struct openchannel2_payload *payload, const jsmntok_t *toks) { const u8 *shutdown_script; + const char *err; struct subd *dualopend = payload->dualopend; /* If our daemon died, we're done */ @@ -756,6 +771,38 @@ openchannel2_hook_deserialize(struct openchannel2_payload *payload, else payload->our_shutdown_scriptpubkey = shutdown_script; + + struct amount_sat sats; + struct amount_msat msats; + payload->rates = tal(payload, struct lease_rates); + err = json_scan(payload, buffer, toks, + "{lease_fee_base_msat:%" + ",lease_fee_basis:%" + ",channel_fee_max_base_msat:%" + ",channel_fee_max_proportional_thousandths:%" + ",funding_weight:%}", + JSON_SCAN(json_to_sat, &sats), + JSON_SCAN(json_to_u16, + &payload->rates->lease_fee_basis), + JSON_SCAN(json_to_msat, &msats), + JSON_SCAN(json_to_u16, + &payload->rates->channel_fee_max_proportional_thousandths), + JSON_SCAN(json_to_u16, + &payload->rates->funding_weight)); + + /* It's possible they didn't send these back! */ + if (err) + payload->rates = tal_free(payload->rates); + + /* Convert to u32s */ + if (payload->rates && + !lease_rates_set_lease_fee_sat(payload->rates, sats)) + fatal("Plugin sent overflowing `lease_fee_base_msat`"); + + if (payload->rates && + !lease_rates_set_chan_fee_base_msat(payload->rates, msats)) + fatal("Plugin sent overflowing `channel_fee_max_base_msat`"); + /* Add a serial_id to everything that doesn't have one yet */ if (payload->psbt) psbt_add_serials(payload->psbt, TX_ACCEPTER); @@ -1699,6 +1746,7 @@ static void accepter_got_offer(struct subd *dualopend, * the plugin */ payload->feerate_our_min = feerate_min(dualopend->ld, NULL); payload->feerate_our_max = feerate_max(dualopend->ld, NULL); + payload->node_blockheight = get_block_height(dualopend->ld->topology); payload->channel_max = chainparams->max_funding; if (feature_negotiated(dualopend->ld->our_features, diff --git a/plugins/funder.c b/plugins/funder.c index 891154dc6..e2e5997cc 100644 --- a/plugins/funder.c +++ b/plugins/funder.c @@ -282,6 +282,9 @@ struct open_info { struct amount_sat channel_max; u64 funding_feerate_perkw; u32 locktime; + u32 lease_blockheight; + u32 node_blockheight; + struct amount_sat requested_lease; }; static struct command_result * @@ -322,6 +325,11 @@ psbt_funded(struct command *cmd, json_add_amount_msat_only(response, "our_funding_msat", our_funding_msat); + /* If we're accepting an lease request, *and* they've + * requested one, fill in our most recent infos */ + if (current_policy->rates && !amount_sat_zero(info->requested_lease)) + json_add_lease_rates(response, current_policy->rates); + return command_finished(cmd, response); } @@ -419,6 +427,7 @@ listfunds_success(struct command *cmd, info->their_funding, available_funds, info->channel_max, + info->requested_lease, &info->our_funding); plugin_log(cmd->plugin, LOG_DBG, "Policy %s returned funding amount of %s. %s", @@ -531,6 +540,21 @@ json_openchannel2_call(struct command *cmd, err, json_tok_full_len(params), json_tok_full(buf, params)); + err = json_scan(tmpctx, buf, params, + "{openchannel2:{" + "requested_lease_msat:%" + ",lease_blockheight_start:%" + ",node_blockheight:%}}", + JSON_SCAN(json_to_sat, &info->requested_lease), + JSON_SCAN(json_to_u32, &info->node_blockheight), + JSON_SCAN(json_to_u32, &info->lease_blockheight)); + + /* These aren't necessarily included */ + if (err) { + info->requested_lease = AMOUNT_SAT(0); + info->node_blockheight = 0; + info->lease_blockheight = 0; + } /* If there's no channel_max, it's actually infinity */ err = json_scan(tmpctx, buf, params, @@ -553,6 +577,40 @@ json_openchannel2_call(struct command *cmd, return command_hook_success(cmd); } + /* Check that their block height isn't too far behind */ + if (!amount_sat_zero(info->requested_lease)) { + u32 upper_bound, lower_bound; + + /* BOLT- #2: + * The receiving node: + * - MAY fail the negotiation if: ... + * - if the `option_will_fund` tlv is present and: + * - the `blockheight` is considered too far in the + * past or future + */ + /* We consider 24 hrs too far out */ + upper_bound = info->node_blockheight + 24 * 6; + lower_bound = info->node_blockheight - 24 * 6; + + /* Check overflow */ + if (upper_bound < info->node_blockheight) + upper_bound = -1; + if (lower_bound > info->node_blockheight) + lower_bound = 0; + + if (upper_bound < info->lease_blockheight + || lower_bound > info->lease_blockheight) { + + plugin_log(cmd->plugin, LOG_DBG, + "their blockheight %d is out of" + " our bounds (ours is %d)", + info->lease_blockheight, + info->node_blockheight); + + return command_hook_success(cmd); + } + } + /* Figure out what our funds are */ req = jsonrpc_request_start(cmd->plugin, cmd, "listfunds", diff --git a/plugins/funder_policy.c b/plugins/funder_policy.c index 428b4c42f..1aefdfef5 100644 --- a/plugins/funder_policy.c +++ b/plugins/funder_policy.c @@ -174,12 +174,17 @@ apply_fuzz(u32 fuzz_factor, struct amount_sat val) static struct amount_sat apply_policy(struct funder_policy *policy, struct amount_sat their_funding, + struct amount_sat requested_lease, struct amount_sat available_funds) { struct amount_sat our_funding; switch (policy->opt) { case MATCH: + /* For matches, we use requested funding, if availalbe */ + if (!amount_sat_zero(requested_lease)) + their_funding = requested_lease; + /* if this fails, it implies ludicrous funding offer, *and* * > 100% match. Just Say No, kids. */ if (!amount_sat_scale(&our_funding, their_funding, @@ -207,12 +212,23 @@ calculate_our_funding(struct funder_policy *policy, struct amount_sat their_funding, struct amount_sat available_funds, struct amount_sat channel_max, + struct amount_sat requested_lease, struct amount_sat *our_funding) { struct amount_sat avail_channel_space, net_available_funds; + /* Are we only funding lease requests ? */ + if (policy->leases_only && amount_sat_zero(requested_lease)) { + *our_funding = AMOUNT_SAT(0); + return tal_fmt(tmpctx, + "Skipping funding open; leases-only=true" + " and this open isn't asking for a lease"); + } + /* Are we skipping this one? */ - if (pseudorand(100) >= policy->fund_probability) { + if (pseudorand(100) >= policy->fund_probability + /* We don't skip lease requests */ + && amount_sat_zero(requested_lease)) { *our_funding = AMOUNT_SAT(0); return tal_fmt(tmpctx, "Skipping, failed fund_probability test"); @@ -269,7 +285,10 @@ calculate_our_funding(struct funder_policy *policy, } /* What's our amount, given our policy */ - *our_funding = apply_policy(policy, their_funding, available_funds); + *our_funding = apply_policy(policy, + their_funding, + requested_lease, + available_funds); /* Don't return an 'error' if we're already at 0 */ if (amount_sat_zero(*our_funding)) diff --git a/plugins/funder_policy.h b/plugins/funder_policy.h index dfb12e199..f554bdbe5 100644 --- a/plugins/funder_policy.h +++ b/plugins/funder_policy.h @@ -9,7 +9,7 @@ struct node_id; /* Policy Options */ enum funder_opt { - /* Use their_funding as the starting + /* Use their_funding/requested_amt as the starting * point for your contribution */ MATCH, @@ -96,6 +96,7 @@ calculate_our_funding(struct funder_policy *policy, struct amount_sat their_funding, struct amount_sat available_funds, struct amount_sat channel_max, + struct amount_sat lease_request, struct amount_sat *our_funding); /* Get the name of this policy option */ diff --git a/plugins/test/run-funder_policy.c b/plugins/test/run-funder_policy.c index a61ca864a..09ca6a3a9 100644 --- a/plugins/test/run-funder_policy.c +++ b/plugins/test/run-funder_policy.c @@ -33,6 +33,7 @@ struct test_case { struct amount_sat their_funds; struct amount_sat available_funds; struct amount_sat channel_max; + struct amount_sat lease_request; struct funder_policy policy; @@ -46,6 +47,7 @@ struct test_case cases[] = { .their_funds = AMOUNT_SAT(5000), .available_funds = AMOUNT_SAT(100000), .channel_max = AMOUNT_SAT(11000), + .lease_request = AMOUNT_SAT(0), .policy = { .opt = FIXED, .mod = 1111, @@ -56,6 +58,7 @@ struct test_case cases[] = { .fuzz_factor = 0, .reserve_tank = AMOUNT_SAT(0), .fund_probability = 100, + .leases_only = false, }, .exp_our_funds = AMOUNT_SAT(1111), @@ -66,6 +69,7 @@ struct test_case cases[] = { .their_funds = AMOUNT_SAT(5000), .available_funds = AMOUNT_SAT(500), .channel_max = AMOUNT_SAT(11000), + .lease_request = AMOUNT_SAT(0), .policy = { .opt = MATCH, .mod = 0, @@ -76,6 +80,7 @@ struct test_case cases[] = { .fuzz_factor = 0, .reserve_tank = AMOUNT_SAT(0), .fund_probability = 100, + .leases_only = false, }, .exp_our_funds = AMOUNT_SAT(0), @@ -86,6 +91,7 @@ struct test_case cases[] = { .their_funds = AMOUNT_SAT(5000), .available_funds = AMOUNT_SAT(6000), .channel_max = AMOUNT_SAT(11000), + .lease_request = AMOUNT_SAT(0), .policy = { .opt = MATCH, .mod = 100, @@ -96,6 +102,7 @@ struct test_case cases[] = { .fuzz_factor = 0, .reserve_tank = AMOUNT_SAT(0), .fund_probability = 100, + .leases_only = false, }, .exp_our_funds = AMOUNT_SAT(5000), @@ -106,6 +113,7 @@ struct test_case cases[] = { .their_funds = AMOUNT_SAT(2500), .available_funds = AMOUNT_SAT(6000), .channel_max = AMOUNT_SAT(11000), + .lease_request = AMOUNT_SAT(0), .policy = { .opt = MATCH, .mod = 200, @@ -116,6 +124,7 @@ struct test_case cases[] = { .fuzz_factor = 0, .reserve_tank = AMOUNT_SAT(0), .fund_probability = 100, + .leases_only = false, }, .exp_our_funds = AMOUNT_SAT(5000), @@ -126,6 +135,7 @@ struct test_case cases[] = { .their_funds = AMOUNT_SAT(2500), .available_funds = AMOUNT_SAT(5000), .channel_max = AMOUNT_SAT(11000), + .lease_request = AMOUNT_SAT(0), .policy = { .opt = AVAILABLE, .mod = 0, @@ -136,6 +146,7 @@ struct test_case cases[] = { .fuzz_factor = 0, .reserve_tank = AMOUNT_SAT(0), .fund_probability = 100, + .leases_only = false, }, .exp_our_funds = AMOUNT_SAT(0), @@ -146,6 +157,7 @@ struct test_case cases[] = { .their_funds = AMOUNT_SAT(2500), .available_funds = AMOUNT_SAT(3000), .channel_max = AMOUNT_SAT(11000), + .lease_request = AMOUNT_SAT(0), .policy = { .opt = AVAILABLE, .mod = 50, @@ -156,6 +168,7 @@ struct test_case cases[] = { .fuzz_factor = 0, .reserve_tank = AMOUNT_SAT(0), .fund_probability = 100, + .leases_only = false, }, .exp_our_funds = AMOUNT_SAT(1500), @@ -166,6 +179,7 @@ struct test_case cases[] = { .their_funds = AMOUNT_SAT(2500), .available_funds = AMOUNT_SAT(5000), .channel_max = AMOUNT_SAT(11000), + .lease_request = AMOUNT_SAT(0), .policy = { .opt = AVAILABLE, .mod = 100, @@ -176,6 +190,7 @@ struct test_case cases[] = { .fuzz_factor = 0, .reserve_tank = AMOUNT_SAT(0), .fund_probability = 100, + .leases_only = false, }, .exp_our_funds = AMOUNT_SAT(5000), @@ -198,6 +213,7 @@ struct test_case cases[] = { .fuzz_factor = 0, .reserve_tank = AMOUNT_SAT(0), .fund_probability = 100, + .leases_only = false, }, .exp_our_funds = AMOUNT_SAT(900), @@ -208,6 +224,7 @@ struct test_case cases[] = { .their_funds = AMOUNT_SAT(5000), .available_funds = AMOUNT_SAT(5000), .channel_max = AMOUNT_SAT(5500), + .lease_request = AMOUNT_SAT(0), .policy = { .opt = FIXED, .mod = 1002, @@ -218,6 +235,7 @@ struct test_case cases[] = { .fuzz_factor = 0, .reserve_tank = AMOUNT_SAT(0), .fund_probability = 100, + .leases_only = false, }, .exp_our_funds = AMOUNT_SAT(500), @@ -228,6 +246,7 @@ struct test_case cases[] = { .their_funds = AMOUNT_SAT(5000), .available_funds = AMOUNT_SAT(500), .channel_max = AMOUNT_SAT(10000), + .lease_request = AMOUNT_SAT(0), .policy = { .opt = FIXED, .mod = 1001, @@ -238,6 +257,7 @@ struct test_case cases[] = { .fuzz_factor = 0, .reserve_tank = AMOUNT_SAT(0), .fund_probability = 100, + .leases_only = false, }, .exp_our_funds = AMOUNT_SAT(500), @@ -248,6 +268,7 @@ struct test_case cases[] = { .their_funds = AMOUNT_SAT(5000), .available_funds = AMOUNT_SAT(1000), .channel_max = AMOUNT_SAT(10000), + .lease_request = AMOUNT_SAT(0), .policy = { .opt = FIXED, .mod = 999, @@ -258,6 +279,7 @@ struct test_case cases[] = { .fuzz_factor = 0, .reserve_tank = AMOUNT_SAT(0), .fund_probability = 100, + .leases_only = false, }, .exp_our_funds = AMOUNT_SAT(0), @@ -268,6 +290,7 @@ struct test_case cases[] = { .their_funds = AMOUNT_SAT(5001), .available_funds = AMOUNT_SAT(5000), .channel_max = AMOUNT_SAT(10000), + .lease_request = AMOUNT_SAT(0), .policy = { .opt = FIXED, .mod = 998, @@ -278,6 +301,7 @@ struct test_case cases[] = { .fuzz_factor = 0, .reserve_tank = AMOUNT_SAT(0), .fund_probability = 100, + .leases_only = false, }, .exp_our_funds = AMOUNT_SAT(0), @@ -288,6 +312,7 @@ struct test_case cases[] = { .their_funds = AMOUNT_SAT(5000), .available_funds = AMOUNT_SAT(1000), .channel_max = AMOUNT_SAT(10000), + .lease_request = AMOUNT_SAT(0), .policy = { .opt = FIXED, .mod = 997, @@ -298,6 +323,7 @@ struct test_case cases[] = { .fuzz_factor = 0, .reserve_tank = AMOUNT_SAT(100), .fund_probability = 100, + .leases_only = false, }, .exp_our_funds = AMOUNT_SAT(900), @@ -308,6 +334,7 @@ struct test_case cases[] = { .their_funds = AMOUNT_SAT(5000), .available_funds = AMOUNT_SAT(999), .channel_max = AMOUNT_SAT(10000), + .lease_request = AMOUNT_SAT(0), .policy = { .opt = FIXED, .mod = 996, @@ -318,6 +345,7 @@ struct test_case cases[] = { .fuzz_factor = 0, .reserve_tank = AMOUNT_SAT(1000), .fund_probability = 100, + .leases_only = false, }, .exp_our_funds = AMOUNT_SAT(0), @@ -328,6 +356,7 @@ struct test_case cases[] = { .their_funds = AMOUNT_SAT(5000), .available_funds = AMOUNT_SAT(5000), .channel_max = AMOUNT_SAT(5000), + .lease_request = AMOUNT_SAT(0), .policy = { .opt = FIXED, .mod = 995, @@ -338,6 +367,7 @@ struct test_case cases[] = { .fuzz_factor = 0, .reserve_tank = AMOUNT_SAT(1000), .fund_probability = 100, + .leases_only = false, }, .exp_our_funds = AMOUNT_SAT(0), @@ -348,6 +378,7 @@ struct test_case cases[] = { .their_funds = AMOUNT_SAT(5000), .available_funds = AMOUNT_SAT(5000), .channel_max = AMOUNT_SAT(11000), + .lease_request = AMOUNT_SAT(0), .policy = { .opt = FIXED, .mod = 988, @@ -358,6 +389,51 @@ struct test_case cases[] = { .fuzz_factor = 0, .reserve_tank = AMOUNT_SAT(0), .fund_probability = 100, + .leases_only = false, + }, + + .exp_our_funds = AMOUNT_SAT(0), + .expect_err = true, + }, + /* By default, use lease request as ceiling */ + { + .their_funds = AMOUNT_SAT(5000), + .available_funds = AMOUNT_SAT(100000), + .channel_max = AMOUNT_SAT(11000), + .lease_request = AMOUNT_SAT(980), + .policy = { + .opt = MATCH, + .mod = 100, + .min_their_funding = AMOUNT_SAT(0), + .max_their_funding = AMOUNT_SAT(10000), + .per_channel_max = AMOUNT_SAT(100000), + .per_channel_min = AMOUNT_SAT(0), + .fuzz_factor = 0, + .reserve_tank = AMOUNT_SAT(0), + .fund_probability = 100, + .leases_only = false, + }, + + .exp_our_funds = AMOUNT_SAT(980), + .expect_err = false, + }, + /* Only fund lease requests */ + { + .their_funds = AMOUNT_SAT(5000), + .available_funds = AMOUNT_SAT(100000), + .channel_max = AMOUNT_SAT(11000), + .lease_request = AMOUNT_SAT(0), + .policy = { + .opt = FIXED, + .mod = 985, + .min_their_funding = AMOUNT_SAT(0), + .max_their_funding = AMOUNT_SAT(10000), + .per_channel_max = AMOUNT_SAT(100000), + .per_channel_min = AMOUNT_SAT(0), + .fuzz_factor = 0, + .reserve_tank = AMOUNT_SAT(0), + .fund_probability = 100, + .leases_only = true, }, .exp_our_funds = AMOUNT_SAT(0), @@ -380,6 +456,7 @@ static void check_fuzzing(struct test_case fuzzcase) fuzzcase.their_funds, fuzzcase.available_funds, fuzzcase.channel_max, + fuzzcase.lease_request, &our_funds); if (amount_sat_greater(our_funds, fuzz_max)) fuzz_max = our_funds; @@ -412,6 +489,7 @@ int main(int argc, const char *argv[]) AMOUNT_SAT(50000), AMOUNT_SAT(50000), AMOUNT_SAT(100000), + AMOUNT_SAT(0), &our_funds); assert(amount_sat_eq(empty, our_funds)); assert(!err); @@ -421,6 +499,7 @@ int main(int argc, const char *argv[]) cases[i].their_funds, cases[i].available_funds, cases[i].channel_max, + cases[i].lease_request, &our_funds); if (!amount_sat_eq(cases[i].exp_our_funds, our_funds)) { fprintf(stderr, "FAIL policy: %s. expected %s, got %s\n", @@ -454,6 +533,7 @@ int main(int argc, const char *argv[]) flipcase.their_funds, flipcase.available_funds, flipcase.channel_max, + flipcase.lease_request, &our_funds); if (!amount_sat_eq(our_funds, AMOUNT_SAT(0))) flipcount++;