mirror of
https://github.com/aljazceru/lightning.git
synced 2026-02-03 21:24:22 +01:00
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
This commit is contained in:
1
plugins/.gitignore
vendored
1
plugins/.gitignore
vendored
@@ -1,5 +1,6 @@
|
||||
autoclean
|
||||
bcli
|
||||
funder
|
||||
pay
|
||||
spenderp
|
||||
multifundchannel
|
||||
|
||||
@@ -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
|
||||
|
||||
629
plugins/funder.c
Normal file
629
plugins/funder.c
Normal file
@@ -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 <bitcoin/feerate.h>
|
||||
#include <bitcoin/psbt.h>
|
||||
#include <ccan/array_size/array_size.h>
|
||||
#include <ccan/list/list.h>
|
||||
#include <ccan/tal/str/str.h>
|
||||
#include <common/channel_id.h>
|
||||
#include <common/json.h>
|
||||
#include <common/json_helpers.h>
|
||||
#include <common/json_stream.h>
|
||||
#include <common/node_id.h>
|
||||
#include <common/psbt_open.h>
|
||||
#include <common/type_to_string.h>
|
||||
#include <common/utils.h>
|
||||
#include <plugins/funder_policy.h>
|
||||
#include <plugins/libplugin.h>
|
||||
|
||||
/* 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;
|
||||
}
|
||||
204
plugins/funder_policy.c
Normal file
204
plugins/funder_policy.c
Normal file
@@ -0,0 +1,204 @@
|
||||
#include <assert.h>
|
||||
#include <ccan/tal/str/str.h>
|
||||
#include <common/node_id.h>
|
||||
#include <common/pseudorand.h>
|
||||
#include <common/type_to_string.h>
|
||||
#include <inttypes.h>
|
||||
#include <plugins/funder_policy.h>
|
||||
|
||||
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;
|
||||
}
|
||||
92
plugins/funder_policy.h
Normal file
92
plugins/funder_policy.h
Normal file
@@ -0,0 +1,92 @@
|
||||
#ifndef LIGHTNING_PLUGINS_FUNDER_POLICY_H
|
||||
#define LIGHTNING_PLUGINS_FUNDER_POLICY_H
|
||||
#include "config.h"
|
||||
#include <common/amount.h>
|
||||
|
||||
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 */
|
||||
Reference in New Issue
Block a user