From 5065f41b4362c6ca70dbaa9fb6152b787391c0c0 Mon Sep 17 00:00:00 2001 From: niftynei Date: Mon, 19 Apr 2021 14:26:23 -0500 Subject: [PATCH 01/20] libplugin: add no-op command complete function When an RPC originates from a plugin, and there's no associated command, it's useful to be able to signal that you're finished. --- plugins/libplugin.c | 4 ++++ plugins/libplugin.h | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/plugins/libplugin.c b/plugins/libplugin.c index c318f5f2acae..7a0e8fe38449 100644 --- a/plugins/libplugin.c +++ b/plugins/libplugin.c @@ -106,6 +106,10 @@ struct command_result *command_param_failed(void) return &complete; } +struct command_result *command_done(void) +{ + return &complete; +} static void ld_send(struct plugin *plugin, struct json_stream *stream) { diff --git a/plugins/libplugin.h b/plugins/libplugin.h index fcda5baa7ada..ee619739968a 100644 --- a/plugins/libplugin.h +++ b/plugins/libplugin.h @@ -219,6 +219,10 @@ struct command_result *forward_result(struct command *cmd, * send_req() path. */ struct command_result *timer_complete(struct plugin *p); +/* Signals that we've completed a command. Useful for when + * there's no `cmd` present */ +struct command_result *command_done(void); + /* Access timer infrastructure to add a timer. * * Freeing this releases the timer, otherwise it's freed after @cb From 2001b1fa8bef326083e9248ed44a2e81f552bcf8 Mon Sep 17 00:00:00 2001 From: niftynei Date: Tue, 20 Apr 2021 15:44:45 -0500 Subject: [PATCH 02/20] tests: remove EXPERIMENTAL_FEATURE flag from openchannel2 hooks These are now included in all builds --- tests/plugins/openchannel_hook_accept.py | 15 +++++---------- tests/plugins/openchannel_hook_accepter.py | 13 ++++--------- tests/plugins/openchannel_hook_reject.py | 15 +++++---------- tests/plugins/reject_odd_funding_amounts.py | 17 ++++++----------- 4 files changed, 20 insertions(+), 40 deletions(-) diff --git a/tests/plugins/openchannel_hook_accept.py b/tests/plugins/openchannel_hook_accept.py index 0022179eb718..9a8d1b87779f 100755 --- a/tests/plugins/openchannel_hook_accept.py +++ b/tests/plugins/openchannel_hook_accept.py @@ -5,14 +5,10 @@ """ from pyln.client import Plugin -from pyln.testing.utils import env plugin = Plugin() -EXPERIMENTAL_FEATURES = env("EXPERIMENTAL_FEATURES", "0") == "1" - - @plugin.hook('openchannel') def on_openchannel(openchannel, plugin, **kwargs): msg = "accept on principle" @@ -20,12 +16,11 @@ def on_openchannel(openchannel, plugin, **kwargs): return {'result': 'continue'} -if EXPERIMENTAL_FEATURES: - @plugin.hook('openchannel2') - def on_openchannel2(openchannel2, plugin, **kwargs): - msg = "accept on principle" - plugin.log(msg) - return {'result': 'continue'} +@plugin.hook('openchannel2') +def on_openchannel2(openchannel2, plugin, **kwargs): + msg = "accept on principle" + plugin.log(msg) + return {'result': 'continue'} plugin.run() diff --git a/tests/plugins/openchannel_hook_accepter.py b/tests/plugins/openchannel_hook_accepter.py index 9c0bd884efd6..bf63cf146f15 100755 --- a/tests/plugins/openchannel_hook_accepter.py +++ b/tests/plugins/openchannel_hook_accepter.py @@ -12,14 +12,10 @@ """ from pyln.client import Plugin, Millisatoshi -from pyln.testing.utils import env plugin = Plugin() -EXPERIMENTAL_FEATURES = env("EXPERIMENTAL_FEATURES", "0") == "1" - - def run_openchannel(funding_sats_str, plugin): # Convert from string to satoshis funding_sats = Millisatoshi(funding_sats_str).to_satoshi() @@ -59,11 +55,10 @@ def on_openchannel(openchannel, plugin, **kwargs): return run_openchannel(openchannel['funding_satoshis'], plugin) -if EXPERIMENTAL_FEATURES: - @plugin.hook('openchannel2') - def on_openchannel2(openchannel2, plugin, **kwargs): - """ Support for v2 channel opens """ - return run_openchannel(openchannel2['their_funding'], plugin) +@plugin.hook('openchannel2') +def on_openchannel2(openchannel2, plugin, **kwargs): + """ Support for v2 channel opens """ + return run_openchannel(openchannel2['their_funding'], plugin) plugin.run() diff --git a/tests/plugins/openchannel_hook_reject.py b/tests/plugins/openchannel_hook_reject.py index 93fe15e40ad8..e9104cbab2f4 100755 --- a/tests/plugins/openchannel_hook_reject.py +++ b/tests/plugins/openchannel_hook_reject.py @@ -6,10 +6,6 @@ """ from pyln.client import Plugin -from pyln.testing.utils import env - -EXPERIMENTAL_FEATURES = env("EXPERIMENTAL_FEATURES", "0") == "1" - plugin = Plugin() @@ -21,12 +17,11 @@ def on_openchannel(openchannel, plugin, **kwargs): return {'result': 'reject', 'error_message': msg} -if EXPERIMENTAL_FEATURES: - @plugin.hook('openchannel2') - def on_openchannel2(openchannel2, plugin, **kwargs): - msg = "reject on principle" - plugin.log(msg) - return {'result': 'reject', 'error_message': msg} +@plugin.hook('openchannel2') +def on_openchannel2(openchannel2, plugin, **kwargs): + msg = "reject on principle" + plugin.log(msg) + return {'result': 'reject', 'error_message': msg} plugin.run() diff --git a/tests/plugins/reject_odd_funding_amounts.py b/tests/plugins/reject_odd_funding_amounts.py index 3e72cb05959b..adf0efc007bd 100755 --- a/tests/plugins/reject_odd_funding_amounts.py +++ b/tests/plugins/reject_odd_funding_amounts.py @@ -5,14 +5,10 @@ """ from pyln.client import Plugin, Millisatoshi -from pyln.testing.utils import env plugin = Plugin() -EXPERIMENTAL_FEATURES = env("EXPERIMENTAL_FEATURES", "0") == "1" - - def run_check(funding_amt_str): if Millisatoshi(funding_amt_str).to_satoshi() % 2 == 1: return {'result': 'reject', 'error_message': "I don't like odd amounts"} @@ -28,14 +24,13 @@ def on_openchannel(openchannel, plugin, **kwargs): return run_check(openchannel['funding_satoshis']) -if EXPERIMENTAL_FEATURES: - @plugin.hook('openchannel2') - def on_openchannel2(openchannel2, plugin, **kwargs): - print("{} VARS".format(len(openchannel2.keys()))) - for k in sorted(openchannel2.keys()): - print("{}={}".format(k, openchannel2[k])) +@plugin.hook('openchannel2') +def on_openchannel2(openchannel2, plugin, **kwargs): + print("{} VARS".format(len(openchannel2.keys()))) + for k in sorted(openchannel2.keys()): + print("{}={}".format(k, openchannel2[k])) - return run_check(openchannel2['their_funding']) + return run_check(openchannel2['their_funding']) plugin.run() From 536d03256c19c64b6e780a5783fea9425833ebc1 Mon Sep 17 00:00:00 2001 From: niftynei Date: Tue, 20 Apr 2021 15:45:10 -0500 Subject: [PATCH 03/20] plugin-tests: cleanup outdated openchannel2 hook fields We removed/changed the fields on the openchannel2 hook but never updated this test (which doesn't run with the CI for #reasons) --- tests/test_plugin.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/tests/test_plugin.py b/tests/test_plugin.py index 9699de37c8db..e4e944b6845c 100644 --- a/tests/test_plugin.py +++ b/tests/test_plugin.py @@ -599,12 +599,11 @@ def test_openchannel_hook(node_factory, bitcoind): if l2.config('experimental-dual-fund'): # openchannel2 var checks expected.update({ - 'commitment_feerate_per_kw': '750', + 'channel_id': '.*', + 'commitment_feerate_per_kw': '7500', + 'funding_feerate_per_kw': '7500', 'feerate_our_max': '150000', 'feerate_our_min': '1875', - 'funding_feerate_best': '7500', - 'funding_feerate_max': '150000', - 'funding_feerate_min': '1875', 'locktime': '.*', 'their_funding': '100000000msat', }) From a7ef21e69ca33776da84bc0ed339b4798308b170 Mon Sep 17 00:00:00 2001 From: niftynei Date: Tue, 20 Apr 2021 16:42:52 -0500 Subject: [PATCH 04/20] dualopend: don't use final channel_id for accepter_start2 The other side doesn't know it until *after* it parses this msg. We add a quick hack to still allow old nodes to work (for now!). This also fixes a bug (spotted by @niftynei) where any errors we sent before accepter_start2 would have the new (unknowable!) channel_id rather than the temp one. Authored-by: Rusty Russell --- openingd/dualopend.c | 55 ++++++++++++++++++++++++++------------------ 1 file changed, 33 insertions(+), 22 deletions(-) diff --git a/openingd/dualopend.c b/openingd/dualopend.c index ad8d5420e682..1030316d9fa8 100644 --- a/openingd/dualopend.c +++ b/openingd/dualopend.c @@ -1896,8 +1896,8 @@ static void accepter_start(struct state *state, const u8 *oc2_msg) { struct bitcoin_blkid chain_hash; struct tlv_opening_tlvs *open_tlv; + struct channel_id cid, full_cid; char *err_reason; - struct channel_id tmp_chan_id; u8 *msg; struct amount_sat total; enum dualopend_wire msg_type; @@ -1907,7 +1907,7 @@ static void accepter_start(struct state *state, const u8 *oc2_msg) open_tlv = tlv_opening_tlvs_new(tmpctx); if (!fromwire_open_channel2(oc2_msg, &chain_hash, - &state->channel_id, /* Temporary! */ + &cid, &tx_state->feerate_per_kw_funding, &state->feerate_per_kw_commitment, &tx_state->opener_funding, @@ -1939,21 +1939,15 @@ static void accepter_start(struct state *state, const u8 *oc2_msg) * `open_channel2`), a temporary `channel_id` should be found * by using a zeroed out basepoint for the unknown peer. */ - derive_tmp_channel_id(&tmp_chan_id, + derive_tmp_channel_id(&state->channel_id, /* Temporary! */ &state->their_points.revocation); - if (!channel_id_eq(&state->channel_id, &tmp_chan_id)) + if (!channel_id_eq(&state->channel_id, &cid)) negotiation_failed(state, "open_channel2 channel_id incorrect." " Expected %s, received %s", type_to_string(tmpctx, struct channel_id, - &tmp_chan_id), + &state->channel_id), type_to_string(tmpctx, struct channel_id, - &state->channel_id)); - - /* Everything's ok. Let's figure out the actual channel_id now */ - derive_channel_id_v2(&state->channel_id, - &state->our_points.revocation, - &state->their_points.revocation); - + &cid)); /* Save feerate on the state as well */ state->feerate_per_kw_funding = tx_state->feerate_per_kw_funding; @@ -1990,8 +1984,12 @@ static void accepter_start(struct state *state, const u8 *oc2_msg) return; } + /* We send the 'real' channel id over to lightningd */ + derive_channel_id_v2(&full_cid, + &state->our_points.revocation, + &state->their_points.revocation); msg = towire_dualopend_got_offer(NULL, - &state->channel_id, + &full_cid, tx_state->opener_funding, tx_state->remoteconf.dust_limit, tx_state->remoteconf.max_htlc_value_in_flight, @@ -2010,7 +2008,6 @@ static void accepter_start(struct state *state, const u8 *oc2_msg) if ((msg_type = fromwire_peektype(msg)) == WIRE_DUALOPEND_FAIL) { if (!fromwire_dualopend_fail(msg, msg, &err_reason)) master_badmsg(msg_type, msg); - open_err_warn(state, "%s", err_reason); return; } @@ -2105,6 +2102,11 @@ static void accepter_start(struct state *state, const u8 *oc2_msg) &state->first_per_commitment_point[LOCAL], a_tlv); + /* Everything's ok. Let's figure out the actual channel_id now */ + derive_channel_id_v2(&state->channel_id, + &state->our_points.revocation, + &state->their_points.revocation); + sync_crypto_write(state->pps, msg); peer_billboard(false, "channel open: accept sent, waiting for reply"); @@ -2479,6 +2481,23 @@ static void opener_start(struct state *state, u8 *msg) open_err_fatal(state, "Parsing accept_channel2 %s", tal_hex(msg, msg)); + if (!channel_id_eq(&cid, &state->channel_id)) { + struct channel_id future_chan_id; + /* FIXME: v0.10.0 actually replied with the complete channel id here, + * so we need to accept it for now */ + derive_channel_id_v2(&future_chan_id, + &state->our_points.revocation, + &state->their_points.revocation); + if (!channel_id_eq(&cid, &future_chan_id)) { + peer_failed_err(state->pps, &cid, + "accept_channel2 ids don't match: " + "expected %s, got %s", + type_to_string(msg, struct channel_id, + &state->channel_id), + type_to_string(msg, struct channel_id, &cid)); + } + } + if (a_tlv->option_upfront_shutdown_script) { state->upfront_shutdown_script[REMOTE] = tal_steal(state, @@ -2492,14 +2511,6 @@ static void opener_start(struct state *state, u8 *msg) &state->our_points.revocation, &state->their_points.revocation); - if (!channel_id_eq(&cid, &state->channel_id)) - peer_failed_err(state->pps, &cid, - "accept_channel2 ids don't match: " - "expected %s, got %s", - type_to_string(msg, struct channel_id, - &state->channel_id), - type_to_string(msg, struct channel_id, &cid)); - /* Check that total funding doesn't overflow */ if (!amount_sat_add(&total, tx_state->opener_funding, tx_state->accepter_funding)) From 5d47c675acc08f5b6e0c5563c0bbe9bc220b1b7b Mon Sep 17 00:00:00 2001 From: niftynei Date: Tue, 20 Apr 2021 16:56:07 -0500 Subject: [PATCH 05/20] openchannel2: add channel_max_msat to openchannel2 hook payload Changelog-Added: Plugins: add a `channel_max_msat` value to the `openchannel2` hook. Tells you the total max funding this channel is allowed to have. --- doc/PLUGINS.md | 1 + lightningd/dual_open_control.c | 11 +++++++++++ tests/test_plugin.py | 1 + 3 files changed, 13 insertions(+) diff --git a/doc/PLUGINS.md b/doc/PLUGINS.md index 6f81c44105ce..db4515a21749 100644 --- a/doc/PLUGINS.md +++ b/doc/PLUGINS.md @@ -1034,6 +1034,7 @@ the v2 protocol, and it has passed basic sanity checks: "max_accepted_htlcs": 483, "channel_flags": 1 "locktime": 2453, + "channel_max_msat": "16777215000msat" } } ``` diff --git a/lightningd/dual_open_control.c b/lightningd/dual_open_control.c index 44a5508819c1..f6868881bfee 100644 --- a/lightningd/dual_open_control.c +++ b/lightningd/dual_open_control.c @@ -241,6 +241,9 @@ struct openchannel2_payload { u8 channel_flags; u32 locktime; u8 *shutdown_scriptpubkey; + /* What's the maximum amount of funding + * this channel can hold */ + struct amount_sat channel_max; struct amount_sat accepter_funding; struct wally_psbt *psbt; @@ -278,6 +281,8 @@ openchannel2_hook_serialize(struct openchannel2_payload *payload, if (tal_bytelen(payload->shutdown_scriptpubkey) != 0) json_add_hex_talarr(stream, "shutdown_scriptpubkey", payload->shutdown_scriptpubkey); + json_add_amount_sat_only(stream, "channel_max_msat", + payload->channel_max); json_object_end(stream); } @@ -1695,6 +1700,12 @@ static void accepter_got_offer(struct subd *dualopend, payload->feerate_our_min = feerate_min(dualopend->ld, NULL); payload->feerate_our_max = feerate_max(dualopend->ld, NULL); + payload->channel_max = chainparams->max_funding; + if (feature_negotiated(dualopend->ld->our_features, + channel->peer->their_features, + OPT_LARGE_CHANNELS)) + payload->channel_max = AMOUNT_SAT(UINT64_MAX); + tal_add_destructor2(dualopend, openchannel2_remove_dualopend, payload); plugin_hook_call_openchannel2(dualopend->ld, payload); } diff --git a/tests/test_plugin.py b/tests/test_plugin.py index e4e944b6845c..6ea840e9d499 100644 --- a/tests/test_plugin.py +++ b/tests/test_plugin.py @@ -606,6 +606,7 @@ def test_openchannel_hook(node_factory, bitcoind): 'feerate_our_min': '1875', 'locktime': '.*', 'their_funding': '100000000msat', + 'channel_max_msat': '16777215000msat', }) else: expected.update({ From dea5f2d1a02f947553a5ec50ad409b1a68e1c940 Mon Sep 17 00:00:00 2001 From: niftynei Date: Wed, 21 Apr 2021 15:45:05 -0500 Subject: [PATCH 06/20] psbt-open: method to quickly check if has our input For dual-funding's accepter plugin, we only want to send psbts that need to be signed to `signpsbt`; this lets us quickly check if they're "signable" --- common/psbt_open.c | 10 ++++++++++ common/psbt_open.h | 4 ++++ 2 files changed, 14 insertions(+) diff --git a/common/psbt_open.c b/common/psbt_open.c index c8b83cce95af..eb654a02f561 100644 --- a/common/psbt_open.c +++ b/common/psbt_open.c @@ -481,3 +481,13 @@ bool psbt_input_is_ours(const struct wally_psbt_input *input) PSBT_TYPE_INPUT_MARKER, &unused); return !(!result); } + +bool psbt_has_our_input(const struct wally_psbt *psbt) +{ + for (size_t i = 0; i < psbt->num_inputs; i++) { + if (psbt_input_is_ours(&psbt->inputs[i])) + return true; + } + + return false; +} diff --git a/common/psbt_open.h b/common/psbt_open.h index f6bc07eb3a9a..bb7a5e57e1fb 100644 --- a/common/psbt_open.h +++ b/common/psbt_open.h @@ -174,4 +174,8 @@ void psbt_input_mark_ours(const tal_t *ctx, */ bool psbt_input_is_ours(const struct wally_psbt_input *input); +/* psbt_has_our_input - Returns true if this psbt contains + * any input that is ours + */ +bool psbt_has_our_input(const struct wally_psbt *psbt); #endif /* LIGHTNING_COMMON_PSBT_OPEN_H */ From e489b661080b54b1c0b3ffbf05c83ceecdde9c0e Mon Sep 17 00:00:00 2001 From: niftynei Date: Wed, 21 Apr 2021 15:46:32 -0500 Subject: [PATCH 07/20] openchannel2/rbf hooks: reject if response malformed You gotta send over an amount if you send a psbt! --- lightningd/dual_open_control.c | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/lightningd/dual_open_control.c b/lightningd/dual_open_control.c index f6868881bfee..6663a1a311b5 100644 --- a/lightningd/dual_open_control.c +++ b/lightningd/dual_open_control.c @@ -622,6 +622,10 @@ rbf_channel_hook_deserialize(struct rbf_channel_payload *payload, "our_funding_msat", &payload->our_funding)) fatal("Plugin failed to supply our_funding_msat field"); + if (payload->psbt + && amount_sat_eq(payload->our_funding, AMOUNT_SAT(0))) + fatal("Plugin failed to supply our_funding_msat field"); + if (!payload->psbt && !amount_sat_eq(payload->our_funding, AMOUNT_SAT(0))) { @@ -782,8 +786,12 @@ openchannel2_hook_deserialize(struct openchannel2_payload *payload, &payload->accepter_funding)) fatal("Plugin failed to supply our_funding_msat field"); - if (!payload->psbt && - !amount_sat_eq(payload->accepter_funding, AMOUNT_SAT(0))) { + if (payload->psbt + && amount_sat_eq(payload->accepter_funding, AMOUNT_SAT(0))) + fatal("Plugin failed to supply our_funding_msat field"); + + if (!payload->psbt + && !amount_sat_eq(payload->accepter_funding, AMOUNT_SAT(0))) { /* Gotta give a PSBT if you set the accepter_funding amount */ /* Let dualopend know we've failed */ payload->err_msg = "Client error. Unable to continue"; From c88f30760d1deb7d6ce2ba40b97fafc9855cb995 Mon Sep 17 00:00:00 2001 From: niftynei Date: Wed, 21 Apr 2021 15:47:19 -0500 Subject: [PATCH 08/20] dualfund: set the locktime for the user-provided PSBT This is set by the peer and is non-negotiable. We're not even going to check if you got it right. You were told about it via `openchannel2`. It is what it is. --- openingd/dualopend.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openingd/dualopend.c b/openingd/dualopend.c index 1030316d9fa8..bd7fc2e8015a 100644 --- a/openingd/dualopend.c +++ b/openingd/dualopend.c @@ -2021,6 +2021,9 @@ static void accepter_start(struct state *state, const u8 *oc2_msg) if (!tx_state->psbt) tx_state->psbt = create_psbt(tx_state, 0, 0, tx_state->tx_locktime); + else + /* Locktimes must match! */ + tx_state->psbt->tx->locktime = tx_state->tx_locktime; /* Check that total funding doesn't overflow */ if (!amount_sat_add(&total, tx_state->opener_funding, From 890ac997fe6f78dbdf906a0d60b488154b78fe9e Mon Sep 17 00:00:00 2001 From: niftynei Date: Wed, 21 Apr 2021 15:54:06 -0500 Subject: [PATCH 09/20] openchannel: add missing string args to format string Oops --- plugins/spender/openchannel.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/spender/openchannel.c b/plugins/spender/openchannel.c index bda0da3e49ee..43deb9beed10 100644 --- a/plugins/spender/openchannel.c +++ b/plugins/spender/openchannel.c @@ -543,7 +543,7 @@ static void json_peer_sigs(struct command *cmd, JSON_SCAN_TAL(cmd, json_to_psbt, &psbt)); if (err) plugin_err(cmd->plugin, - "`openchannel_peer_sigs` did not scan: %s", + "`openchannel_peer_sigs` did not scan: %s. %*.s", err, json_tok_full_len(params), json_tok_full(buf, params)); From d8534e725b28a1ee8a06d98f1ad616de8f316a06 Mon Sep 17 00:00:00 2001 From: niftynei Date: Fri, 23 Apr 2021 14:00:40 -0500 Subject: [PATCH 10/20] amount: `amount_sat_scale` method For scaling/multiplying sats --- common/amount.c | 18 ++++++++++++++++-- common/amount.h | 3 +++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/common/amount.c b/common/amount.c index aefec1ca138d..3736d2913216 100644 --- a/common/amount.c +++ b/common/amount.c @@ -325,10 +325,10 @@ WARN_UNUSED_RESULT bool amount_msat_add_sat(struct amount_msat *val, } WARN_UNUSED_RESULT bool amount_msat_scale(struct amount_msat *val, - struct amount_msat sat, + struct amount_msat msat, double scale) { - double scaled = sat.millisatoshis * scale; + double scaled = msat.millisatoshis * scale; /* If mantissa is < 64 bits, a naive "if (scaled > * UINT64_MAX)" doesn't work. Stick to powers of 2. */ @@ -338,6 +338,20 @@ WARN_UNUSED_RESULT bool amount_msat_scale(struct amount_msat *val, return true; } +WARN_UNUSED_RESULT bool amount_sat_scale(struct amount_sat *val, + struct amount_sat sat, + double scale) +{ + double scaled = sat.satoshis * scale; + + /* If mantissa is < 64 bits, a naive "if (scaled > + * UINT64_MAX)" doesn't work. Stick to powers of 2. */ + if (scaled >= (double)((u64)1 << 63) * 2) + return false; + val->satoshis = scaled; + return true; +} + bool amount_sat_eq(struct amount_sat a, struct amount_sat b) { return a.satoshis == b.satoshis; diff --git a/common/amount.h b/common/amount.h index 3b1a84ea522d..58df61c5c32b 100644 --- a/common/amount.h +++ b/common/amount.h @@ -83,6 +83,9 @@ WARN_UNUSED_RESULT bool amount_sat_sub_msat(struct amount_msat *val, WARN_UNUSED_RESULT bool amount_msat_scale(struct amount_msat *val, struct amount_msat msat, double scale); +WARN_UNUSED_RESULT bool amount_sat_scale(struct amount_sat *val, + struct amount_sat sat, + double scale); struct amount_msat amount_msat_div(struct amount_msat msat, u64 div); struct amount_sat amount_sat_div(struct amount_sat sat, u64 div); From e4eef8e79969a51679e191aeeb839a04ee8e2772 Mon Sep 17 00:00:00 2001 From: niftynei Date: Thu, 22 Apr 2021 17:51:22 -0500 Subject: [PATCH 11/20] funder: add a plugin, `funder`. policies for dual-funding Behold! An immaculately concepted plugin for configuring your node to do amazing things* *fund channel open requests Changelog-Added: Plugins: Add `funder` plugin, which allows you to setup a policy for funding v2 channel open requests. Requres --experimental-dual-fund option --- plugins/.gitignore | 1 + plugins/Makefile | 13 + plugins/funder.c | 629 ++++++++++++++++++++++++++++++++++++++++ plugins/funder_policy.c | 204 +++++++++++++ plugins/funder_policy.h | 92 ++++++ tests/test_opening.py | 6 +- 6 files changed, 943 insertions(+), 2 deletions(-) create mode 100644 plugins/funder.c create mode 100644 plugins/funder_policy.c create mode 100644 plugins/funder_policy.h diff --git a/plugins/.gitignore b/plugins/.gitignore index 3917b5a70bd0..487230e970cc 100644 --- a/plugins/.gitignore +++ b/plugins/.gitignore @@ -1,5 +1,6 @@ autoclean bcli +funder pay spenderp multifundchannel diff --git a/plugins/Makefile b/plugins/Makefile index 4fd3422e0e74..713dff19cfe7 100644 --- a/plugins/Makefile +++ b/plugins/Makefile @@ -43,10 +43,18 @@ PLUGIN_SPENDER_HEADER := \ plugins/spender/openchannel.h PLUGIN_SPENDER_OBJS := $(PLUGIN_SPENDER_SRC:.c=.o) +PLUGIN_FUNDER_SRC := \ + plugins/funder.c \ + plugins/funder_policy.c +PLUGIN_FUNDER_HEADER := \ + plugins/funder_policy.h +PLUGIN_FUNDER_OBJS := $(PLUGIN_FUNDER_SRC:.c=.o) + PLUGIN_ALL_SRC := \ $(PLUGIN_AUTOCLEAN_SRC) \ $(PLUGIN_BCLI_SRC) \ $(PLUGIN_FETCHINVOICE_SRC) \ + $(PLUGIN_FUNDER_SRC) \ $(PLUGIN_KEYSEND_SRC) \ $(PLUGIN_TXPREPARE_SRC) \ $(PLUGIN_LIB_SRC) \ @@ -57,6 +65,7 @@ PLUGIN_ALL_SRC := \ PLUGIN_ALL_HEADER := \ $(PLUGIN_LIB_HEADER) \ + $(PLUGIN_FUNDER_HEADER) \ $(PLUGIN_PAY_LIB_HEADER) \ $(PLUGIN_OFFERS_HEADER) \ $(PLUGIN_SPENDER_HEADER) @@ -66,6 +75,7 @@ PLUGINS := \ plugins/autoclean \ plugins/bcli \ plugins/fetchinvoice \ + plugins/funder \ plugins/keysend \ plugins/offers \ plugins/pay \ @@ -106,6 +116,7 @@ PLUGIN_COMMON_OBJS := \ common/memleak.o \ common/node_id.o \ common/param.o \ + common/psbt_open.o \ common/pseudorand.o \ common/random_select.o \ common/setup.o \ @@ -137,6 +148,8 @@ plugins/offers: bitcoin/chainparams.o $(PLUGIN_OFFERS_OBJS) $(PLUGIN_LIB_OBJS) $ plugins/fetchinvoice: bitcoin/chainparams.o $(PLUGIN_FETCHINVOICE_OBJS) $(PLUGIN_LIB_OBJS) $(PLUGIN_COMMON_OBJS) common/bolt12.o common/bolt12_merkle.o common/iso4217.o $(WIRE_OBJS) bitcoin/block.o common/channel_id.o bitcoin/preimage.o $(JSMN_OBJS) $(CCAN_OBJS) common/gossmap.o common/fp16.o common/dijkstra.o common/route.o common/blindedpath.o common/hmac.o common/blinding.o +plugins/funder: bitcoin/chainparams.o bitcoin/psbt.o common/psbt_open.o $(PLUGIN_FUNDER_OBJS) $(PLUGIN_LIB_OBJS) $(PLUGIN_COMMON_OBJS) $(JSMN_OBJS) $(CCAN_OBJS) + $(PLUGIN_ALL_OBJS): $(PLUGIN_LIB_HEADER) # Generated from PLUGINS definition in plugins/Makefile diff --git a/plugins/funder.c b/plugins/funder.c new file mode 100644 index 000000000000..0aaf899e3bb6 --- /dev/null +++ b/plugins/funder.c @@ -0,0 +1,629 @@ +/* This is a plugin which allows you to specify + * your policy for accepting/dual-funding incoming + * v2 channel-open requests. + * + * "They say marriages are made in Heaven. + * But so is funder and lightning." + * - Clint Eastwood + */ +#include "config.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* In-progress channel opens */ +static struct list_head pending_opens; + +/* Current set policy */ +static struct funder_policy current_policy; + +struct pending_open { + struct list_node list; + struct plugin *p; + + struct node_id peer_id; + struct channel_id channel_id; + + const struct wally_psbt *psbt; +}; + +static struct pending_open * +find_channel_pending_open(const struct channel_id *cid) +{ + struct pending_open *open; + list_for_each(&pending_opens, open, list) { + if (channel_id_eq(&open->channel_id, cid)) + return open; + } + return NULL; +} + +static struct pending_open * +new_channel_open(const tal_t *ctx, + struct plugin *p, + const struct node_id id, + const struct channel_id cid, + const struct wally_psbt *psbt STEALS) +{ + struct pending_open *open; + + /* Make sure we haven't gotten this yet */ + assert(!find_channel_pending_open(&cid)); + + open = tal(ctx, struct pending_open); + open->p = p; + open->peer_id = id; + open->channel_id = cid; + open->psbt = tal_steal(open, psbt); + + list_add_tail(&pending_opens, &open->list); + + return open; +} + +static struct command_result * +unreserve_done(struct command *cmd UNUSED, + const char *buf, + const jsmntok_t *result, + struct pending_open *open) +{ + plugin_log(open->p, LOG_DBG, + "`unreserveinputs` for channel %s completed. %*.s", + type_to_string(tmpctx, struct channel_id, &open->channel_id), + json_tok_full_len(result), + json_tok_full(buf, result)); + + return command_done(); +} + +static void unreserve_psbt(struct pending_open *open) +{ + struct out_req *req; + + plugin_log(open->p, LOG_DBG, + "Calling `unreserveinputs` for channel %s", + type_to_string(tmpctx, struct channel_id, + &open->channel_id)); + + req = jsonrpc_request_start(open->p, NULL, + "unreserveinputs", + unreserve_done, unreserve_done, + open); + json_add_psbt(req->js, "psbt", open->psbt); + send_outreq(open->p, req); +} + +static void cleanup_peer_pending_opens(const struct node_id *id) +{ + struct pending_open *i, *next; + list_for_each_safe(&pending_opens, i, next, list) { + if (node_id_eq(&i->peer_id, id)) { + unreserve_psbt(i); + list_del(&i->list); + } + } +} + +static struct pending_open * +cleanup_channel_pending_open(const struct channel_id *cid) +{ + struct pending_open *open; + open = find_channel_pending_open(cid); + + if (!open) + return NULL; + + list_del(&open->list); + return open; +} + +static struct command_result * +command_hook_cont_psbt(struct command *cmd, struct wally_psbt *psbt) +{ + struct json_stream *response; + + response = jsonrpc_stream_success(cmd); + json_add_string(response, "result", "continue"); + json_add_psbt(response, "psbt", psbt); + return command_finished(cmd, response); +} + +static struct command_result * +signpsbt_done(struct command *cmd, + const char *buf, + const jsmntok_t *result, + struct pending_open *open) +{ + struct wally_psbt *signed_psbt; + const char *err; + + plugin_log(cmd->plugin, LOG_DBG, + "`signpsbt` done for channel %s", + type_to_string(tmpctx, struct channel_id, + &open->channel_id)); + err = json_scan(tmpctx, buf, result, + "{signed_psbt:%}", + JSON_SCAN_TAL(cmd, json_to_psbt, &signed_psbt)); + + if (err) + plugin_err(cmd->plugin, + "`signpsbt` payload did not scan %s: %*.s", + err, json_tok_full_len(result), + json_tok_full(buf, result)); + + cleanup_channel_pending_open(&open->channel_id); + return command_hook_cont_psbt(cmd, signed_psbt); +} + +static struct command_result * +json_openchannel2_sign_call(struct command *cmd, + const char *buf, + const jsmntok_t *params) +{ + struct channel_id cid; + struct wally_psbt *psbt; + const char *err; + struct out_req *req; + struct pending_open *open; + + err = json_scan(tmpctx, buf, params, + "{openchannel2_sign:" + "{channel_id:%,psbt:%}}", + JSON_SCAN(json_to_channel_id, &cid), + JSON_SCAN_TAL(cmd, json_to_psbt, &psbt)); + + if (err) + plugin_err(cmd->plugin, + "`openchannel2_sign` payload did not scan %s: %.*s", + err, json_tok_full_len(params), + json_tok_full(buf, params)); + + /* If we're not tracking this open, just pass through */ + open = find_channel_pending_open(&cid); + if (!open) { + plugin_log(cmd->plugin, LOG_DBG, + "nothing to sign for channel %s", + type_to_string(tmpctx, struct channel_id, &cid)); + return command_hook_cont_psbt(cmd, psbt); + } + + if (!psbt_has_our_input(psbt)) { + plugin_log(cmd->plugin, LOG_DBG, + "no inputs to sign for channel %s", + type_to_string(tmpctx, struct channel_id, &cid)); + return command_hook_cont_psbt(cmd, psbt); + } + + plugin_log(cmd->plugin, LOG_DBG, + "openchannel_sign PSBT is %s", + type_to_string(tmpctx, struct wally_psbt, psbt)); + + req = jsonrpc_request_start(cmd->plugin, cmd, + "signpsbt", + &signpsbt_done, + &forward_error, + open); + json_add_psbt(req->js, "psbt", psbt); + /* Use input markers to identify which inputs + * are ours, only sign those */ + json_array_start(req->js, "signonly"); + for (size_t i = 0; i < psbt->num_inputs; i++) { + if (psbt_input_is_ours(&psbt->inputs[i])) + json_add_num(req->js, NULL, i); + } + json_array_end(req->js); + + plugin_log(cmd->plugin, LOG_DBG, + "calling `signpsbt` for channel %s", + type_to_string(tmpctx, struct channel_id, + &open->channel_id)); + return send_outreq(cmd->plugin, req); +} + +static struct command_result * +json_openchannel2_changed_call(struct command *cmd, + const char *buf, + const jsmntok_t *params) +{ + struct channel_id cid; + struct wally_psbt *psbt; + const char *err; + + err = json_scan(tmpctx, buf, params, + "{openchannel2_changed:" + "{channel_id:%,psbt:%}}", + JSON_SCAN(json_to_channel_id, &cid), + JSON_SCAN_TAL(cmd, json_to_psbt, &psbt)); + + if (err) + plugin_err(cmd->plugin, + "`openchannel2_changed` payload did not" + " scan %s: %.*s", + err, json_tok_full_len(params), + json_tok_full(buf, params)); + + plugin_log(cmd->plugin, LOG_DBG, + "openchannel_changed PSBT is %s", + type_to_string(tmpctx, struct wally_psbt, psbt)); + + /* FIXME: do we have any additions or updates to make based + * on their changes? */ + /* For now, we assume we're the same as before and continue + * on as planned */ + return command_hook_cont_psbt(cmd, psbt); +} + +/* Tiny struct to pass info to callback for fundpsbt */ +struct open_info { + struct channel_id cid; + struct node_id id; + struct amount_sat our_funding; +}; + +static struct command_result * +psbt_funded(struct command *cmd, + const char *buf, + const jsmntok_t *result, + struct open_info *info) +{ + struct wally_psbt *psbt; + struct json_stream *response; + struct amount_msat our_funding_msat; + + const char *err; + + err = json_scan(tmpctx, buf, result, + "{psbt:%}", + JSON_SCAN_TAL(tmpctx, json_to_psbt, &psbt)); + if (err) + plugin_err(cmd->plugin, + "`fundpsbt` response did not scan %s: %.*s", + err, json_tok_full_len(result), + json_tok_full(buf, result)); + + /* We also mark all of our inputs as *ours*, so we + * can easily identify them for `signpsbt` later */ + for (size_t i = 0; i < psbt->num_inputs; i++) + psbt_input_mark_ours(psbt, &psbt->inputs[i]); + + new_channel_open(cmd->plugin, cmd->plugin, + info->id, info->cid, psbt); + + if (!amount_sat_to_msat(&our_funding_msat, info->our_funding)) + abort(); + + response = jsonrpc_stream_success(cmd); + json_add_string(response, "result", "continue"); + json_add_psbt(response, "psbt", psbt); + json_add_amount_msat_only(response, "our_funding_msat", + our_funding_msat); + + return command_finished(cmd, response); +} + +static struct command_result * +psbt_fund_failed(struct command *cmd, + const char *buf, + const jsmntok_t *error, + struct open_info *info) +{ + /* Attempt to fund a psbt for this open failed. + * We probably ran out of funds (race?) */ + plugin_log(cmd->plugin, LOG_INFORM, + "Unable to secure %s from wallet," + " continuing channel open to %s" + " without our participation. err %.*s", + type_to_string(tmpctx, struct amount_sat, + &info->our_funding), + type_to_string(tmpctx, struct node_id, + &info->id), + json_tok_full_len(error), + json_tok_full(buf, error)); + + return command_hook_success(cmd); +} + +static struct command_result * +json_openchannel2_call(struct command *cmd, + const char *buf, + const jsmntok_t *params) +{ + struct open_info *info = tal(cmd, struct open_info); + struct amount_sat their_funding, available_funds, channel_max; + struct amount_msat max_htlc_inflight, htlc_minimum; + u64 funding_feerate_perkw, commitment_feerate_perkw, + feerate_our_max, feerate_our_min; + u32 to_self_delay, max_accepted_htlcs, locktime; + u16 channel_flags; + const char *err; + struct out_req *req; + + err = json_scan(tmpctx, buf, params, + "{openchannel2:" + "{id:%" + ",channel_id:%" + ",their_funding:%" + ",max_htlc_value_in_flight_msat:%" + ",htlc_minimum_msat:%" + ",funding_feerate_per_kw:%" + ",commitment_feerate_per_kw:%" + ",feerate_our_max:%" + ",feerate_our_min:%" + ",to_self_delay:%" + ",max_accepted_htlcs:%" + ",channel_flags:%" + ",locktime:%}}", + JSON_SCAN(json_to_node_id, &info->id), + JSON_SCAN(json_to_channel_id, &info->cid), + JSON_SCAN(json_to_sat, &their_funding), + JSON_SCAN(json_to_msat, &max_htlc_inflight), + JSON_SCAN(json_to_msat, &htlc_minimum), + JSON_SCAN(json_to_u64, &funding_feerate_perkw), + JSON_SCAN(json_to_u64, &commitment_feerate_perkw), + JSON_SCAN(json_to_u64, &feerate_our_max), + JSON_SCAN(json_to_u64, &feerate_our_min), + JSON_SCAN(json_to_u32, &to_self_delay), + JSON_SCAN(json_to_u32, &max_accepted_htlcs), + JSON_SCAN(json_to_u16, &channel_flags), + JSON_SCAN(json_to_u32, &locktime)); + + if (err) + plugin_err(cmd->plugin, + "`openchannel2` payload did not scan %s: %.*s", + err, json_tok_full_len(params), + json_tok_full(buf, params)); + + + /* If there's no channel_max, it's actually infinity */ + err = json_scan(tmpctx, buf, params, + "{openchannel2:{channel_max_msat:%}}", + JSON_SCAN(json_to_sat, &channel_max)); + if (err) + channel_max = AMOUNT_SAT(UINT64_MAX); + + /* We don't fund anything that's above or below our feerate */ + if (funding_feerate_perkw < feerate_our_min + || funding_feerate_perkw > feerate_our_max) + return command_hook_success(cmd); + + info->our_funding = calculate_our_funding(current_policy, + info->id, + their_funding, + available_funds, + channel_max); + plugin_log(cmd->plugin, LOG_DBG, + "Policy %s returned funding amount of %s", + funder_policy_desc(tmpctx, current_policy), + type_to_string(tmpctx, struct amount_sat, + &info->our_funding)); + + if (amount_sat_eq(info->our_funding, AMOUNT_SAT(0))) + return command_hook_success(cmd); + + plugin_log(cmd->plugin, LOG_DBG, + "Funding channel %s with %s (their input %s)", + type_to_string(tmpctx, struct channel_id, &info->cid), + type_to_string(tmpctx, struct amount_sat, + &info->our_funding), + type_to_string(tmpctx, struct amount_sat, &their_funding)); + + req = jsonrpc_request_start(cmd->plugin, cmd, + "fundpsbt", + &psbt_funded, + &psbt_fund_failed, + info); + json_add_bool(req->js, "reserve", true); + json_add_string(req->js, "satoshi", + type_to_string(tmpctx, struct amount_sat, + &info->our_funding)); + json_add_string(req->js, "feerate", + tal_fmt(tmpctx, "%"PRIu64"%s", funding_feerate_perkw, + feerate_style_name(FEERATE_PER_KSIPA))); + /* Our startweight is zero because we're freeriding on their open + * transaction ! */ + json_add_num(req->js, "startweight", 0); + json_add_num(req->js, "min_witness_weight", 110); + json_add_bool(req->js, "excess_as_change", true); + json_add_num(req->js, "locktime", locktime); + + return send_outreq(cmd->plugin, req); +} + +static void json_disconnect(struct command *cmd, + const char *buf, + const jsmntok_t *params) +{ + struct node_id id; + const char *err; + + err = json_scan(tmpctx, buf, params, + "{id:%}", + JSON_SCAN(json_to_node_id, &id)); + if (err) + plugin_err(cmd->plugin, + "`disconnect` notification payload did not" + " scan %s: %.*s", + err, json_tok_full_len(params), + json_tok_full(buf, params)); + + plugin_log(cmd->plugin, LOG_DBG, + "Cleaning up inflights for peer id %s", + type_to_string(tmpctx, struct node_id, &id)); + + cleanup_peer_pending_opens(&id); +} + +static void json_channel_open_failed(struct command *cmd, + const char *buf, + const jsmntok_t *params) +{ + struct channel_id cid; + struct pending_open *open; + const char *err; + + err = json_scan(tmpctx, buf, params, + "{channel_open_failed:" + "{channel_id:%}}", + JSON_SCAN(json_to_channel_id, &cid)); + if (err) + plugin_err(cmd->plugin, + "`channel_open_failed` notification payload did" + " not scan %s: %.*s", + err, json_tok_full_len(params), + json_tok_full(buf, params)); + + plugin_log(cmd->plugin, LOG_DBG, + "Cleaning up inflight for channel_id %s", + type_to_string(tmpctx, struct channel_id, &cid)); + + open = cleanup_channel_pending_open(&cid); + if (open) + unreserve_psbt(open); +} + +static const char *init(struct plugin *p, const char *b, const jsmntok_t *t) +{ + list_head_init(&pending_opens); + + return NULL; +} + +const struct plugin_hook hooks[] = { + { + "openchannel2", + json_openchannel2_call, + }, + { + "openchannel2_changed", + json_openchannel2_changed_call, + }, + { + "openchannel2_sign", + json_openchannel2_sign_call, + }, +}; + +const struct plugin_notification notifs[] = { + { + "channel_open_failed", + json_channel_open_failed, + }, + { + "disconnect", + json_disconnect, + }, +}; + +static char *amount_option(const char *arg, struct amount_sat *amt) +{ + if (!parse_amount_sat(amt, arg, strlen(arg))) + return tal_fmt(NULL, "Unable to parse amount '%s'", arg); + + return NULL; +} + +static char *amount_sat_or_u64_option(const char *arg, u64 *amt) +{ + struct amount_sat sats; + char *err; + + err = u64_option(arg, amt); + if (err) { + tal_free(err); + if (!parse_amount_sat(&sats, arg, strlen(arg))) + return tal_fmt(NULL, + "Unable to parse option '%s'", + arg); + + *amt = sats.satoshis; /* Raw: convert to u64 */ + } + + return NULL; +} + +int main(int argc, char **argv) +{ + char *owner = tal(NULL, char); + + setup_locale(); + + /* Our default funding policy is fixed (0msat) */ + current_policy = default_funder_policy(FIXED, 0); + + plugin_main(argv, init, PLUGIN_RESTARTABLE, true, + NULL, + NULL, 0, + notifs, ARRAY_SIZE(notifs), + hooks, ARRAY_SIZE(hooks), + plugin_option("funder-policy", + "string", + "Policy to use for dual-funding requests." + " [match, available, fixed]", + funding_option, ¤t_policy.opt), + plugin_option("funder-policy-mod", + "string", + "Percent to apply policy at" + " (match/available); or amount to fund" + " (fixed)", + amount_sat_or_u64_option, + ¤t_policy.mod), + plugin_option("funder-min-their-funding", + "string", + "Minimum funding peer must open with" + " to activate our policy", + amount_option, + ¤t_policy.min_their_funding), + plugin_option("funder-max-their-funding", + "string", + "Maximum funding peer may open with" + " to activate our policy", + amount_option, + ¤t_policy.max_their_funding), + plugin_option("funder-per-channel-min", + "string", + "Minimum funding we'll add to a channel." + " If we can't meet this, we don't fund", + amount_option, + ¤t_policy.per_channel_min), + plugin_option("funder-per-channel-max", + "string", + "Maximum funding we'll add to a channel." + " We cap all contributions to this", + amount_option, + ¤t_policy.per_channel_max), + plugin_option("funder-reserve-tank", + "string", + "Amount of funds we'll always leave" + " available.", + amount_option, + ¤t_policy.reserve_tank), + plugin_option("funder-fuzz-percent", + "int", + "Percent to fuzz the policy contribution by." + " Defaults to 5%. Max is 100%", + u32_option, + ¤t_policy.fuzz_factor), + plugin_option("funder-fund-probability", + "int", + "Percent of requests to consider." + " Defaults to 100%. Setting to 0% will" + " disable dual-funding", + u32_option, + ¤t_policy.fund_probability), + NULL); + + tal_free(owner); + return 0; +} diff --git a/plugins/funder_policy.c b/plugins/funder_policy.c new file mode 100644 index 000000000000..812fba4d8550 --- /dev/null +++ b/plugins/funder_policy.c @@ -0,0 +1,204 @@ +#include +#include +#include +#include +#include +#include +#include + +const char *funder_opt_name(enum funder_opt opt) +{ + switch (opt) { + case MATCH: + return "match"; + case AVAILABLE: + return "available"; + case FIXED: + return "fixed"; + } + abort(); +} + +char *funding_option(const char *arg, enum funder_opt *opt) +{ + if (streq(arg, "match")) + *opt = MATCH; + else if (streq(arg, "available")) + *opt = AVAILABLE; + else if (streq(arg, "fixed")) + *opt = FIXED; + else + return tal_fmt(NULL, "'%s' is not a valid option" + " (match, available, fixed)", + arg); + return NULL; +} + +const char *funder_policy_desc(const tal_t *ctx, + struct funder_policy policy) +{ + if (policy.opt == FIXED) { + struct amount_sat amt = amount_sat(policy.mod); + return tal_fmt(ctx, "%s (%s)", + funder_opt_name(policy.opt), + type_to_string(ctx, struct amount_sat, &amt)); + } else + return tal_fmt(ctx, "%s (%"PRIu64"%%)", + funder_opt_name(policy.opt), policy.mod); + + /* FIXME: add in more info? */ +} + +struct funder_policy +new_funder_policy(enum funder_opt opt, + u64 policy_mod, + struct amount_sat min_their_funding, + struct amount_sat max_their_funding, + struct amount_sat per_channel_min, + struct amount_sat per_channel_max, + u32 fuzz_factor, + struct amount_sat reserve_tank, + u32 fund_probability) +{ + struct funder_policy policy; + + policy.opt = opt; + policy.mod = policy_mod; + policy.min_their_funding = min_their_funding; + policy.max_their_funding = max_their_funding; + policy.per_channel_min = per_channel_min; + policy.per_channel_max = per_channel_max; + policy.fuzz_factor = fuzz_factor; + policy.reserve_tank = reserve_tank; + policy.fund_probability = fund_probability; + + return policy; +} + +struct funder_policy +default_funder_policy(enum funder_opt policy, + u64 policy_mod) +{ + return new_funder_policy(policy, policy_mod, + AMOUNT_SAT(10000), + AMOUNT_SAT(UINT_MAX), + AMOUNT_SAT(10000), + AMOUNT_SAT(UINT_MAX), + 5, /* fuzz_factor */ + AMOUNT_SAT(0), /* reserve_tank */ + 100); +} + +static struct amount_sat +apply_fuzz(u32 fuzz_factor, struct amount_sat val) +{ + s32 fuzz_percent; + s64 fuzz; + bool ok; + /* Don't even deal with stupid numbers. */ + if ((s64)val.satoshis < 0) /* Raw: val check */ + return AMOUNT_SAT(0); + + fuzz_percent = pseudorand((fuzz_factor * 2) + 1) - fuzz_factor; + fuzz = (s64)val.satoshis * fuzz_percent / 100; /* Raw: fuzzing */ + if (fuzz > 0) + ok = amount_sat_add(&val, val, amount_sat(fuzz)); + else + ok = amount_sat_sub(&val, val, amount_sat(fuzz * -1)); + + assert(ok); + return val; +} + +static struct amount_sat +apply_policy(struct funder_policy policy, + struct amount_sat their_funding, + struct amount_sat available_funds) +{ + struct amount_sat our_funding; + + switch (policy.opt) { + case MATCH: + /* if this fails, it implies ludicrous funding offer, *and* + * > 100% match. Just Say No, kids. */ + if (!amount_sat_scale(&our_funding, their_funding, + policy.mod / 100.0)) + our_funding = AMOUNT_SAT(0); + return our_funding; + case AVAILABLE: + /* Use the 'available_funds' as the starting + * point for your contribution */ + if (!amount_sat_scale(&our_funding, available_funds, + policy.mod / 100.0)) + abort(); + return our_funding; + case FIXED: + /* Use a static amount */ + return amount_sat(policy.mod); + } + + abort(); +} + +struct amount_sat +calculate_our_funding(struct funder_policy policy, + struct node_id id, + struct amount_sat their_funding, + struct amount_sat available_funds, + struct amount_sat channel_max) +{ + struct amount_sat our_funding, avail_channel_space, + net_available_funds; + + /* Are we skipping this one? */ + if (pseudorand(100) >= policy.fund_probability) + return AMOUNT_SAT(0); + + /* Figure out amount of actual headroom we have */ + if (!amount_sat_sub(&avail_channel_space, channel_max, their_funding)) + return AMOUNT_SAT(0); + + /* Figure out actual available funds, given our requested + * 'reserve_tank' */ + if (!amount_sat_sub(&net_available_funds, available_funds, + policy.reserve_tank)) + return AMOUNT_SAT(0); + + /* Are they funding enough ? */ + if (amount_sat_less(their_funding, policy.min_their_funding)) + return AMOUNT_SAT(0); + + /* Are they funding too much ? */ + if (amount_sat_greater(their_funding, policy.max_their_funding)) + return AMOUNT_SAT(0); + + /* What's our amount, given our policy */ + our_funding = apply_policy(policy, their_funding, available_funds); + + /* our_funding is probably sane, so let's fuzz this amount a bit */ + our_funding = apply_fuzz(policy.fuzz_factor, our_funding); + + /* Is our_funding more than we can fit? if so set to avail space */ + if (amount_sat_greater(our_funding, avail_channel_space)) + our_funding = avail_channel_space; + + /* Is our_funding more than we want to fund in a channel? + * if so set at our desired per-channel max */ + if (amount_sat_greater(our_funding, policy.per_channel_max)) + our_funding = policy.per_channel_max; + + /* FIXME: net_available_funds needs to know feerate, and make + * worst-case UTXO assumptions? */ + + /* Is our_funding more than we have available? if so + * set to max available */ + if (amount_sat_greater(our_funding, net_available_funds)) + our_funding = net_available_funds; + + /* Is our_funding less than our per-channel minimum? + * if so, don't fund */ + if (amount_sat_less(our_funding, policy.per_channel_min)) + return AMOUNT_SAT(0); + + return our_funding; +} diff --git a/plugins/funder_policy.h b/plugins/funder_policy.h new file mode 100644 index 000000000000..dceb1d7dc35c --- /dev/null +++ b/plugins/funder_policy.h @@ -0,0 +1,92 @@ +#ifndef LIGHTNING_PLUGINS_FUNDER_POLICY_H +#define LIGHTNING_PLUGINS_FUNDER_POLICY_H +#include "config.h" +#include + +struct node_id; + +/* Policy Options */ +enum funder_opt { + /* Use their_funding as the starting + * point for your contribution */ + MATCH, + + /* Use the 'available_funds' as the starting + * point for your contribution */ + AVAILABLE, + + /* Use a static amount */ + FIXED, +}; + +struct funder_policy { + /* How to interpret/apply the 'mod' field */ + enum funder_opt opt; + + /* for MATCH/AVAILABLE, is a percent of base; + * for FIXED is the satoshi amount */ + u64 mod; + + /* `their_funding` must be this much or greater to activate + * the policy. Defaults to 10,000 sats */ + struct amount_sat min_their_funding; + + /* `their_funding` must be this amount or less to activate + * the policy. Defaults to MAX_UNITsats */ + struct amount_sat max_their_funding; + + /* Upper limit on amount to add. Defaults to + * `available_funds` */ + struct amount_sat per_channel_max; + + /* Lower limit on amount to add. Defaults to + * 10,000sat */ + struct amount_sat per_channel_min; + + /* Percent to fuzz by. Default is 5% */ + u32 fuzz_factor; + + /* Minimum amount to leave unused in `available_funds`. + * Note that this is presently best-effort due to concurrency. + * Default is 0msat */ + struct amount_sat reserve_tank; + + /* Percent of open offers we'll consider funding. */ + u32 fund_probability; +}; + +struct funder_policy +new_funder_policy(enum funder_opt opt, + u64 policy_mod, + struct amount_sat min_their_funding, + struct amount_sat max_their_funding, + struct amount_sat per_channel_min, + struct amount_sat per_channel_max, + u32 fuzz_factor, + struct amount_sat reserve_tank, + u32 fund_probability); + +/* Get a new funder_policy, set to the defaults */ +struct funder_policy +default_funder_policy(enum funder_opt policy, + u64 policy_mod); + +/* Given the policy and this request's details, figure + * out how much we should contribute to this channel */ +struct amount_sat +calculate_our_funding(struct funder_policy policy, + struct node_id id, + struct amount_sat their_funding, + struct amount_sat available_funds, + struct amount_sat channel_max); + +/* Get the name of this policy option */ +const char *funder_opt_name(enum funder_opt opt); + +/* Get a (short, for now) description of the provided policy */ +const char *funder_policy_desc(const tal_t *ctx, + const struct funder_policy policy); + +/* Convert a cmdline option to a funding_opt */ +char *funding_option(const char *arg, enum funder_opt *opt); +#endif /* LIGHTNING_PLUGINS_FUNDER_POLICY_H */ diff --git a/tests/test_opening.py b/tests/test_opening.py index 76e8033f2460..1703f54df3b3 100644 --- a/tests/test_opening.py +++ b/tests/test_opening.py @@ -203,8 +203,10 @@ def test_v2_open_sigs_restart_while_dead(node_factory, bitcoind): @unittest.skipIf(TEST_NETWORK != 'regtest', 'elementsd doesnt yet support PSBT features we need') def test_v2_rbf(node_factory, bitcoind, chainparams): l1, l2 = node_factory.get_nodes(2, - opts=[{'experimental-dual-fund': None}, - {'experimental-dual-fund': None}]) + opts=[{'experimental-dual-fund': None, + 'wumbo': None}, + {'experimental-dual-fund': None, + 'wumbo': None}]) l1.rpc.connect(l2.info['id'], 'localhost', l2.port) amount = 2**24 From e13b310c4db25f66dbcf26e92d88f2dc79ba96b7 Mon Sep 17 00:00:00 2001 From: niftynei Date: Fri, 23 Apr 2021 15:32:47 -0500 Subject: [PATCH 12/20] funder: use listfunds to fetch utxo data Compute available_funds locally, instead of getting it from the openchannel2 hook payload. Suggested-By: Rusty Russell @rustyrussell --- plugins/funder.c | 188 +++++++++++++++++++++++++++++++++++------------ 1 file changed, 143 insertions(+), 45 deletions(-) diff --git a/plugins/funder.c b/plugins/funder.c index 0aaf899e3bb6..d8f0990a94cc 100644 --- a/plugins/funder.c +++ b/plugins/funder.c @@ -270,6 +270,10 @@ struct open_info { struct channel_id cid; struct node_id id; struct amount_sat our_funding; + struct amount_sat their_funding; + struct amount_sat channel_max; + u64 funding_feerate_perkw; + u32 locktime; }; static struct command_result * @@ -335,17 +339,135 @@ psbt_fund_failed(struct command *cmd, return command_hook_success(cmd); } +static struct command_result * +listfunds_success(struct command *cmd, + const char *buf, + const jsmntok_t *result, + struct open_info *info) +{ + struct amount_sat available_funds; + const jsmntok_t *outputs_tok, *tok; + struct out_req *req; + size_t i; + + outputs_tok = json_get_member(buf, result, "outputs"); + if (!outputs_tok) + plugin_err(cmd->plugin, + "`listfunds` payload has no outputs token: %*.s", + json_tok_full_len(result), + json_tok_full(buf, result)); + + available_funds = AMOUNT_SAT(0); + json_for_each_arr(i, tok, outputs_tok) { + struct amount_sat val; + bool is_reserved; + char *status; + const char *err; + + err = json_scan(tmpctx, buf, tok, + "{amount_msat:%" + ",status:%" + ",reserved:%}", + JSON_SCAN(json_to_sat, &val), + JSON_SCAN_TAL(cmd, json_strdup, &status), + JSON_SCAN(json_to_bool, &is_reserved)); + if (err) + plugin_err(cmd->plugin, + "`listfunds` payload did not scan. %s: %*.s", + err, json_tok_full_len(result), + json_tok_full(buf, result)); + + /* we skip reserved funds */ + if (is_reserved) + continue; + + /* we skip unconfirmed+spent funds */ + if (!streq(status, "confirmed")) + continue; + + if (!amount_sat_add(&available_funds, available_funds, val)) + plugin_err(cmd->plugin, + "`listfunds` overflowed output values"); + + /* FIXME: count of utxos? */ + } + + info->our_funding = calculate_our_funding(current_policy, + info->id, + info->their_funding, + available_funds, + info->channel_max); + plugin_log(cmd->plugin, LOG_DBG, + "Policy %s returned funding amount of %s", + funder_policy_desc(tmpctx, current_policy), + type_to_string(tmpctx, struct amount_sat, + &info->our_funding)); + + if (amount_sat_eq(info->our_funding, AMOUNT_SAT(0))) + return command_hook_success(cmd); + + plugin_log(cmd->plugin, LOG_DBG, + "Funding channel %s with %s (their input %s)", + type_to_string(tmpctx, struct channel_id, &info->cid), + type_to_string(tmpctx, struct amount_sat, + &info->our_funding), + type_to_string(tmpctx, struct amount_sat, + &info->their_funding)); + + req = jsonrpc_request_start(cmd->plugin, cmd, + "fundpsbt", + &psbt_funded, + &psbt_fund_failed, + info); + json_add_bool(req->js, "reserve", true); + json_add_string(req->js, "satoshi", + type_to_string(tmpctx, struct amount_sat, + &info->our_funding)); + json_add_string(req->js, "feerate", + tal_fmt(tmpctx, "%"PRIu64"%s", + info->funding_feerate_perkw, + feerate_style_name(FEERATE_PER_KSIPA))); + /* Our startweight is zero because we're freeriding on their open + * transaction ! */ + json_add_num(req->js, "startweight", 0); + json_add_num(req->js, "min_witness_weight", 110); + json_add_bool(req->js, "excess_as_change", true); + json_add_num(req->js, "locktime", info->locktime); + + return send_outreq(cmd->plugin, req); +} + +static struct command_result * +listfunds_failed(struct command *cmd, + const char *buf, + const jsmntok_t *error, + struct open_info *info) +{ + + /* Something went wrong fetching the funds info + * for our wallet. Just keep going */ + plugin_log(cmd->plugin, LOG_INFORM, + "Unable to fetch wallet funds info." + " Continuing channel open to %s" + " without our participation. err %.*s", + type_to_string(tmpctx, struct node_id, + &info->id), + json_tok_full_len(error), + json_tok_full(buf, error)); + + return command_hook_success(cmd); +} + static struct command_result * json_openchannel2_call(struct command *cmd, const char *buf, const jsmntok_t *params) { struct open_info *info = tal(cmd, struct open_info); - struct amount_sat their_funding, available_funds, channel_max; struct amount_msat max_htlc_inflight, htlc_minimum; - u64 funding_feerate_perkw, commitment_feerate_perkw, + u64 commitment_feerate_perkw, feerate_our_max, feerate_our_min; - u32 to_self_delay, max_accepted_htlcs, locktime; + u32 to_self_delay, max_accepted_htlcs; u16 channel_flags; const char *err; struct out_req *req; @@ -367,17 +489,17 @@ json_openchannel2_call(struct command *cmd, ",locktime:%}}", JSON_SCAN(json_to_node_id, &info->id), JSON_SCAN(json_to_channel_id, &info->cid), - JSON_SCAN(json_to_sat, &their_funding), + JSON_SCAN(json_to_sat, &info->their_funding), JSON_SCAN(json_to_msat, &max_htlc_inflight), JSON_SCAN(json_to_msat, &htlc_minimum), - JSON_SCAN(json_to_u64, &funding_feerate_perkw), + JSON_SCAN(json_to_u64, &info->funding_feerate_perkw), JSON_SCAN(json_to_u64, &commitment_feerate_perkw), JSON_SCAN(json_to_u64, &feerate_our_max), JSON_SCAN(json_to_u64, &feerate_our_min), JSON_SCAN(json_to_u32, &to_self_delay), JSON_SCAN(json_to_u32, &max_accepted_htlcs), JSON_SCAN(json_to_u16, &channel_flags), - JSON_SCAN(json_to_u32, &locktime)); + JSON_SCAN(json_to_u32, &info->locktime)); if (err) plugin_err(cmd->plugin, @@ -389,54 +511,30 @@ json_openchannel2_call(struct command *cmd, /* If there's no channel_max, it's actually infinity */ err = json_scan(tmpctx, buf, params, "{openchannel2:{channel_max_msat:%}}", - JSON_SCAN(json_to_sat, &channel_max)); + JSON_SCAN(json_to_sat, &info->channel_max)); if (err) - channel_max = AMOUNT_SAT(UINT64_MAX); + info->channel_max = AMOUNT_SAT(UINT64_MAX); /* We don't fund anything that's above or below our feerate */ - if (funding_feerate_perkw < feerate_our_min - || funding_feerate_perkw > feerate_our_max) - return command_hook_success(cmd); + if (info->funding_feerate_perkw < feerate_our_min + || info->funding_feerate_perkw > feerate_our_max) { - info->our_funding = calculate_our_funding(current_policy, - info->id, - their_funding, - available_funds, - channel_max); - plugin_log(cmd->plugin, LOG_DBG, - "Policy %s returned funding amount of %s", - funder_policy_desc(tmpctx, current_policy), - type_to_string(tmpctx, struct amount_sat, - &info->our_funding)); + plugin_log(cmd->plugin, LOG_DBG, + "their feerate %"PRIu64" is out of" + " our bounds (%"PRIu64"-%"PRIu64")", + info->funding_feerate_perkw, + feerate_our_min, + feerate_our_max); - if (amount_sat_eq(info->our_funding, AMOUNT_SAT(0))) return command_hook_success(cmd); + } - plugin_log(cmd->plugin, LOG_DBG, - "Funding channel %s with %s (their input %s)", - type_to_string(tmpctx, struct channel_id, &info->cid), - type_to_string(tmpctx, struct amount_sat, - &info->our_funding), - type_to_string(tmpctx, struct amount_sat, &their_funding)); - + /* Figure out what our funds are */ req = jsonrpc_request_start(cmd->plugin, cmd, - "fundpsbt", - &psbt_funded, - &psbt_fund_failed, + "listfunds", + &listfunds_success, + &listfunds_failed, info); - json_add_bool(req->js, "reserve", true); - json_add_string(req->js, "satoshi", - type_to_string(tmpctx, struct amount_sat, - &info->our_funding)); - json_add_string(req->js, "feerate", - tal_fmt(tmpctx, "%"PRIu64"%s", funding_feerate_perkw, - feerate_style_name(FEERATE_PER_KSIPA))); - /* Our startweight is zero because we're freeriding on their open - * transaction ! */ - json_add_num(req->js, "startweight", 0); - json_add_num(req->js, "min_witness_weight", 110); - json_add_bool(req->js, "excess_as_change", true); - json_add_num(req->js, "locktime", locktime); return send_outreq(cmd->plugin, req); } From 86eb1e093d4b5923a86873b99c5c1dc22339fb0d Mon Sep 17 00:00:00 2001 From: niftynei Date: Thu, 22 Apr 2021 17:53:28 -0500 Subject: [PATCH 13/20] funder-test: tests for our policy configurations --- plugins/Makefile | 2 + plugins/test/Makefile | 23 ++ plugins/test/run-funder_policy.c | 411 +++++++++++++++++++++++++++++++ 3 files changed, 436 insertions(+) create mode 100644 plugins/test/Makefile create mode 100644 plugins/test/run-funder_policy.c diff --git a/plugins/Makefile b/plugins/Makefile index 713dff19cfe7..410a2a7be9e6 100644 --- a/plugins/Makefile +++ b/plugins/Makefile @@ -156,3 +156,5 @@ $(PLUGIN_ALL_OBJS): $(PLUGIN_LIB_HEADER) ALL_C_HEADERS += plugins/list_of_builtin_plugins_gen.h plugins/list_of_builtin_plugins_gen.h: plugins/Makefile Makefile @$(call VERBOSE,GEN $@,echo "static const char *list_of_builtin_plugins[] = { $(foreach d,$(notdir $(PLUGINS)),\"$d\",) NULL };" > $@) + +include plugins/test/Makefile diff --git a/plugins/test/Makefile b/plugins/test/Makefile new file mode 100644 index 000000000000..2c4b5c121653 --- /dev/null +++ b/plugins/test/Makefile @@ -0,0 +1,23 @@ +# Note that these actually #include everything they need, except ccan/ and bitcoin/. +# That allows for unit testing of statics, and special effects. +PLUGIN_TEST_SRC := $(wildcard plugins/test/run-*.c) +PLUGIN_TEST_OBJS := $(PLUGIN_TEST_SRC:.c=.o) +PLUGIN_TEST_PROGRAMS := $(PLUGIN_TEST_OBJS:.o=) + +ALL_C_SOURCES += $(PLUGIN__TEST_SRC) +ALL_TEST_PROGRAMS += $(PLUGIN_TEST_PROGRAMS) + +PLUGIN_TEST_COMMON_OBJS := \ + common/amount.o \ + common/pseudorand.o \ + common/setup.o \ + common/type_to_string.o \ + common/utils.o + +update-mocks: $(PLUGIN_TEST_SRC:%=update-mocks/%) + +$(PLUGIN_TEST_PROGRAMS): $(CCAN_OBJS) $(BITCOIN_OBJS) $(WIRE_OBJS) $(PLUGIN_TEST_COMMON_OBJS) + +$(PLUGIN_TEST_OBJS): $(PLUGIN_FUNDER_HEADER) $(PLUGIN_FUNDER_SRC) + +check-units: $(PLUGIN_TEST_PROGRAMS:%=unittest/%) diff --git a/plugins/test/run-funder_policy.c b/plugins/test/run-funder_policy.c new file mode 100644 index 000000000000..d1051311743c --- /dev/null +++ b/plugins/test/run-funder_policy.c @@ -0,0 +1,411 @@ +#include "../funder_policy.c" +#include +#include +#include +#include +#include +#include +#include + +/* AUTOGENERATED MOCKS START */ +/* Generated stub for fromwire_bigsize */ +bigsize_t fromwire_bigsize(const u8 **cursor UNNEEDED, size_t *max UNNEEDED) +{ fprintf(stderr, "fromwire_bigsize called!\n"); abort(); } +/* Generated stub for fromwire_channel_id */ +void fromwire_channel_id(const u8 **cursor UNNEEDED, size_t *max UNNEEDED, + struct channel_id *channel_id UNNEEDED) +{ fprintf(stderr, "fromwire_channel_id called!\n"); abort(); } +/* Generated stub for fromwire_node_id */ +void fromwire_node_id(const u8 **cursor UNNEEDED, size_t *max UNNEEDED, struct node_id *id UNNEEDED) +{ fprintf(stderr, "fromwire_node_id called!\n"); abort(); } +/* Generated stub for towire_bigsize */ +void towire_bigsize(u8 **pptr UNNEEDED, const bigsize_t val UNNEEDED) +{ fprintf(stderr, "towire_bigsize called!\n"); abort(); } +/* Generated stub for towire_channel_id */ +void towire_channel_id(u8 **pptr UNNEEDED, const struct channel_id *channel_id UNNEEDED) +{ fprintf(stderr, "towire_channel_id called!\n"); abort(); } +/* Generated stub for towire_node_id */ +void towire_node_id(u8 **pptr UNNEEDED, const struct node_id *id UNNEEDED) +{ fprintf(stderr, "towire_node_id called!\n"); abort(); } +/* AUTOGENERATED MOCKS END */ + +struct test_case { + struct amount_sat their_funds; + struct amount_sat available_funds; + struct amount_sat channel_max; + + struct funder_policy policy; + + struct amount_sat exp_our_funds; +}; + +struct test_case cases[] = { + /* Straight fixed */ + { + .their_funds = AMOUNT_SAT(5000), + .available_funds = AMOUNT_SAT(100000), + .channel_max = AMOUNT_SAT(11000), + .policy = { + .opt = FIXED, + .mod = 1111, + .min_their_funding = AMOUNT_SAT(0), + .max_their_funding = AMOUNT_SAT(10000), + .per_channel_max = AMOUNT_SAT(10000), + .per_channel_min = AMOUNT_SAT(0), + .fuzz_factor = 0, + .reserve_tank = AMOUNT_SAT(0), + .fund_probability = 100, + }, + + .exp_our_funds = AMOUNT_SAT(1111), + }, + /* Match 0 */ + { + .their_funds = AMOUNT_SAT(5000), + .available_funds = AMOUNT_SAT(500), + .channel_max = AMOUNT_SAT(11000), + .policy = { + .opt = MATCH, + .mod = 0, + .min_their_funding = AMOUNT_SAT(0), + .max_their_funding = AMOUNT_SAT(10000), + .per_channel_max = AMOUNT_SAT(10000), + .per_channel_min = AMOUNT_SAT(1000), + .fuzz_factor = 0, + .reserve_tank = AMOUNT_SAT(0), + .fund_probability = 100, + }, + + .exp_our_funds = AMOUNT_SAT(0), + }, + /* Match 100 */ + { + .their_funds = AMOUNT_SAT(5000), + .available_funds = AMOUNT_SAT(6000), + .channel_max = AMOUNT_SAT(11000), + .policy = { + .opt = MATCH, + .mod = 100, + .min_their_funding = AMOUNT_SAT(0), + .max_their_funding = AMOUNT_SAT(10000), + .per_channel_max = AMOUNT_SAT(10000), + .per_channel_min = AMOUNT_SAT(1000), + .fuzz_factor = 0, + .reserve_tank = AMOUNT_SAT(0), + .fund_probability = 100, + }, + + .exp_our_funds = AMOUNT_SAT(5000), + }, + /* Match 200 */ + { + .their_funds = AMOUNT_SAT(2500), + .available_funds = AMOUNT_SAT(6000), + .channel_max = AMOUNT_SAT(11000), + .policy = { + .opt = MATCH, + .mod = 200, + .min_their_funding = AMOUNT_SAT(0), + .max_their_funding = AMOUNT_SAT(10000), + .per_channel_max = AMOUNT_SAT(10000), + .per_channel_min = AMOUNT_SAT(1000), + .fuzz_factor = 0, + .reserve_tank = AMOUNT_SAT(0), + .fund_probability = 100, + }, + + .exp_our_funds = AMOUNT_SAT(5000), + }, + /* Available 0 */ + { + .their_funds = AMOUNT_SAT(2500), + .available_funds = AMOUNT_SAT(5000), + .channel_max = AMOUNT_SAT(11000), + .policy = { + .opt = AVAILABLE, + .mod = 0, + .min_their_funding = AMOUNT_SAT(0), + .max_their_funding = AMOUNT_SAT(10000), + .per_channel_max = AMOUNT_SAT(10000), + .per_channel_min = AMOUNT_SAT(1000), + .fuzz_factor = 0, + .reserve_tank = AMOUNT_SAT(0), + .fund_probability = 100, + }, + + .exp_our_funds = AMOUNT_SAT(0), + }, + /* Available 50 */ + { + .their_funds = AMOUNT_SAT(2500), + .available_funds = AMOUNT_SAT(3000), + .channel_max = AMOUNT_SAT(11000), + .policy = { + .opt = AVAILABLE, + .mod = 50, + .min_their_funding = AMOUNT_SAT(0), + .max_their_funding = AMOUNT_SAT(10000), + .per_channel_max = AMOUNT_SAT(10000), + .per_channel_min = AMOUNT_SAT(1000), + .fuzz_factor = 0, + .reserve_tank = AMOUNT_SAT(0), + .fund_probability = 100, + }, + + .exp_our_funds = AMOUNT_SAT(1500), + }, + /* Available 100+ */ + { + .their_funds = AMOUNT_SAT(2500), + .available_funds = AMOUNT_SAT(5000), + .channel_max = AMOUNT_SAT(11000), + .policy = { + .opt = AVAILABLE, + .mod = 100, + .min_their_funding = AMOUNT_SAT(0), + .max_their_funding = AMOUNT_SAT(10000), + .per_channel_max = AMOUNT_SAT(10000), + .per_channel_min = AMOUNT_SAT(1000), + .fuzz_factor = 0, + .reserve_tank = AMOUNT_SAT(0), + .fund_probability = 100, + }, + + .exp_our_funds = AMOUNT_SAT(5000), + }, + /* Fixed above per-channel max*/ + { + .their_funds = AMOUNT_SAT(5000), + .available_funds = AMOUNT_SAT(5000), + .channel_max = AMOUNT_SAT(11000), + .policy = { + .opt = FIXED, + /* We give these weird numbering so + * they're easy to identify when they break */ + .mod = 1011, + .min_their_funding = AMOUNT_SAT(0), + .max_their_funding = AMOUNT_SAT(10000), + .per_channel_max = AMOUNT_SAT(900), + .per_channel_min = AMOUNT_SAT(100), + .fuzz_factor = 0, + .reserve_tank = AMOUNT_SAT(0), + .fund_probability = 100, + }, + + .exp_our_funds = AMOUNT_SAT(900), + }, + /* Fixed less than available space */ + { + .their_funds = AMOUNT_SAT(5000), + .available_funds = AMOUNT_SAT(5000), + .channel_max = AMOUNT_SAT(5500), + .policy = { + .opt = FIXED, + .mod = 1002, + .min_their_funding = AMOUNT_SAT(0), + .max_their_funding = AMOUNT_SAT(10000), + .per_channel_max = AMOUNT_SAT(10000), + .per_channel_min = AMOUNT_SAT(0), + .fuzz_factor = 0, + .reserve_tank = AMOUNT_SAT(0), + .fund_probability = 100, + }, + + .exp_our_funds = AMOUNT_SAT(500), + }, + /* Fixed less than available funds */ + { + .their_funds = AMOUNT_SAT(5000), + .available_funds = AMOUNT_SAT(500), + .channel_max = AMOUNT_SAT(10000), + .policy = { + .opt = FIXED, + .mod = 1001, + .min_their_funding = AMOUNT_SAT(0), + .max_their_funding = AMOUNT_SAT(10000), + .per_channel_max = AMOUNT_SAT(10000), + .per_channel_min = AMOUNT_SAT(0), + .fuzz_factor = 0, + .reserve_tank = AMOUNT_SAT(0), + .fund_probability = 100, + }, + + .exp_our_funds = AMOUNT_SAT(500), + }, + /* Peer is under 'min_their_funding' */ + { + .their_funds = AMOUNT_SAT(5000), + .available_funds = AMOUNT_SAT(1000), + .channel_max = AMOUNT_SAT(10000), + .policy = { + .opt = FIXED, + .mod = 999, + .min_their_funding = AMOUNT_SAT(5001), + .max_their_funding = AMOUNT_SAT(10000), + .per_channel_max = AMOUNT_SAT(10000), + .per_channel_min = AMOUNT_SAT(0), + .fuzz_factor = 0, + .reserve_tank = AMOUNT_SAT(0), + .fund_probability = 100, + }, + + .exp_our_funds = AMOUNT_SAT(0), + }, + /* Peer exceeds 'max_their_funding' */ + { + .their_funds = AMOUNT_SAT(5001), + .available_funds = AMOUNT_SAT(5000), + .channel_max = AMOUNT_SAT(10000), + .policy = { + .opt = FIXED, + .mod = 998, + .min_their_funding = AMOUNT_SAT(0), + .max_their_funding = AMOUNT_SAT(500), + .per_channel_max = AMOUNT_SAT(10000), + .per_channel_min = AMOUNT_SAT(0), + .fuzz_factor = 0, + .reserve_tank = AMOUNT_SAT(0), + .fund_probability = 100, + }, + + .exp_our_funds = AMOUNT_SAT(0), + }, + /* Fixed less than available funds less reserve tank */ + { + .their_funds = AMOUNT_SAT(5000), + .available_funds = AMOUNT_SAT(1000), + .channel_max = AMOUNT_SAT(10000), + .policy = { + .opt = FIXED, + .mod = 997, + .min_their_funding = AMOUNT_SAT(0), + .max_their_funding = AMOUNT_SAT(10000), + .per_channel_max = AMOUNT_SAT(10000), + .per_channel_min = AMOUNT_SAT(0), + .fuzz_factor = 0, + .reserve_tank = AMOUNT_SAT(100), + .fund_probability = 100, + }, + + .exp_our_funds = AMOUNT_SAT(900), + }, + /* Fixed below per-channel min */ + { + .their_funds = AMOUNT_SAT(5000), + .available_funds = AMOUNT_SAT(5000), + .channel_max = AMOUNT_SAT(11000), + .policy = { + .opt = FIXED, + .mod = 988, + .min_their_funding = AMOUNT_SAT(0), + .max_their_funding = AMOUNT_SAT(10000), + .per_channel_max = AMOUNT_SAT(10000), + .per_channel_min = AMOUNT_SAT(989), + .fuzz_factor = 0, + .reserve_tank = AMOUNT_SAT(0), + .fund_probability = 100, + }, + + .exp_our_funds = AMOUNT_SAT(0), + }, +}; + +static void check_fuzzing(struct test_case fuzzcase) +{ + struct node_id id; + struct amount_sat our_funds; + struct amount_sat fuzz_max = AMOUNT_SAT(0), + fuzz_min = AMOUNT_SAT(UINT_MAX); + u64 fuzz_amt = fuzzcase.policy.mod * fuzzcase.policy.fuzz_factor / 100; + + memset(&id, 2, sizeof(struct node_id)); + + for (size_t i = 0; i < 100; i++) { + our_funds = calculate_our_funding(fuzzcase.policy, id, + fuzzcase.their_funds, + fuzzcase.available_funds, + fuzzcase.channel_max); + if (amount_sat_greater(our_funds, fuzz_max)) + fuzz_max = our_funds; + if (amount_sat_less(our_funds, fuzz_min)) + fuzz_min = our_funds; + } + + assert(fuzz_max.satoshis <= fuzzcase.policy.mod + fuzz_amt); + assert(fuzz_min.satoshis >= fuzzcase.policy.mod - fuzz_amt); +} + +int main(int argc, const char *argv[]) +{ + struct funder_policy policy; + struct node_id id; + struct amount_sat empty = AMOUNT_SAT(0), our_funds; + bool ok = true; + size_t i = 0, flips = 0; + struct test_case flipcase, fuzzcase; + size_t flipcount = 0; + + common_setup(argv[0]); + memset(&id, 2, sizeof(struct node_id)); + + /* Check the default funder policy, at fixed (0msat) */ + policy = default_funder_policy(FIXED, 0); + + /* Use the first test case inputs? */ + our_funds = calculate_our_funding(policy, id, + cases[i].their_funds, + cases[i].available_funds, + cases[i].channel_max); + assert(amount_sat_eq(empty, our_funds)); + + for (i = 0; i < ARRAY_SIZE(cases); i++) { + our_funds = calculate_our_funding(cases[i].policy, id, + cases[i].their_funds, + cases[i].available_funds, + cases[i].channel_max); + if (!amount_sat_eq(cases[i].exp_our_funds, our_funds)) { + fprintf(stderr, "FAIL policy: %s. expected %s, got %s\n", + funder_policy_desc(NULL, cases[i].policy), + type_to_string(NULL, struct amount_sat, + &cases[i].exp_our_funds), + type_to_string(NULL, struct amount_sat, + &our_funds)); + ok = false; + } + } + if (!ok) + exit(1); + + /* Try a few fund_probabilitys, we should only fund + * 1/10th of the time */ + flips = 10; + flipcase = cases[0]; + flipcase.policy.fund_probability = flips; + + for (i = 0; i < 100 * flips; i++) { + our_funds = calculate_our_funding(flipcase.policy, id, + flipcase.their_funds, + flipcase.available_funds, + flipcase.channel_max); + if (!amount_sat_eq(our_funds, AMOUNT_SAT(0))) + flipcount++; + } + /* We should be close to 100, give or take 100 on each side */ + assert(flipcount > 0); + assert(flipcount < 200); + + /* Try some value fuzzing with a high fuzz (for roll overs) */ + fuzzcase = cases[0]; + fuzzcase.policy.mod = 1000; + /* This is higher than our allowed fuzz factor, it'll + * get shifted down to 100 */ + fuzzcase.policy.fuzz_factor = 100; + check_fuzzing(fuzzcase); + + /* Try some fuzzing with a low fuzz */ + fuzzcase.policy.fuzz_factor = 1; + check_fuzzing(fuzzcase); + common_shutdown(); + + return 0; +} From 45e87a97df09d89e6d1bee3df039b546ede087df Mon Sep 17 00:00:00 2001 From: niftynei Date: Fri, 23 Apr 2021 16:59:08 -0500 Subject: [PATCH 14/20] contrib: if you're in dev mode, use dual-funding (with matching) If you're using the regtest node, turn on dual funding and automatically attempt to dual fund at a 100% match for every channel open that you do. --- contrib/startup_regtest.sh | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/contrib/startup_regtest.sh b/contrib/startup_regtest.sh index 6502685b8213..4df1e9fca232 100755 --- a/contrib/startup_regtest.sh +++ b/contrib/startup_regtest.sh @@ -92,8 +92,16 @@ start_nodes() { # If we've configured to use developer, add dev options if $LIGHTNINGD --help | grep -q dev-fast-gossip; then - echo "dev-fast-gossip" >> "/tmp/l$i-$network/config" - echo "dev-bitcoind-poll=5" >> "/tmp/l$i-$network/config" + cat <<- EOF >> "/tmp/l$i-$network/config" + dev-fast-gossip + dev-bitcoind-poll=5 + experimental-dual-fund + funder-policy=match + funder-policy-mod=1000 + funder-min-their-funding=10000 + funder-per-channel-max=100000 + funder-fuzz-percent=0 + EOF fi From ec846cd5733dcf4e7f9b8be81513f37815e13d77 Mon Sep 17 00:00:00 2001 From: niftynei Date: Thu, 22 Apr 2021 16:33:08 -0500 Subject: [PATCH 15/20] funder: sanitize inputs Error out if we've got the wrong info --- plugins/funder.c | 6 ++++++ plugins/funder_policy.c | 26 ++++++++++++++++++++++++++ plugins/funder_policy.h | 3 +++ 3 files changed, 35 insertions(+) diff --git a/plugins/funder.c b/plugins/funder.c index d8f0990a94cc..7388a12696f4 100644 --- a/plugins/funder.c +++ b/plugins/funder.c @@ -593,8 +593,14 @@ static void json_channel_open_failed(struct command *cmd, static const char *init(struct plugin *p, const char *b, const jsmntok_t *t) { + const char *err; + list_head_init(&pending_opens); + err = funder_check_policy(¤t_policy); + if (err) + plugin_err(p, "Invalid parameter combination: %s", err); + return NULL; } diff --git a/plugins/funder_policy.c b/plugins/funder_policy.c index 812fba4d8550..b5a8f36ed242 100644 --- a/plugins/funder_policy.c +++ b/plugins/funder_policy.c @@ -89,6 +89,32 @@ default_funder_policy(enum funder_opt policy, 100); } +char *funder_check_policy(const struct funder_policy *policy) +{ + if (policy->fund_probability > 100) + return "fund_probability max is 100"; + + if (policy->fuzz_factor > 100) + return "fuzz_percent max is 100"; + + switch (policy->opt) { + case FIXED: + /* We don't do anything for fixed */ + return NULL; + case MATCH: + if (policy->mod > 200) + return "Max allowed policy_mod for 'match'" + " is 200"; + return NULL; + case AVAILABLE: + if (policy->mod > 100) + return "Max allowed policy_mod for 'available'" + " is 100"; + return NULL; + } + abort(); +} + static struct amount_sat apply_fuzz(u32 fuzz_factor, struct amount_sat val) { diff --git a/plugins/funder_policy.h b/plugins/funder_policy.h index dceb1d7dc35c..51e1cf172874 100644 --- a/plugins/funder_policy.h +++ b/plugins/funder_policy.h @@ -89,4 +89,7 @@ const char *funder_policy_desc(const tal_t *ctx, /* Convert a cmdline option to a funding_opt */ char *funding_option(const char *arg, enum funder_opt *opt); + +/* Check policy settings, return error if fails */ +char *funder_check_policy(const struct funder_policy *policy); #endif /* LIGHTNING_PLUGINS_FUNDER_POLICY_H */ From 9795006ec22474e93a3e96a27ce3c7339a7d0f04 Mon Sep 17 00:00:00 2001 From: niftynei Date: Thu, 22 Apr 2021 15:52:43 -0500 Subject: [PATCH 16/20] funder: `funderupdate` command to see/set configs Changelog-Added: Plugins: `funder` plugin now has new command `funderupdate` which will show current funding configuration and allow you to modify them --- plugins/funder.c | 136 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 135 insertions(+), 1 deletion(-) diff --git a/plugins/funder.c b/plugins/funder.c index 7388a12696f4..97f87ba94410 100644 --- a/plugins/funder.c +++ b/plugins/funder.c @@ -591,6 +591,140 @@ static void json_channel_open_failed(struct command *cmd, unreserve_psbt(open); } +static void policy_to_json(struct json_stream *stream, + struct funder_policy *policy) +{ + json_add_string(stream, "summary", + funder_policy_desc(stream, current_policy)); + json_add_string(stream, "policy", + funder_opt_name(policy->opt)); + json_add_num(stream, "policy_mod", policy->mod); + json_add_amount_sat_only(stream, "min_their_funding", + policy->min_their_funding); + json_add_amount_sat_only(stream, "max_their_funding", + policy->max_their_funding); + json_add_amount_sat_only(stream, "per_channel_min", + policy->per_channel_min); + json_add_amount_sat_only(stream, "per_channel_max", + policy->per_channel_max); + json_add_amount_sat_only(stream, "reserve_tank", + policy->reserve_tank); + json_add_num(stream, "fuzz_percent", policy->fuzz_factor); + json_add_num(stream, "fund_probability", policy->fund_probability); +} + +static struct command_result * +param_funder_opt(struct command *cmd, const char *name, + const char *buffer, const jsmntok_t *tok, + enum funder_opt **opt) +{ + char *opt_str, *err; + + *opt = tal(cmd, enum funder_opt); + opt_str = tal_strndup(cmd, buffer + tok->start, + tok->end - tok->start); + + err = funding_option(opt_str, *opt); + if (err) + return command_fail_badparam(cmd, name, buffer, tok, err); + + return NULL; +} + + +static struct command_result * +param_policy_mod(struct command *cmd, const char *name, + const char *buffer, const jsmntok_t *tok, + u64 **mod) +{ + struct amount_sat sats; + char *arg_str, *err; + + *mod = tal(cmd, u64); + arg_str = tal_strndup(cmd, buffer + tok->start, + tok->end - tok->start); + + err = u64_option(arg_str, *mod); + if (err) { + if (!parse_amount_sat(&sats, arg_str, strlen(arg_str))) + return command_fail_badparam(cmd, name, + buffer, tok, err); + + **mod = sats.satoshis; /* Raw: convert to u64 */ + } + + return NULL; +} + +static struct command_result * +json_funderupdate(struct command *cmd, + const char *buf, + const jsmntok_t *params) +{ + struct json_stream *res; + struct amount_sat *min_their_funding, *max_their_funding, + *per_channel_min, *per_channel_max, + *reserve_tank; + u32 *fuzz_factor, *fund_probability; + u64 *mod; + enum funder_opt *opt; + const char *err; + struct funder_policy policy = current_policy; + + if (!param(cmd, buf, params, + p_opt("policy", param_funder_opt, &opt), + p_opt("policy_mod", param_policy_mod, &mod), + p_opt("min_their_funding", param_sat, &min_their_funding), + p_opt("max_their_funding", param_sat, &max_their_funding), + p_opt("per_channel_min", param_sat, &per_channel_min), + p_opt("per_channel_max", param_sat, &per_channel_max), + p_opt("reserve_tank", param_sat, &reserve_tank), + p_opt("fuzz_percent", param_number, &fuzz_factor), + p_opt("fund_probability", param_number, &fund_probability), + NULL)) + return command_param_failed(); + + if (opt) + policy.opt = *opt; + if (mod) + policy.mod = *mod; + if (min_their_funding) + policy.min_their_funding = *min_their_funding; + if (max_their_funding) + policy.max_their_funding = *max_their_funding; + if (per_channel_min) + policy.per_channel_min = *per_channel_min; + if (per_channel_max) + policy.per_channel_max = *per_channel_max; + if (reserve_tank) + policy.reserve_tank = *reserve_tank; + if (fuzz_factor) + policy.fuzz_factor = *fuzz_factor; + if (fund_probability) + policy.fund_probability = *fund_probability; + + err = funder_check_policy(&policy); + if (err) + return command_done_err(cmd, JSONRPC2_INVALID_PARAMS, + err, NULL); + + current_policy = policy; + res = jsonrpc_stream_success(cmd); + policy_to_json(res, ¤t_policy); + return command_finished(cmd, res); +} + +static const struct plugin_command commands[] = { { + "funderupdate", + "channels", + "Update configuration for dual-funding offer", + "Update current funder settings. Modifies how node" + " reacts to incoming channel open requests. Responds with list" + " of current configs.", + json_funderupdate + } +}; + static const char *init(struct plugin *p, const char *b, const jsmntok_t *t) { const char *err; @@ -668,7 +802,7 @@ int main(int argc, char **argv) plugin_main(argv, init, PLUGIN_RESTARTABLE, true, NULL, - NULL, 0, + commands, ARRAY_SIZE(commands), notifs, ARRAY_SIZE(notifs), hooks, ARRAY_SIZE(hooks), plugin_option("funder-policy", From d97ef4d195cd0a7a55cc73bc79a8b9b729047925 Mon Sep 17 00:00:00 2001 From: niftynei Date: Fri, 23 Apr 2021 17:19:56 -0500 Subject: [PATCH 17/20] rbf_channel hook: add channel_max_msat parameter Changelog-Added: Plugins: `rbf_channel` hook has `channel_max_msat` parameter --- doc/PLUGINS.md | 3 ++- lightningd/dual_open_control.c | 11 +++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/doc/PLUGINS.md b/doc/PLUGINS.md index db4515a21749..ac5c5932770f 100644 --- a/doc/PLUGINS.md +++ b/doc/PLUGINS.md @@ -1157,7 +1157,8 @@ requests an RBF for a channel funding transaction. "funding_feerate_per_kw": 7500, "feerate_our_max": 10000, "feerate_our_min": 253, - "locktime": 2453, + "channel_max_msat": "16777215000msat", + "locktime": 2453 } } ``` diff --git a/lightningd/dual_open_control.c b/lightningd/dual_open_control.c index 6663a1a311b5..8203bd5d744f 100644 --- a/lightningd/dual_open_control.c +++ b/lightningd/dual_open_control.c @@ -183,6 +183,9 @@ struct rbf_channel_payload { /* General info */ u32 feerate_our_max; u32 feerate_our_min; + /* What's the maximum amount of funding + * this channel can hold */ + struct amount_sat channel_max; /* Returned from hook */ struct amount_sat our_funding; @@ -206,6 +209,8 @@ rbf_channel_hook_serialize(struct rbf_channel_payload *payload, payload->feerate_our_min); json_add_num(stream, "funding_feerate_per_kw", payload->funding_feerate_per_kw); + json_add_amount_sat_only(stream, "channel_max_msat", + payload->channel_max); json_object_end(stream); } @@ -1649,6 +1654,12 @@ static void rbf_got_offer(struct subd *dualopend, const u8 *msg) /* No error message known (yet) */ payload->err_msg = NULL; + payload->channel_max = chainparams->max_funding; + if (feature_negotiated(dualopend->ld->our_features, + channel->peer->their_features, + OPT_LARGE_CHANNELS)) + payload->channel_max = AMOUNT_SAT(UINT_MAX); + tal_add_destructor2(dualopend, rbf_channel_remove_dualopend, payload); plugin_hook_call_rbf_channel(dualopend->ld, payload); } From 1e0f05f2dbd6cedd11375dbb1f5aae89cc51442d Mon Sep 17 00:00:00 2001 From: niftynei Date: Fri, 23 Apr 2021 17:20:44 -0500 Subject: [PATCH 18/20] funder: handle RBF callback Fund an RBF same as you would an open. We dont' do anything fancy with our inputs -- we dont' have a copy of the last PSBT that was sent. --- plugins/funder.c | 71 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/plugins/funder.c b/plugins/funder.c index 97f87ba94410..7c62bd5457ac 100644 --- a/plugins/funder.c +++ b/plugins/funder.c @@ -539,6 +539,73 @@ json_openchannel2_call(struct command *cmd, return send_outreq(cmd->plugin, req); } +/* Peer has asked us to RBF */ +static struct command_result * +json_rbf_channel_call(struct command *cmd, + const char *buf, + const jsmntok_t *params) +{ + struct open_info *info = tal(cmd, struct open_info); + u64 feerate_our_max, feerate_our_min; + const char *err; + struct out_req *req; + + err = json_scan(tmpctx, buf, params, + "{rbf_channel:" + "{id:%" + ",channel_id:%" + ",their_funding:%" + ",funding_feerate_per_kw:%" + ",feerate_our_max:%" + ",feerate_our_min:%" + ",locktime:%}}", + JSON_SCAN(json_to_node_id, &info->id), + JSON_SCAN(json_to_channel_id, &info->cid), + JSON_SCAN(json_to_sat, &info->their_funding), + JSON_SCAN(json_to_u64, &info->funding_feerate_perkw), + JSON_SCAN(json_to_u64, &feerate_our_max), + JSON_SCAN(json_to_u64, &feerate_our_min), + JSON_SCAN(json_to_u32, &info->locktime)); + + if (err) + plugin_err(cmd->plugin, + "`rbf_channel` payload did not scan %s: %.*s", + err, json_tok_full_len(params), + json_tok_full(buf, params)); + + /* If there's no channel_max, it's actually infinity */ + err = json_scan(tmpctx, buf, params, + "{rbf_channel:{channel_max_msat:%}}", + JSON_SCAN(json_to_sat, &info->channel_max)); + if (err) + info->channel_max = AMOUNT_SAT(UINT64_MAX); + + /* We don't fund anything that's above or below our feerate */ + if (info->funding_feerate_perkw < feerate_our_min + || info->funding_feerate_perkw > feerate_our_max) { + + plugin_log(cmd->plugin, LOG_DBG, + "their feerate %"PRIu64" is out of" + " our bounds (%"PRIu64"-%"PRIu64")", + info->funding_feerate_perkw, + feerate_our_min, + feerate_our_max); + + return command_hook_success(cmd); + } + + /* Figure out what our funds are... same flow + * as with openchannel2 callback. We assume that THEY + * will use the same inputs, so we use whatever we want here */ + req = jsonrpc_request_start(cmd->plugin, cmd, + "listfunds", + &listfunds_success, + &listfunds_failed, + info); + + return send_outreq(cmd->plugin, req); +} + static void json_disconnect(struct command *cmd, const char *buf, const jsmntok_t *params) @@ -751,6 +818,10 @@ const struct plugin_hook hooks[] = { "openchannel2_sign", json_openchannel2_sign_call, }, + { + "rbf_channel", + json_rbf_channel_call, + }, }; const struct plugin_notification notifs[] = { From 884f1d4b124b21b4ffbb4a7fb87f58bacc1d5089 Mon Sep 17 00:00:00 2001 From: niftynei Date: Thu, 29 Apr 2021 11:28:17 -0500 Subject: [PATCH 19/20] pyln-testing: use provided outnum instead of trying to find it Dual-funded channels won't match the old amount check. Good news is we're already returning the outnum so we just use that. --- contrib/pyln-testing/pyln/testing/utils.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/contrib/pyln-testing/pyln/testing/utils.py b/contrib/pyln-testing/pyln/testing/utils.py index dff897eb6b52..6f57506efbfb 100644 --- a/contrib/pyln-testing/pyln/testing/utils.py +++ b/contrib/pyln-testing/pyln/testing/utils.py @@ -864,8 +864,7 @@ def has_funds_on_addr(addr): txnum = i scid = "{}x{}x{}".format(self.bitcoin.rpc.getblockcount(), - txnum, - get_tx_p2wsh_outnum(self.bitcoin, res['tx'], amount)) + txnum, res['outnum']) if wait_for_active: self.wait_channel_active(scid) From de9102a270814b6e57592420bfc1a2c30b6dca9e Mon Sep 17 00:00:00 2001 From: niftynei Date: Tue, 27 Apr 2021 16:12:14 -0500 Subject: [PATCH 20/20] tests: add test for funder options --- tests/test_opening.py | 57 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/tests/test_opening.py b/tests/test_opening.py index 1703f54df3b3..15f96d4adf42 100644 --- a/tests/test_opening.py +++ b/tests/test_opening.py @@ -699,3 +699,60 @@ def test_rbf_no_overlap(node_factory, bitcoind, chainparams): with pytest.raises(RpcError, match='No overlapping input present.'): l1.rpc.openchannel_update(chan_id, bump['psbt']) + + +@unittest.skipIf(TEST_NETWORK != 'regtest', 'elementsd doesnt yet support PSBT features we need') +def test_funder_options(node_factory, bitcoind): + l1, l2, l3 = node_factory.get_nodes(3, opts={'experimental-dual-fund': None}) + l1.fundwallet(10**7) + + # Check the default options + funder_opts = l1.rpc.call('funderupdate') + + assert funder_opts['policy'] == 'fixed' + assert funder_opts['policy_mod'] == 0 + assert funder_opts['min_their_funding'] == '10000000msat' + assert funder_opts['max_their_funding'] == '4294967295000msat' + assert funder_opts['per_channel_min'] == '10000000msat' + assert funder_opts['per_channel_max'] == '4294967295000msat' + assert funder_opts['reserve_tank'] == '0msat' + assert funder_opts['fuzz_percent'] == 5 + assert funder_opts['fund_probability'] == 100 + + # l2 funds a chanenl with us. We don't contribute + l2.rpc.connect(l1.info['id'], 'localhost', l1.port) + l2.fundchannel(l1, 10**6) + chan_info = only_one(only_one(l2.rpc.listpeers(l1.info['id'])['peers'])['channels']) + # l1 contributed nothing + assert chan_info['funding_msat'][l1.info['id']] == '0msat' + + # Change all the options + funder_opts = l1.rpc.call('funderupdate', + {'policy': 'available', + 'policy_mod': 100, + 'min_their_funding': '100000msat', + 'max_their_funding': '2000000000msat', + 'per_channel_min': '8000000msat', + 'per_channel_max': '10000000000msat', + 'reserve_tank': '3000000msat', + 'fund_probability': 99, + 'fuzz_percent': 0}) + + assert funder_opts['policy'] == 'available' + assert funder_opts['policy_mod'] == 100 + assert funder_opts['min_their_funding'] == '100000msat' + assert funder_opts['max_their_funding'] == '2000000000msat' + assert funder_opts['per_channel_min'] == '8000000msat' + assert funder_opts['per_channel_max'] == '10000000000msat' + assert funder_opts['reserve_tank'] == '3000000msat' + assert funder_opts['fuzz_percent'] == 0 + assert funder_opts['fund_probability'] == 99 + + # Set the fund probability back up to 100. + funder_opts = l1.rpc.call('funderupdate', + {'fund_probability': 100}) + l3.rpc.connect(l1.info['id'], 'localhost', l1.port) + l3.fundchannel(l1, 10**6) + chan_info = only_one(only_one(l3.rpc.listpeers(l1.info['id'])['peers'])['channels']) + # l1 contributed everything + assert chan_info['funding_msat'][l1.info['id']] != '0msat'