Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .msggen.json
Original file line number Diff line number Diff line change
Expand Up @@ -3144,6 +3144,7 @@
"Offer.absolute_expiry": 6,
"Offer.amount": 1,
"Offer.description": 2,
"Offer.fronting_nodes[]": 15,
"Offer.issuer": 3,
"Offer.label": 4,
"Offer.optional_recurrence": 14,
Expand Down Expand Up @@ -11328,6 +11329,10 @@
"added": "pre-v0.10.1",
"deprecated": null
},
"Offer.fronting_nodes[]": {
"added": "v25.12",
"deprecated": null
},
"Offer.issuer": {
"added": "pre-v0.10.1",
"deprecated": null
Expand Down
1 change: 1 addition & 0 deletions cln-grpc/proto/node.proto

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions cln-grpc/src/convert.rs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions cln-rpc/src/model.rs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions contrib/msggen/msggen/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -26796,6 +26796,16 @@
"Make recurrence optional, for backwards compatibility (older payers will only pay once)."
]
},
"fronting_nodes": {
"added": "v25.12",
"type": "array",
"items": {
"type": "pubkey"
},
"description": [
"An optional array of peer nodes to create blinded paths from. One of these blinded paths will also be used for the invoice, when they request it."
]
},
"dev_paths": {
"hidden": true
}
Expand Down
3 changes: 2 additions & 1 deletion contrib/pyln-client/pyln/client/lightning.py
Original file line number Diff line number Diff line change
Expand Up @@ -1129,7 +1129,7 @@ def newaddr(self, addresstype=None):

def offer(self, amount, description=None, issuer=None, label=None, quantity_max=None, absolute_expiry=None,
recurrence=None, recurrence_base=None, recurrence_paywindow=None, recurrence_limit=None,
single_use=None):
single_use=None, fronting_nodes=None):
"""
Create an offer (or returns an existing one), which is a precursor to creating one or more invoices.
It automatically enables the processing of an incoming invoice_request, and issuing of invoices.
Expand All @@ -1146,6 +1146,7 @@ def offer(self, amount, description=None, issuer=None, label=None, quantity_max=
"recurrence_paywindow": recurrence_paywindow,
"recurrence_limit": recurrence_limit,
"single_use": single_use,
"fronting_nodes": fronting_nodes,
}
return self.call("offer", payload)

Expand Down
1,112 changes: 556 additions & 556 deletions contrib/pyln-grpc-proto/pyln/grpc/node_pb2.py

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions doc/lightningd-config.5.md
Original file line number Diff line number Diff line change
Expand Up @@ -518,6 +518,10 @@ delete the others.

### Payment and invoice control options:

* **payment-fronting-node**=*nodeid*

Always use this *nodeid* as the entry point when we generate invoices or offers. For BOLT11 invoices, this node must be a neighbor: we will use a routehint with the alias for the short channel id to provide limited privacy (we still reveal our node id). For BOLT12 invoices and offers , we provide a blinded path from the node to us, to provide better privacy.

* **disable-mpp** [plugin `pay`]

Disable the multi-part payment sending support in the `pay` plugin. By default
Expand Down
10 changes: 10 additions & 0 deletions doc/schemas/offer.json
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,16 @@
"Make recurrence optional, for backwards compatibility (older payers will only pay once)."
]
},
"fronting_nodes": {
"added": "v25.12",
"type": "array",
"items": {
"type": "pubkey"
},
"description": [
"An optional array of peer nodes to create blinded paths from. One of these blinded paths will also be used for the invoice, when they request it."
]
},
"dev_paths": {
"hidden": true
}
Expand Down
25 changes: 24 additions & 1 deletion lightningd/invoice.c
Original file line number Diff line number Diff line change
Expand Up @@ -662,6 +662,25 @@ static struct route_info **select_inchan_mpp(const tal_t *ctx,
return routehints;
}

static struct route_info **select_inchan_all(const tal_t *ctx,
struct lightningd *ld,
struct routehint_candidate
*candidates)
{
struct route_info **routehints;

log_debug(ld->log, "Selecting all %zu candidates",
tal_count(candidates));

routehints = tal_arr(ctx, struct route_info *, tal_count(candidates));
for (size_t i = 0; i < tal_count(candidates); i++) {
routehints[i] = tal_dup(routehints, struct route_info,
candidates[i].r);
}

return routehints;
}

/* Encapsulating struct while we wait for gossipd to give us incoming channels */
struct chanhints {
bool expose_all_private;
Expand Down Expand Up @@ -723,6 +742,10 @@ add_routehints(struct invoice_info *info,

needed = info->b11->msat ? *info->b11->msat : AMOUNT_MSAT(1);

/* --payment-fronting-node means use all candidates. */
if (tal_count(info->cmd->ld->fronting_nodes))
info->b11->routes = select_inchan_all(info->b11, info->cmd->ld, candidates);

/* If we are not completely unpublished, try with reservoir
* sampling first.
*
Expand All @@ -738,7 +761,7 @@ add_routehints(struct invoice_info *info,
* should make an effort to avoid overlapping incoming
* channels, which is done by select_inchan_mpp.
*/
if (!node_unpublished)
else if (!node_unpublished)
info->b11->routes = select_inchan(info->b11,
info->cmd->ld,
needed,
Expand Down
1 change: 1 addition & 0 deletions lightningd/lightningd.c
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,7 @@ static struct lightningd *new_lightningd(const tal_t *ctx)
/* The gossip seeker automatically connects to a this many peers */
ld->autoconnect_seeker_peers = 10;

ld->fronting_nodes = tal_arr(ld, struct node_id, 0);
return ld;
}

Expand Down
3 changes: 3 additions & 0 deletions lightningd/lightningd.h
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,9 @@ struct lightningd {

/* Minimum number of peers seeker should maintain. */
u32 autoconnect_seeker_peers;

/* Nodes to use for invoices / offers */
struct node_id *fronting_nodes;
};

/* Turning this on allows a tal allocation to return NULL, rather than aborting.
Expand Down
15 changes: 15 additions & 0 deletions lightningd/options.c
Original file line number Diff line number Diff line change
Expand Up @@ -1265,6 +1265,16 @@ static char *opt_add_api_beg(const char *arg, struct lightningd *ld)
return NULL;
}

static char *opt_add_node_id(const char *arg, struct node_id **arr)
{
struct node_id n;
if (!node_id_from_hexstr(arg, strlen(arg), &n))
return "Unparsable nodeid";

tal_arr_expand(arr, n);
return NULL;
}

char *hsm_secret_arg(const tal_t *ctx,
const char *arg,
const u8 **hsm_secret)
Expand Down Expand Up @@ -1578,6 +1588,10 @@ static void register_opts(struct lightningd *ld)
ld,
"Re-enable a long-deprecated API (which will be removed entirely next version!)");
opt_register_logging(ld);
clnopt_witharg("--payment-fronting-node",
OPT_MULTI,
opt_add_node_id, NULL,
&ld->fronting_nodes, "Put this node in all invoices and offers, and use blinded path (bolt12) or route hints (bolt11) to route to this node. Must be a neighboring node. Can be specified multiple times.");

/* Old bookkeeper migration flags. */
opt_register_early_arg("--bookkeeper-dir",
Expand Down Expand Up @@ -1844,6 +1858,7 @@ bool is_known_opt_cb_arg(char *(*cb_arg)(const char *, void *))
|| cb_arg == (void *)opt_subd_dev_disconnect
|| cb_arg == (void *)opt_set_crash_timeout
|| cb_arg == (void *)opt_add_api_beg
|| cb_arg == (void *)opt_add_node_id
|| cb_arg == (void *)opt_force_featureset
|| cb_arg == (void *)opt_force_privkey
|| cb_arg == (void *)opt_force_bip32_seed
Expand Down
8 changes: 2 additions & 6 deletions lightningd/plugin.c
Original file line number Diff line number Diff line change
Expand Up @@ -2128,12 +2128,8 @@ void plugins_init(struct plugins *plugins)
setenv("LIGHTNINGD_PLUGIN", "1", 1);
setenv("LIGHTNINGD_VERSION", version(), 1);

if (plugins_send_getmanifest(plugins, NULL)) {
void *ret;
ret = io_loop_with_timers(plugins->ld);
log_debug(plugins->ld->log, "io_loop_with_timers: %s", __func__);
assert(ret == plugins);
}
if (plugins_send_getmanifest(plugins, NULL))
io_loop_with_timers(plugins->ld);
}

static void plugin_config_cb(const char *buffer,
Expand Down
33 changes: 29 additions & 4 deletions lightningd/routehint.c
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,16 @@ static bool scid_in_arr(const struct short_channel_id *scidarr,
return false;
}

static bool is_fronting_node(const struct lightningd *ld,
const struct node_id *node)
{
for (size_t i = 0; i < tal_count(ld->fronting_nodes); i++) {
if (node_id_eq(&ld->fronting_nodes[i], node))
return true;
}
return false;
}

struct routehint_candidate *
routehint_candidates(const tal_t *ctx,
struct lightningd *ld,
Expand Down Expand Up @@ -62,7 +72,7 @@ routehint_candidates(const tal_t *ctx,
struct routehint_candidate candidate;
struct amount_msat fee_base, htlc_max;
struct route_info *r;
bool is_public;
bool is_public, is_fronting;

r = tal(tmpctx, struct route_info);

Expand Down Expand Up @@ -92,6 +102,17 @@ routehint_candidates(const tal_t *ctx,
json_tok_full(buf, toks));
}

/* If they specify fronting nodes, always use them. */
if (tal_count(ld->fronting_nodes)) {
if (!is_fronting_node(ld, &r->pubkey)) {
log_debug(ld->log, "%s: not a fronting node",
fmt_node_id(tmpctx, &r->pubkey));
continue;
}
is_fronting = true;
} else
is_fronting = false;

/* Note: listincoming returns real scid or local alias if no real scid. */
candidate.c = any_channel_by_scid(ld, r->short_channel_id, true);
if (!candidate.c) {
Expand Down Expand Up @@ -133,6 +154,10 @@ routehint_candidates(const tal_t *ctx,
if (expose_all_private != NULL && *expose_all_private)
is_public = true;

/* Also, consider fronting nodes public */
if (is_fronting)
is_public = true;

r->fee_base_msat = fee_base.millisatoshis; /* Raw: route_info */
/* Could wrap: if so ignore */
if (!amount_msat_eq(amount_msat(r->fee_base_msat), fee_base)) {
Expand All @@ -156,7 +181,7 @@ routehint_candidates(const tal_t *ctx,
continue;
}
/* If they give us a hint, we use even if capacity 0 */
} else if (amount_msat_is_zero(capacity)) {
} else if (!is_fronting && amount_msat_is_zero(capacity)) {
log_debug(ld->log, "%s: deadend",
fmt_short_channel_id(tmpctx,
r->short_channel_id));
Expand All @@ -166,8 +191,8 @@ routehint_candidates(const tal_t *ctx,
continue;
}

/* Is it offline? */
if (candidate.c->owner == NULL) {
/* Is it offline? Leave it if it's fronting. */
if (!is_fronting && candidate.c->owner == NULL) {
log_debug(ld->log, "%s: offline",
fmt_short_channel_id(tmpctx,
r->short_channel_id));
Expand Down
Loading
Loading