From 0180ad9ba52103668aa52941be0cca02f0dd31b5 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Wed, 7 Aug 2024 11:19:52 +0930 Subject: [PATCH 01/28] doc: document askrene. It's experimental, so API may change, but it definitely does need explanation! Signed-off-by: Rusty Russell --- contrib/msggen/msggen/schema.json | 828 ++++++++++++++++++ doc/Makefile | 7 + doc/index.rst | 7 + doc/schemas/lightning-askrene-age.json | 63 ++ .../lightning-askrene-create-channel.json | 105 +++ .../lightning-askrene-disable-node.json | 49 ++ .../lightning-askrene-inform-channel.json | 111 +++ doc/schemas/lightning-askrene-listlayers.json | 182 ++++ doc/schemas/lightning-askrene-reserve.json | 72 ++ doc/schemas/lightning-askrene-unreserve.json | 72 ++ doc/schemas/lightning-getroutes.json | 174 ++++ 11 files changed, 1670 insertions(+) create mode 100644 doc/schemas/lightning-askrene-age.json create mode 100644 doc/schemas/lightning-askrene-create-channel.json create mode 100644 doc/schemas/lightning-askrene-disable-node.json create mode 100644 doc/schemas/lightning-askrene-inform-channel.json create mode 100644 doc/schemas/lightning-askrene-listlayers.json create mode 100644 doc/schemas/lightning-askrene-reserve.json create mode 100644 doc/schemas/lightning-askrene-unreserve.json create mode 100644 doc/schemas/lightning-getroutes.json diff --git a/contrib/msggen/msggen/schema.json b/contrib/msggen/msggen/schema.json index 72b219bdc372..7dc35385a9fe 100644 --- a/contrib/msggen/msggen/schema.json +++ b/contrib/msggen/msggen/schema.json @@ -192,6 +192,660 @@ } ] }, + "lightning-askrene-age.json": { + "$schema": "../rpc-schema-draft.json", + "type": "object", + "additionalProperties": false, + "rpc": "askrene-age", + "title": "Command for expiring information in a layer (EXPERIMENTAL)", + "description": [ + "WARNING: experimental, so API may change.", + "", + "The **askrene-age** RPC command tells askrene that information added to a layer by *askrene-inform-channel* beyond a certain age is less useful. It currently completely forgets constraints older than *cutoff*." + ], + "request": { + "required": [ + "layer", + "cutoff" + ], + "properties": { + "layer": { + "type": "string", + "description": [ + "The name of the layer to apply this change to." + ] + }, + "cutoff": { + "type": "u64", + "description": [ + "The UNIX timestamp: constraints older than this will be forgotten." + ] + } + } + }, + "response": { + "required": [ + "layer", + "num_removed" + ], + "properties": { + "layer": { + "type": "string", + "description": [ + "The *layer* parameter provided." + ] + }, + "num_removed": { + "type": "u64", + "description": [ + "The number of constraints removed from *layer*" + ] + } + } + }, + "see_also": [ + "lightning-getroutes(7)", + "lightning-askrene-inform-channel(7)", + "lightning-askrene-listlayers(7)" + ], + "author": [ + "Rusty Russell <> is mainly responsible." + ], + "resources": [ + "Main web site: " + ] + }, + "lightning-askrene-create-channel.json": { + "$schema": "../rpc-schema-draft.json", + "type": "object", + "additionalProperties": false, + "rpc": "askrene-create-channel", + "title": "Command to add a channel to layer (EXPERIMENTAL)", + "description": [ + "WARNING: experimental, so API may change.", + "", + "The **askrene-create-channel** RPC command tells askrene to populate one direction of a channel in the given layer. If the channel already exists, it will be overridden. If the layer does not exist, it will be created." + ], + "request": { + "required": [ + "layer", + "source", + "destination", + "short_channel_id", + "capacity_msat", + "htlc_min", + "htlc_max", + "base_fee", + "proportional_fee", + "delay" + ], + "properties": { + "layer": { + "type": "string", + "description": [ + "The name of the layer to apply this change to." + ] + }, + "source": { + "type": "pubkey", + "description": [ + "The source node id for the channel." + ] + }, + "destination": { + "type": "pubkey", + "description": [ + "The destination node id for the channel." + ] + }, + "short_channel_id": { + "type": "short_channel_id", + "description": [ + "The short channel id for the channel. If a channel with this short channel id already exists in *layer*, the *source*, *destination* and *capacity_msat* must be the same." + ] + }, + "capacity_msat": { + "type": "msat", + "description": [ + "The capacity (onchain size) of the channel." + ] + }, + "htlc_min": { + "type": "msat", + "description": [ + "The minimum value allowed in this direction." + ] + }, + "htlc_max": { + "type": "msat", + "description": [ + "The maximum value allowed in this direction." + ] + }, + "base_fee": { + "type": "msat", + "description": [ + "The base fee to apply to use the channel in this direction." + ] + }, + "proportional_fee": { + "type": "u32", + "description": [ + "The proportional fee (in parts per million) to apply to use the channel in this direction." + ] + }, + "delay": { + "type": "u16", + "description": [ + "The CLTV delay required for this direction." + ] + } + } + }, + "response": { + "required": [], + "properties": {} + }, + "see_also": [ + "lightning-getroutes(7)", + "lightning-askrene-disable-node(7)", + "lightning-askrene-inform-channel(7)", + "lightning-askrene-listlayers(7)", + "lightning-askrene-age(7)" + ], + "author": [ + "Rusty Russell <> is mainly responsible." + ], + "resources": [ + "Main web site: " + ] + }, + "lightning-askrene-disable-node.json": { + "$schema": "../rpc-schema-draft.json", + "type": "object", + "additionalProperties": false, + "rpc": "askrene-disable-node", + "title": "Command to disable all channels to/from a node in a layer (EXPERIMENTAL)", + "description": [ + "WARNING: experimental, so API may change.", + "", + "The **askrene-disable-node** RPC command tells askrene to disable all channels connected to a node whenever the given layer is used. This is mainly useful to force the use of alternate paths: while individual channels can be disabled using askrene-create-channel or askrene-inform-channel, that would be racy if new channels appeared." + ], + "request": { + "required": [ + "layer", + "node" + ], + "properties": { + "layer": { + "type": "string", + "description": [ + "The name of the layer to apply this change to." + ] + }, + "node": { + "type": "pubkey", + "description": [ + "The node to disable. It does not need to exist." + ] + } + } + }, + "response": { + "required": [], + "properties": {} + }, + "see_also": [ + "lightning-getroutes(7)", + "lightning-askrene-create-channel(7)", + "lightning-askrene-inform-channel(7)", + "lightning-askrene-listlayers(7)", + "lightning-askrene-age(7)" + ], + "author": [ + "Rusty Russell <> is mainly responsible." + ], + "resources": [ + "Main web site: " + ] + }, + "lightning-askrene-inform-channel.json": { + "$schema": "../rpc-schema-draft.json", + "type": "object", + "additionalProperties": false, + "rpc": "askrene-inform-channel", + "title": "Command to add channel capacity restrictions to layer (EXPERIMENTAL)", + "description": [ + "WARNING: experimental, so API may change.", + "", + "The **askrene-inform-channel** RPC command tells askrene about the minimum or maximum current capacity of a given channel. It can be applied whether the curren channel exists or not. If the layer does not exist, it will be created." + ], + "request": { + "required": [ + "layer", + "short_channel_id", + "direction" + ], + "properties": { + "layer": { + "type": "string", + "description": [ + "The name of the layer to apply this change to." + ] + }, + "short_channel_id": { + "type": "short_channel_id", + "description": [ + "The short channel id to apply this change to." + ] + }, + "direction": { + "type": "u32", + "description": [ + "The direction to apply this change to." + ] + }, + "maximum_msat": { + "type": "msat", + "description": [ + "The maximum value which this channel could pass. This or *minimum_msat* must be specified, but not both." + ] + }, + "minimum_msat": { + "type": "msat", + "description": [ + "The minumum value which this channel could pass. This or *minimum_msat* must be specified, but not both." + ] + } + } + }, + "response": { + "required": [ + "constraint" + ], + "properties": { + "constraint": { + "type": "object", + "required": [ + "short_channel_id", + "direction", + "timestamp" + ], + "properties": { + "short_channel_id": { + "type": "short_channel_id", + "description": [ + "The *short_channel_id* specified." + ] + }, + "direction": { + "type": "u32", + "description": [ + "The *direction* specified." + ] + }, + "timestamp": { + "type": "u64", + "description": [ + "The UNIX time (seconds since 1970) this was created." + ] + }, + "maximum_msat": { + "type": "msat", + "description": [ + "The *minimum_msat* (if specified)" + ] + }, + "minimum_msat": { + "type": "msat", + "description": [ + "The *maximum_msat* (if specified)" + ] + } + } + } + } + }, + "see_also": [ + "lightning-getroutes(7)", + "lightning-askrene-disable-node(7)", + "lightning-askrene-create-channel(7)", + "lightning-askrene-listlayers(7)", + "lightning-askrene-age(7)" + ], + "author": [ + "Rusty Russell <> is mainly responsible." + ], + "resources": [ + "Main web site: " + ] + }, + "lightning-askrene-listlayers.json": { + "$schema": "../rpc-schema-draft.json", + "type": "object", + "additionalProperties": false, + "rpc": "askrene-listlayers", + "title": "Command to display information about layers (EXPERIMENTAL)", + "description": [ + "WARNING: experimental, so API may change.", + "", + "The **askrene-listlayers** RPC command reports any modifications each layer (or, the layer specified) would make to the topology, if it were used for *getroutes*." + ], + "request": { + "required": [], + "properties": { + "layer": { + "type": "string", + "description": [ + "The name of the layer to report on." + ] + } + } + }, + "response": { + "required": [ + "layers" + ], + "properties": { + "layers": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "layer", + "disabled_nodes", + "created_channels", + "constraints" + ], + "properties": { + "layer": { + "type": "string", + "description": [ + "The name of the layer." + ] + }, + "disabled_nodes": { + "type": "array", + "items": { + "type": "pubkey", + "description": [ + "The id of the disabled node." + ] + } + }, + "created_channels": { + "type": "array", + "items": { + "type": "object", + "required": [ + "source", + "destination", + "short_channel_id", + "capacity_msat", + "htlc_minimum_msat", + "htlc_maximum_msat", + "fee_base_msat", + "fee_proportional_millionths", + "delay" + ], + "properties": { + "source": { + "type": "pubkey", + "description": [ + "The source node id for the channel." + ] + }, + "destination": { + "type": "pubkey", + "description": [ + "The destination node id for the channel." + ] + }, + "short_channel_id": { + "type": "short_channel_id", + "description": [ + "The short channel id for the channel." + ] + }, + "capacity_msat": { + "type": "msat", + "description": [ + "The capacity (onchain size) of the channel." + ] + }, + "htlc_minimum_msat": { + "type": "msat", + "description": [ + "The minimum value allowed in this direction." + ] + }, + "htlc_maximum_msat": { + "type": "msat", + "description": [ + "The maximum value allowed in this direction." + ] + }, + "fee_base_msat": { + "type": "msat", + "description": [ + "The base fee to apply to use the channel in this direction." + ] + }, + "fee_proportional_millionths": { + "type": "u32", + "description": [ + "The proportional fee (in parts per million) to apply to use the channel in this direction." + ] + }, + "delay": { + "type": "u16", + "description": [ + "The CLTV delay required for this direction." + ] + } + } + } + }, + "constraints": { + "type": "array", + "items": { + "type": "object", + "required": [ + "short_channel_id", + "direction" + ], + "properties": { + "short_channel_id": { + "type": "short_channel_id", + "description": [ + "The short channel id." + ] + }, + "direction": { + "type": "u32", + "description": [ + "The direction." + ] + }, + "maximum_msat": { + "type": "msat", + "description": [ + "The maximum value which this channel could pass. This or *minimum_msat* will be present, but not both." + ] + }, + "minimum_msat": { + "type": "msat", + "description": [ + "The minimum value which this channel could pass. This or *minimum_msat* will be present, but not both." + ] + } + } + } + } + } + } + } + } + }, + "see_also": [ + "lightning-getroutes(7)", + "lightning-askrene-disable-node(7)", + "lightning-askrene-create-channel(7)", + "lightning-askrene-inform-channel(7)", + "lightning-askrene-age(7)" + ], + "author": [ + "Rusty Russell <> is mainly responsible." + ], + "resources": [ + "Main web site: " + ] + }, + "lightning-askrene-reserve.json": { + "$schema": "../rpc-schema-draft.json", + "type": "object", + "additionalProperties": false, + "rpc": "askrene-reserve", + "title": "Command for informing askrene that you are trying a path (EXPERIMENTAL)", + "description": [ + "WARNING: experimental, so API may change.", + "", + "The **askrene-reserve** RPC command tells askrene that a path is being attempted. This allows it to take that into account when other *getroutes* calls are made. You should call **askrene-unreserve** after the attempt has completed.", + "", + "Note that additional properties inside the *path* elements are ignored, which is useful when used with the result of *getroutes*." + ], + "request": { + "required": [ + "path" + ], + "properties": { + "path": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": true, + "required": [ + "short_channel_id", + "direction", + "amount_msat" + ], + "properties": { + "short_channel_id": { + "type": "short_channel_id", + "description": [ + "The channel joining these nodes." + ] + }, + "direction": { + "type": "u32", + "description": [ + "0 if this channel is traversed from lesser to greater **id**, otherwise 1." + ] + }, + "amount_msat": { + "type": "msat", + "description": [ + "The amount to send into this hop." + ] + } + } + } + } + } + }, + "response": { + "required": [], + "properties": {} + }, + "see_also": [ + "lightning-getroutes(7)", + "lightning-askrene-unreserve(7)", + "lightning-askrene-disable-node(7)", + "lightning-askrene-create-channel(7)", + "lightning-askrene-inform-channel(7)", + "lightning-askrene-listlayers(7)", + "lightning-askrene-age(7)" + ], + "author": [ + "Rusty Russell <> is mainly responsible." + ], + "resources": [ + "Main web site: " + ] + }, + "lightning-askrene-unreserve.json": { + "$schema": "../rpc-schema-draft.json", + "type": "object", + "additionalProperties": false, + "rpc": "askrene-unreserve", + "title": "Command for informing askrene that you are no longer trying a path (EXPERIMENTAL)", + "description": [ + "WARNING: experimental, so API may change.", + "", + "The **askrene-unreserve** RPC command tells askrene that a path attempt has finished: it should only be called after a successful **askrene-reserve** call.", + "", + "Note that additional properties inside the *path* elements are ignored, which is useful when used with the result of *getroutes*." + ], + "request": { + "required": [ + "path" + ], + "properties": { + "path": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": true, + "required": [ + "short_channel_id", + "direction", + "amount_msat" + ], + "properties": { + "short_channel_id": { + "type": "short_channel_id", + "description": [ + "The channel joining these nodes." + ] + }, + "direction": { + "type": "u32", + "description": [ + "0 if this channel is traversed from lesser to greater **id**, otherwise 1." + ] + }, + "amount_msat": { + "type": "msat", + "description": [ + "The amount to send into this hop." + ] + } + } + } + } + } + }, + "response": { + "required": [], + "properties": {} + }, + "see_also": [ + "lightning-getroutes(7)", + "lightning-askrene-reserve(7)", + "lightning-askrene-disable-node(7)", + "lightning-askrene-create-channel(7)", + "lightning-askrene-inform-channel(7)", + "lightning-askrene-listlayers(7)", + "lightning-askrene-age(7)" + ], + "author": [ + "Rusty Russell <> is mainly responsible." + ], + "resources": [ + "Main web site: " + ] + }, "lightning-autoclean-once.json": { "$schema": "../rpc-schema-draft.json", "type": "object", @@ -12738,6 +13392,180 @@ } ] }, + "lightning-getroutes.json": { + "$schema": "../rpc-schema-draft.json", + "type": "object", + "additionalProperties": false, + "rpc": "getroutes", + "title": "Command for routing a payment (EXPERIMENTAL)", + "description": [ + "WARNING: experimental, so API may change.", + "", + "The **getroutes** RPC command attempts to find the best set of paths for the payment from *source* to *destination* of *amount_msat*, using the given *layers* on top of the gossip information. The result is constrained by *maxfee*, and will arrive at the destiation with *finalcltv*.", + "", + "Layers are generally maintained by plugins, either to contain persistent information about capacities which have been discovered, or to contain transient information for this particular payment (such as blinded paths or routehints).", + "", + "There are two automatic layers: *auto.localchans* contains information on local channels from this node (including non-public ones), and their exact current spendable capacities, and *auto.sourcefree* overrides all channels leading out of the *source* to be zero fee and zero delay. These are both useful in the case where the source is the current node." + ], + "categories": [ + "readonly" + ], + "request": { + "required": [ + "source", + "destination", + "amount_msat", + "layers", + "maxfee_msat", + "finalcltv" + ], + "properties": { + "source": { + "type": "pubkey", + "description": [ + "Node pubkey to start the paths" + ] + }, + "destination": { + "type": "pubkey", + "description": [ + "Node pubkey to end the paths" + ] + }, + "amount_msat": { + "type": "msat", + "description": [ + "Amount to send. It can be a whole number, or a whole number ending in *msat* or *sat*, or a number with three decimal places ending in *sat*, or a number with 1 to 11 decimal places ending in *btc*." + ] + }, + "layers": { + "type": "array", + "items": { + "type": "string", + "description": [ + "Layer to apply to the gossip map before attempting to find routes." + ] + } + }, + "maxfee_msat": { + "type": "msat", + "description": [ + "Maximum fee to spend: we will never return a set of routes more expensive than this. It can be a whole number, or a whole number ending in *msat* or *sat*, or a number with three decimal places ending in *sat*, or a number with 1 to 11 decimal places ending in *btc*." + ] + }, + "finalcltv": { + "type": "u32", + "description": [ + "Number of blocks for the final node. We need to know this because no HTLC is allowed to have a CLTV delay more than 2016 blocks." + ] + } + } + }, + "response": { + "required": [ + "probability_ppm", + "routes" + ], + "properties": { + "probability_ppm": { + "type": "u64", + "description": [ + "The estimated probability of success using these routes, in millionths." + ] + }, + "routes": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "probability_ppm", + "amount_msat", + "path" + ], + "properties": { + "probability_ppm": { + "type": "u64", + "description": [ + "The estimated probability of success using this route, in millionths." + ] + }, + "amount_msat": { + "type": "msat", + "description": [ + "The amount delivered to the *destination* by this path." + ] + }, + "path": { + "type": "array", + "description": [ + "The hops to get from *source* to *destination*." + ], + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "short_channel_id", + "direction", + "next_node_id", + "amount_msat", + "delay" + ], + "properties": { + "short_channel_id": { + "type": "short_channel_id", + "description": [ + "The channel joining these nodes." + ] + }, + "direction": { + "type": "u32", + "description": [ + "0 if this channel is traversed from lesser to greater **id**, otherwise 1." + ] + }, + "amount_msat": { + "type": "msat", + "description": [ + "The amount to send into this hop." + ] + }, + "next_node_id": { + "type": "pubkey", + "description": [ + "The peer id at the end of this hop." + ] + }, + "delay": { + "type": "u32", + "description": [ + "The total CLTV expected by the node at the start of this hop." + ] + } + } + } + } + } + } + } + } + }, + "author": [ + "<> wrote the minimum-cost-flow solver, Rusty Russell <> wrote the API and this documentation." + ], + "see_also": [ + "lightning-askrene-reserve(7)", + "lightning-askrene-unreserve(7)", + "lightning-askrene-disable-node(7)", + "lightning-askrene-create-channel(7)", + "lightning-askrene-inform-channel(7)", + "lightning-askrene-report(7)", + "lightning-askrene-age(7)" + ], + "resources": [ + "Main web site: " + ] + }, "lightning-help.json": { "$schema": "../rpc-schema-draft.json", "type": "object", diff --git a/doc/Makefile b/doc/Makefile index c32d5a677ffa..674ca926270b 100644 --- a/doc/Makefile +++ b/doc/Makefile @@ -6,6 +6,12 @@ doc-wrongdir: GENERATE_MARKDOWN := doc/lightning-addgossip.7 \ doc/lightning-addpsbtoutput.7 \ + doc/lightning-askrene-create-channel.7 \ + doc/lightning-askrene-disable-node.7 \ + doc/lightning-askrene-inform-channel.7 \ + doc/lightning-askrene-listlayers.7 \ + doc/lightning-askrene-reserve.7 \ + doc/lightning-askrene-unreserve.7 \ doc/lightning-autoclean-once.7 \ doc/lightning-autoclean-status.7 \ doc/lightning-batching.7 \ @@ -53,6 +59,7 @@ GENERATE_MARKDOWN := doc/lightning-addgossip.7 \ doc/lightning-getinfo.7 \ doc/lightning-getlog.7 \ doc/lightning-getroute.7 \ + doc/lightning-getroutes.7 \ doc/lightning-help.7 \ doc/lightning-invoice.7 \ doc/lightning-invoicerequest.7 \ diff --git a/doc/index.rst b/doc/index.rst index 0e3815b5b3cb..74937401325e 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -14,6 +14,12 @@ Core Lightning Documentation .. block_start manpages lightning-addgossip lightning-addpsbtoutput + lightning-askrene-create-channel + lightning-askrene-disable-node + lightning-askrene-inform-channel + lightning-askrene-listlayers + lightning-askrene-reserve + lightning-askrene-unreserve lightning-autoclean-once lightning-autoclean-status lightning-batching @@ -62,6 +68,7 @@ Core Lightning Documentation lightning-getinfo lightning-getlog lightning-getroute + lightning-getroutes lightning-help lightning-hsmtool lightning-invoice diff --git a/doc/schemas/lightning-askrene-age.json b/doc/schemas/lightning-askrene-age.json new file mode 100644 index 000000000000..97527234d733 --- /dev/null +++ b/doc/schemas/lightning-askrene-age.json @@ -0,0 +1,63 @@ +{ + "$schema": "../rpc-schema-draft.json", + "type": "object", + "additionalProperties": false, + "rpc": "askrene-age", + "title": "Command for expiring information in a layer (EXPERIMENTAL)", + "description": [ + "WARNING: experimental, so API may change.", + "", + "The **askrene-age** RPC command tells askrene that information added to a layer by *askrene-inform-channel* beyond a certain age is less useful. It currently completely forgets constraints older than *cutoff*." + ], + "request": { + "required": [ + "layer", + "cutoff" + ], + "properties": { + "layer": { + "type": "string", + "description": [ + "The name of the layer to apply this change to." + ] + }, + "cutoff": { + "type": "u64", + "description": [ + "The UNIX timestamp: constraints older than this will be forgotten." + ] + } + } + }, + "response": { + "required": [ + "layer", + "num_removed" + ], + "properties": { + "layer": { + "type": "string", + "description": [ + "The *layer* parameter provided." + ] + }, + "num_removed": { + "type": "u64", + "description": [ + "The number of constraints removed from *layer*" + ] + } + } + }, + "see_also": [ + "lightning-getroutes(7)", + "lightning-askrene-inform-channel(7)", + "lightning-askrene-listlayers(7)" + ], + "author": [ + "Rusty Russell <> is mainly responsible." + ], + "resources": [ + "Main web site: " + ] +} diff --git a/doc/schemas/lightning-askrene-create-channel.json b/doc/schemas/lightning-askrene-create-channel.json new file mode 100644 index 000000000000..ff43fdf7eb99 --- /dev/null +++ b/doc/schemas/lightning-askrene-create-channel.json @@ -0,0 +1,105 @@ +{ + "$schema": "../rpc-schema-draft.json", + "type": "object", + "additionalProperties": false, + "rpc": "askrene-create-channel", + "title": "Command to add a channel to layer (EXPERIMENTAL)", + "description": [ + "WARNING: experimental, so API may change.", + "", + "The **askrene-create-channel** RPC command tells askrene to populate one direction of a channel in the given layer. If the channel already exists, it will be overridden. If the layer does not exist, it will be created." + ], + "request": { + "required": [ + "layer", + "source", + "destination", + "short_channel_id", + "capacity_msat", + "htlc_min", + "htlc_max", + "base_fee", + "proportional_fee", + "delay" + ], + "properties": { + "layer": { + "type": "string", + "description": [ + "The name of the layer to apply this change to." + ] + }, + "source": { + "type": "pubkey", + "description": [ + "The source node id for the channel." + ] + }, + "destination": { + "type": "pubkey", + "description": [ + "The destination node id for the channel." + ] + }, + "short_channel_id": { + "type": "short_channel_id", + "description": [ + "The short channel id for the channel. If a channel with this short channel id already exists in *layer*, the *source*, *destination* and *capacity_msat* must be the same." + ] + }, + "capacity_msat": { + "type": "msat", + "description": [ + "The capacity (onchain size) of the channel." + ] + }, + "htlc_min": { + "type": "msat", + "description": [ + "The minimum value allowed in this direction." + ] + }, + "htlc_max": { + "type": "msat", + "description": [ + "The maximum value allowed in this direction." + ] + }, + "base_fee": { + "type": "msat", + "description": [ + "The base fee to apply to use the channel in this direction." + ] + }, + "proportional_fee": { + "type": "u32", + "description": [ + "The proportional fee (in parts per million) to apply to use the channel in this direction." + ] + }, + "delay": { + "type": "u16", + "description": [ + "The CLTV delay required for this direction." + ] + } + } + }, + "response": { + "required": [], + "properties": {} + }, + "see_also": [ + "lightning-getroutes(7)", + "lightning-askrene-disable-node(7)", + "lightning-askrene-inform-channel(7)", + "lightning-askrene-listlayers(7)", + "lightning-askrene-age(7)" + ], + "author": [ + "Rusty Russell <> is mainly responsible." + ], + "resources": [ + "Main web site: " + ] +} diff --git a/doc/schemas/lightning-askrene-disable-node.json b/doc/schemas/lightning-askrene-disable-node.json new file mode 100644 index 000000000000..389dacaed91e --- /dev/null +++ b/doc/schemas/lightning-askrene-disable-node.json @@ -0,0 +1,49 @@ +{ + "$schema": "../rpc-schema-draft.json", + "type": "object", + "additionalProperties": false, + "rpc": "askrene-disable-node", + "title": "Command to disable all channels to/from a node in a layer (EXPERIMENTAL)", + "description": [ + "WARNING: experimental, so API may change.", + "", + "The **askrene-disable-node** RPC command tells askrene to disable all channels connected to a node whenever the given layer is used. This is mainly useful to force the use of alternate paths: while individual channels can be disabled using askrene-create-channel or askrene-inform-channel, that would be racy if new channels appeared." + ], + "request": { + "required": [ + "layer", + "node" + ], + "properties": { + "layer": { + "type": "string", + "description": [ + "The name of the layer to apply this change to." + ] + }, + "node": { + "type": "pubkey", + "description": [ + "The node to disable. It does not need to exist." + ] + } + } + }, + "response": { + "required": [], + "properties": {} + }, + "see_also": [ + "lightning-getroutes(7)", + "lightning-askrene-create-channel(7)", + "lightning-askrene-inform-channel(7)", + "lightning-askrene-listlayers(7)", + "lightning-askrene-age(7)" + ], + "author": [ + "Rusty Russell <> is mainly responsible." + ], + "resources": [ + "Main web site: " + ] +} diff --git a/doc/schemas/lightning-askrene-inform-channel.json b/doc/schemas/lightning-askrene-inform-channel.json new file mode 100644 index 000000000000..84017b2852e3 --- /dev/null +++ b/doc/schemas/lightning-askrene-inform-channel.json @@ -0,0 +1,111 @@ +{ + "$schema": "../rpc-schema-draft.json", + "type": "object", + "additionalProperties": false, + "rpc": "askrene-inform-channel", + "title": "Command to add channel capacity restrictions to layer (EXPERIMENTAL)", + "description": [ + "WARNING: experimental, so API may change.", + "", + "The **askrene-inform-channel** RPC command tells askrene about the minimum or maximum current capacity of a given channel. It can be applied whether the curren channel exists or not. If the layer does not exist, it will be created." + ], + "request": { + "required": [ + "layer", + "short_channel_id", + "direction" + ], + "properties": { + "layer": { + "type": "string", + "description": [ + "The name of the layer to apply this change to." + ] + }, + "short_channel_id": { + "type": "short_channel_id", + "description": [ + "The short channel id to apply this change to." + ] + }, + "direction": { + "type": "u32", + "description": [ + "The direction to apply this change to." + ] + }, + "maximum_msat": { + "type": "msat", + "description": [ + "The maximum value which this channel could pass. This or *minimum_msat* must be specified, but not both." + ] + }, + "minimum_msat": { + "type": "msat", + "description": [ + "The minumum value which this channel could pass. This or *minimum_msat* must be specified, but not both." + ] + } + } + }, + "response": { + "required": [ + "constraint" + ], + "properties": { + "constraint": { + "type": "object", + "required": [ + "short_channel_id", + "direction", + "timestamp" + ], + "properties": { + "short_channel_id": { + "type": "short_channel_id", + "description": [ + "The *short_channel_id* specified." + ] + }, + "direction": { + "type": "u32", + "description": [ + "The *direction* specified." + ] + }, + "timestamp": { + "type": "u64", + "description": [ + "The UNIX time (seconds since 1970) this was created." + ] + }, + "maximum_msat": { + "type": "msat", + "description": [ + "The *minimum_msat* (if specified)" + ] + }, + "minimum_msat": { + "type": "msat", + "description": [ + "The *maximum_msat* (if specified)" + ] + } + } + } + } + }, + "see_also": [ + "lightning-getroutes(7)", + "lightning-askrene-disable-node(7)", + "lightning-askrene-create-channel(7)", + "lightning-askrene-listlayers(7)", + "lightning-askrene-age(7)" + ], + "author": [ + "Rusty Russell <> is mainly responsible." + ], + "resources": [ + "Main web site: " + ] +} diff --git a/doc/schemas/lightning-askrene-listlayers.json b/doc/schemas/lightning-askrene-listlayers.json new file mode 100644 index 000000000000..a0a5cba70511 --- /dev/null +++ b/doc/schemas/lightning-askrene-listlayers.json @@ -0,0 +1,182 @@ +{ + "$schema": "../rpc-schema-draft.json", + "type": "object", + "additionalProperties": false, + "rpc": "askrene-listlayers", + "title": "Command to display information about layers (EXPERIMENTAL)", + "description": [ + "WARNING: experimental, so API may change.", + "", + "The **askrene-listlayers** RPC command reports any modifications each layer (or, the layer specified) would make to the topology, if it were used for *getroutes*." + ], + "request": { + "required": [], + "properties": { + "layer": { + "type": "string", + "description": [ + "The name of the layer to report on." + ] + } + } + }, + "response": { + "required": [ + "layers" + ], + "properties": { + "layers": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "layer", + "disabled_nodes", + "created_channels", + "constraints" + ], + "properties": { + "layer": { + "type": "string", + "description": [ + "The name of the layer." + ] + }, + "disabled_nodes": { + "type": "array", + "items": { + "type": "pubkey", + "description": [ + "The id of the disabled node." + ] + } + }, + "created_channels": { + "type": "array", + "items": { + "type": "object", + "required": [ + "source", + "destination", + "short_channel_id", + "capacity_msat", + "htlc_minimum_msat", + "htlc_maximum_msat", + "fee_base_msat", + "fee_proportional_millionths", + "delay" + ], + "properties": { + "source": { + "type": "pubkey", + "description": [ + "The source node id for the channel." + ] + }, + "destination": { + "type": "pubkey", + "description": [ + "The destination node id for the channel." + ] + }, + "short_channel_id": { + "type": "short_channel_id", + "description": [ + "The short channel id for the channel." + ] + }, + "capacity_msat": { + "type": "msat", + "description": [ + "The capacity (onchain size) of the channel." + ] + }, + "htlc_minimum_msat": { + "type": "msat", + "description": [ + "The minimum value allowed in this direction." + ] + }, + "htlc_maximum_msat": { + "type": "msat", + "description": [ + "The maximum value allowed in this direction." + ] + }, + "fee_base_msat": { + "type": "msat", + "description": [ + "The base fee to apply to use the channel in this direction." + ] + }, + "fee_proportional_millionths": { + "type": "u32", + "description": [ + "The proportional fee (in parts per million) to apply to use the channel in this direction." + ] + }, + "delay": { + "type": "u16", + "description": [ + "The CLTV delay required for this direction." + ] + } + } + } + }, + "constraints": { + "type": "array", + "items": { + "type": "object", + "required": [ + "short_channel_id", + "direction" + ], + "properties": { + "short_channel_id": { + "type": "short_channel_id", + "description": [ + "The short channel id." + ] + }, + "direction": { + "type": "u32", + "description": [ + "The direction." + ] + }, + "maximum_msat": { + "type": "msat", + "description": [ + "The maximum value which this channel could pass. This or *minimum_msat* will be present, but not both." + ] + }, + "minimum_msat": { + "type": "msat", + "description": [ + "The minimum value which this channel could pass. This or *minimum_msat* will be present, but not both." + ] + } + } + } + } + } + } + } + } + }, + "see_also": [ + "lightning-getroutes(7)", + "lightning-askrene-disable-node(7)", + "lightning-askrene-create-channel(7)", + "lightning-askrene-inform-channel(7)", + "lightning-askrene-age(7)" + ], + "author": [ + "Rusty Russell <> is mainly responsible." + ], + "resources": [ + "Main web site: " + ] +} diff --git a/doc/schemas/lightning-askrene-reserve.json b/doc/schemas/lightning-askrene-reserve.json new file mode 100644 index 000000000000..7b91e3800998 --- /dev/null +++ b/doc/schemas/lightning-askrene-reserve.json @@ -0,0 +1,72 @@ +{ + "$schema": "../rpc-schema-draft.json", + "type": "object", + "additionalProperties": false, + "rpc": "askrene-reserve", + "title": "Command for informing askrene that you are trying a path (EXPERIMENTAL)", + "description": [ + "WARNING: experimental, so API may change.", + "", + "The **askrene-reserve** RPC command tells askrene that a path is being attempted. This allows it to take that into account when other *getroutes* calls are made. You should call **askrene-unreserve** after the attempt has completed.", + "", + "Note that additional properties inside the *path* elements are ignored, which is useful when used with the result of *getroutes*." + ], + "request": { + "required": [ + "path" + ], + "properties": { + "path": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": true, + "required": [ + "short_channel_id", + "direction", + "amount_msat" + ], + "properties": { + "short_channel_id": { + "type": "short_channel_id", + "description": [ + "The channel joining these nodes." + ] + }, + "direction": { + "type": "u32", + "description": [ + "0 if this channel is traversed from lesser to greater **id**, otherwise 1." + ] + }, + "amount_msat": { + "type": "msat", + "description": [ + "The amount to send into this hop." + ] + } + } + } + } + } + }, + "response": { + "required": [], + "properties": {} + }, + "see_also": [ + "lightning-getroutes(7)", + "lightning-askrene-unreserve(7)", + "lightning-askrene-disable-node(7)", + "lightning-askrene-create-channel(7)", + "lightning-askrene-inform-channel(7)", + "lightning-askrene-listlayers(7)", + "lightning-askrene-age(7)" + ], + "author": [ + "Rusty Russell <> is mainly responsible." + ], + "resources": [ + "Main web site: " + ] +} diff --git a/doc/schemas/lightning-askrene-unreserve.json b/doc/schemas/lightning-askrene-unreserve.json new file mode 100644 index 000000000000..377595a5caa5 --- /dev/null +++ b/doc/schemas/lightning-askrene-unreserve.json @@ -0,0 +1,72 @@ +{ + "$schema": "../rpc-schema-draft.json", + "type": "object", + "additionalProperties": false, + "rpc": "askrene-unreserve", + "title": "Command for informing askrene that you are no longer trying a path (EXPERIMENTAL)", + "description": [ + "WARNING: experimental, so API may change.", + "", + "The **askrene-unreserve** RPC command tells askrene that a path attempt has finished: it should only be called after a successful **askrene-reserve** call.", + "", + "Note that additional properties inside the *path* elements are ignored, which is useful when used with the result of *getroutes*." + ], + "request": { + "required": [ + "path" + ], + "properties": { + "path": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": true, + "required": [ + "short_channel_id", + "direction", + "amount_msat" + ], + "properties": { + "short_channel_id": { + "type": "short_channel_id", + "description": [ + "The channel joining these nodes." + ] + }, + "direction": { + "type": "u32", + "description": [ + "0 if this channel is traversed from lesser to greater **id**, otherwise 1." + ] + }, + "amount_msat": { + "type": "msat", + "description": [ + "The amount to send into this hop." + ] + } + } + } + } + } + }, + "response": { + "required": [], + "properties": {} + }, + "see_also": [ + "lightning-getroutes(7)", + "lightning-askrene-reserve(7)", + "lightning-askrene-disable-node(7)", + "lightning-askrene-create-channel(7)", + "lightning-askrene-inform-channel(7)", + "lightning-askrene-listlayers(7)", + "lightning-askrene-age(7)" + ], + "author": [ + "Rusty Russell <> is mainly responsible." + ], + "resources": [ + "Main web site: " + ] +} diff --git a/doc/schemas/lightning-getroutes.json b/doc/schemas/lightning-getroutes.json new file mode 100644 index 000000000000..303ec958777d --- /dev/null +++ b/doc/schemas/lightning-getroutes.json @@ -0,0 +1,174 @@ +{ + "$schema": "../rpc-schema-draft.json", + "type": "object", + "additionalProperties": false, + "rpc": "getroutes", + "title": "Command for routing a payment (EXPERIMENTAL)", + "description": [ + "WARNING: experimental, so API may change.", + "", + "The **getroutes** RPC command attempts to find the best set of paths for the payment from *source* to *destination* of *amount_msat*, using the given *layers* on top of the gossip information. The result is constrained by *maxfee*, and will arrive at the destiation with *finalcltv*.", + "", + "Layers are generally maintained by plugins, either to contain persistent information about capacities which have been discovered, or to contain transient information for this particular payment (such as blinded paths or routehints).", + "", + "There are two automatic layers: *auto.localchans* contains information on local channels from this node (including non-public ones), and their exact current spendable capacities, and *auto.sourcefree* overrides all channels leading out of the *source* to be zero fee and zero delay. These are both useful in the case where the source is the current node." + ], + "categories": [ + "readonly" + ], + "request": { + "required": [ + "source", + "destination", + "amount_msat", + "layers", + "maxfee_msat", + "finalcltv" + ], + "properties": { + "source": { + "type": "pubkey", + "description": [ + "Node pubkey to start the paths" + ] + }, + "destination": { + "type": "pubkey", + "description": [ + "Node pubkey to end the paths" + ] + }, + "amount_msat": { + "type": "msat", + "description": [ + "Amount to send. It can be a whole number, or a whole number ending in *msat* or *sat*, or a number with three decimal places ending in *sat*, or a number with 1 to 11 decimal places ending in *btc*." + ] + }, + "layers": { + "type": "array", + "items": { + "type": "string", + "description": [ + "Layer to apply to the gossip map before attempting to find routes." + ] + } + }, + "maxfee_msat": { + "type": "msat", + "description": [ + "Maximum fee to spend: we will never return a set of routes more expensive than this. It can be a whole number, or a whole number ending in *msat* or *sat*, or a number with three decimal places ending in *sat*, or a number with 1 to 11 decimal places ending in *btc*." + ] + }, + "finalcltv": { + "type": "u32", + "description": [ + "Number of blocks for the final node. We need to know this because no HTLC is allowed to have a CLTV delay more than 2016 blocks." + ] + } + } + }, + "response": { + "required": [ + "probability_ppm", + "routes" + ], + "properties": { + "probability_ppm": { + "type": "u64", + "description": [ + "The estimated probability of success using these routes, in millionths." + ] + }, + "routes": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "probability_ppm", + "amount_msat", + "path" + ], + "properties": { + "probability_ppm": { + "type": "u64", + "description": [ + "The estimated probability of success using this route, in millionths." + ] + }, + "amount_msat": { + "type": "msat", + "description": [ + "The amount delivered to the *destination* by this path." + ] + }, + "path": { + "type": "array", + "description": [ + "The hops to get from *source* to *destination*." + ], + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "short_channel_id", + "direction", + "next_node_id", + "amount_msat", + "delay" + ], + "properties": { + "short_channel_id": { + "type": "short_channel_id", + "description": [ + "The channel joining these nodes." + ] + }, + "direction": { + "type": "u32", + "description": [ + "0 if this channel is traversed from lesser to greater **id**, otherwise 1." + ] + }, + "amount_msat": { + "type": "msat", + "description": [ + "The amount to send into this hop." + ] + }, + "next_node_id": { + "type": "pubkey", + "description": [ + "The peer id at the end of this hop." + ] + }, + "delay": { + "type": "u32", + "description": [ + "The total CLTV expected by the node at the start of this hop." + ] + } + } + } + } + } + } + } + } + }, + "author": [ + "<> wrote the minimum-cost-flow solver, Rusty Russell <> wrote the API and this documentation." + ], + "see_also": [ + "lightning-askrene-reserve(7)", + "lightning-askrene-unreserve(7)", + "lightning-askrene-disable-node(7)", + "lightning-askrene-create-channel(7)", + "lightning-askrene-inform-channel(7)", + "lightning-askrene-report(7)", + "lightning-askrene-age(7)" + ], + "resources": [ + "Main web site: " + ] +} From 1034c6fe690a29719d392c91d9db4e5534f36b44 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Wed, 7 Aug 2024 11:19:52 +0930 Subject: [PATCH 02/28] gossmap: don't process channel_announcement until amount is present. This simplifies the callers significantly: all channel_announcements now have an amount, so gossmap_chan_get_capacity() only fails on a local modification. Signed-off-by: Rusty Russell --- common/gossmap.c | 21 +++++++++++++++------ common/gossmap.h | 2 +- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/common/gossmap.c b/common/gossmap.c index ac88d315246d..068a25c41566 100644 --- a/common/gossmap.c +++ b/common/gossmap.c @@ -444,7 +444,8 @@ void gossmap_remove_node(struct gossmap *map, struct gossmap_node *node) * * [`point`:`node_id_2`] */ static struct gossmap_chan *add_channel(struct gossmap *map, - size_t cannounce_off) + size_t cannounce_off, + size_t msglen) { /* Note that first two bytes are message type */ const size_t feature_len_off = 2 + (64 + 64 + 64 + 64); @@ -473,6 +474,12 @@ static struct gossmap_chan *add_channel(struct gossmap *map, return NULL; } + /* gossipd writes WIRE_GOSSIP_STORE_CHANNEL_AMOUNT after this (not for + * local channels), so ignore channel_announcement until that appears */ + if (msglen + && (map->map_size < cannounce_off + msglen + sizeof(struct gossip_hdr) + sizeof(u16) + sizeof(u64))) + return NULL; + /* We carefully map pointers to indexes, since new_node can move them! */ n[0] = gossmap_find_node(map, &node_id[0]); if (n[0]) @@ -668,9 +675,11 @@ static bool map_catchup(struct gossmap *map, bool *changed) off = map->map_end + sizeof(ghdr); type = map_be16(map, off); - if (type == WIRE_CHANNEL_ANNOUNCEMENT) - add_channel(map, off); - else if (type == WIRE_CHANNEL_UPDATE) + if (type == WIRE_CHANNEL_ANNOUNCEMENT) { + /* Don't read yet if amount field is not there! */ + if (!add_channel(map, off, be16_to_cpu(ghdr.len))) + break; + } else if (type == WIRE_CHANNEL_UPDATE) update_channel(map, off); else if (type == WIRE_GOSSIP_STORE_DELETE_CHAN) remove_channel_by_deletemsg(map, off); @@ -944,7 +953,7 @@ void gossmap_apply_localmods(struct gossmap *map, continue; /* Create new channel, pointing into local. */ - chan = add_channel(map, map->map_size + mod->local_off); + chan = add_channel(map, map->map_size + mod->local_off, 0); } /* Save old, overwrite (keep nodeidx) */ @@ -1144,7 +1153,7 @@ bool gossmap_chan_get_capacity(const struct gossmap *map, off += sizeof(ghdr) + be16_to_cpu(ghdr.len); /* Partial write, this can happen. */ - if (off + sizeof(ghdr) + 2 > map->map_size) + if (off + sizeof(ghdr) + sizeof(u16) + sizeof(u64) > map->map_size) return false; /* Get type of next field. */ diff --git a/common/gossmap.h b/common/gossmap.h index f4fbc883addb..61ed2bbbbf76 100644 --- a/common/gossmap.h +++ b/common/gossmap.h @@ -161,7 +161,7 @@ static inline bool gossmap_chan_set(const struct gossmap_chan *chan, int dir) return chan->cupdate_off[dir] != 0; } -/* Return capacity if it's known (fails only on race condition, or a local mod) */ +/* Return capacity if it's known (fails on a local mod) */ bool gossmap_chan_get_capacity(const struct gossmap *map, const struct gossmap_chan *c, struct amount_sat *amount); From df086151b7e01d69066d634b5fec31f439d33c55 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Wed, 7 Aug 2024 11:19:52 +0930 Subject: [PATCH 03/28] libplugin: add data pointer for plugin convenience. This avoids globals (and means memleak traverses the variables!): we only change over the test plugin though, to avoid unnecessary churn. Signed-off-by: Rusty Russell --- plugins/autoclean.c | 3 +- plugins/bcli.c | 2 +- plugins/bkpr/bookkeeper.c | 2 +- plugins/chanbackup.c | 2 +- plugins/commando.c | 2 +- plugins/funder.c | 2 +- plugins/keysend.c | 2 +- plugins/libplugin.c | 17 +++++++++++ plugins/libplugin.h | 6 ++++ plugins/offers.c | 2 +- plugins/pay.c | 2 +- plugins/recover.c | 2 +- plugins/renepay/main.c | 2 +- plugins/spender/main.c | 2 +- plugins/sql.c | 2 +- plugins/topology.c | 2 +- plugins/txprepare.c | 2 +- tests/plugins/test_libplugin.c | 53 ++++++++++++++++++++++++---------- 18 files changed, 77 insertions(+), 30 deletions(-) diff --git a/plugins/autoclean.c b/plugins/autoclean.c index 92c86fe2a338..f1e2e5198170 100644 --- a/plugins/autoclean.c +++ b/plugins/autoclean.c @@ -805,7 +805,8 @@ int main(int argc, char *argv[]) setup_locale(); timer_cinfo = new_clean_info(NULL, NULL); - plugin_main(argv, init, PLUGIN_STATIC, true, NULL, commands, ARRAY_SIZE(commands), + plugin_main(argv, init, NULL, PLUGIN_STATIC, true, NULL, + commands, ARRAY_SIZE(commands), NULL, 0, NULL, 0, NULL, 0, plugin_option_dynamic("autoclean-cycle", "int", diff --git a/plugins/bcli.c b/plugins/bcli.c index 27cfb6a4f61a..b169a74df754 100644 --- a/plugins/bcli.c +++ b/plugins/bcli.c @@ -1171,7 +1171,7 @@ int main(int argc, char *argv[]) /* Initialize our global context object here to handle startup options. */ bitcoind = new_bitcoind(NULL); - plugin_main(argv, init, PLUGIN_STATIC, false /* Do not init RPC on startup*/, + plugin_main(argv, init, NULL, PLUGIN_STATIC, false /* Do not init RPC on startup*/, NULL, commands, ARRAY_SIZE(commands), NULL, 0, NULL, 0, NULL, 0, plugin_option("bitcoin-datadir", diff --git a/plugins/bkpr/bookkeeper.c b/plugins/bkpr/bookkeeper.c index 0bb4b9cc3897..4735e335954f 100644 --- a/plugins/bkpr/bookkeeper.c +++ b/plugins/bkpr/bookkeeper.c @@ -1809,7 +1809,7 @@ int main(int argc, char *argv[]) datadir = NULL; db_dsn = NULL; - plugin_main(argv, init, PLUGIN_STATIC, true, NULL, + plugin_main(argv, init, NULL, PLUGIN_STATIC, true, NULL, commands, ARRAY_SIZE(commands), notifs, ARRAY_SIZE(notifs), NULL, 0, diff --git a/plugins/chanbackup.c b/plugins/chanbackup.c index cd094e41e709..ca8365495294 100644 --- a/plugins/chanbackup.c +++ b/plugins/chanbackup.c @@ -827,7 +827,7 @@ int main(int argc, char *argv[]) { setup_locale(); - plugin_main(argv, init, PLUGIN_STATIC, true, NULL, + plugin_main(argv, init, NULL, PLUGIN_STATIC, true, NULL, commands, ARRAY_SIZE(commands), notifs, ARRAY_SIZE(notifs), hooks, ARRAY_SIZE(hooks), NULL, 0, /* Notification topics we publish */ diff --git a/plugins/commando.c b/plugins/commando.c index 0f20ad92732f..aede4c35b634 100644 --- a/plugins/commando.c +++ b/plugins/commando.c @@ -825,7 +825,7 @@ static const struct plugin_command commands[] = { { int main(int argc, char *argv[]) { setup_locale(); - plugin_main(argv, init, PLUGIN_STATIC, true, NULL, + plugin_main(argv, init, NULL, PLUGIN_STATIC, true, NULL, commands, ARRAY_SIZE(commands), NULL, 0, hooks, ARRAY_SIZE(hooks), diff --git a/plugins/funder.c b/plugins/funder.c index eb292bd6dcf3..66063b363f65 100644 --- a/plugins/funder.c +++ b/plugins/funder.c @@ -1695,7 +1695,7 @@ int main(int argc, char **argv) /* Our default funding policy is fixed (0msat) */ current_policy = default_funder_policy(NULL, FIXED, 0); - plugin_main(argv, init, PLUGIN_RESTARTABLE, true, + plugin_main(argv, init, NULL, PLUGIN_RESTARTABLE, true, NULL, commands, ARRAY_SIZE(commands), notifs, ARRAY_SIZE(notifs), diff --git a/plugins/keysend.c b/plugins/keysend.c index 486a203b6fba..b97bd8932443 100644 --- a/plugins/keysend.c +++ b/plugins/keysend.c @@ -594,7 +594,7 @@ int main(int argc, char *argv[]) features->bits[i] = tal_arr(features, u8, 0); set_feature_bit(&features->bits[NODE_ANNOUNCE_FEATURE], KEYSEND_FEATUREBIT); - plugin_main(argv, init, PLUGIN_STATIC, true, features, commands, + plugin_main(argv, init, NULL, PLUGIN_STATIC, true, features, commands, ARRAY_SIZE(commands), NULL, 0, hooks, ARRAY_SIZE(hooks), notification_topics, ARRAY_SIZE(notification_topics), NULL); } diff --git a/plugins/libplugin.c b/plugins/libplugin.c index fffcbe5aaffd..3ba3b85ab688 100644 --- a/plugins/libplugin.c +++ b/plugins/libplugin.c @@ -79,6 +79,9 @@ struct plugin { /* to append to all our command ids */ const char *id; + /* Data for the plugin user */ + void *data; + /* options to i-promise-to-fix-broken-api-user */ const char **beglist; @@ -2179,6 +2182,7 @@ static struct plugin *new_plugin(const tal_t *ctx, void plugin_main(char *argv[], const char *(*init)(struct plugin *p, const char *buf, const jsmntok_t *), + void *data, const enum plugin_restartability restartability, bool init_rpc, struct feature_set *features STEALS, @@ -2208,6 +2212,7 @@ void plugin_main(char *argv[], init, restartability, init_rpc, features, commands, num_commands, notif_subs, num_notif_subs, hook_subs, num_hook_subs, notif_topics, num_notif_topics, ap); + plugin_set_data(plugin, data); va_end(ap); setup_command_usage(plugin); @@ -2414,3 +2419,15 @@ bool plugin_developer_mode(const struct plugin *plugin) { return plugin->developer; } + +void plugin_set_data(struct plugin *plugin, void *data TAKES) +{ + if (taken(data)) + tal_steal(plugin, data); + plugin->data = data; +} + +void *plugin_get_data_(struct plugin *plugin) +{ + return plugin->data; +} diff --git a/plugins/libplugin.h b/plugins/libplugin.h index 130127d2b458..6a3be12c0394 100644 --- a/plugins/libplugin.h +++ b/plugins/libplugin.h @@ -440,6 +440,11 @@ static inline void *plugin_option_jsonfmt_check(bool (*jsonfmt)(struct plugin *, /* Is --developer enabled? */ bool plugin_developer_mode(const struct plugin *plugin); +/* Store a single pointer for our state in the plugin */ +void plugin_set_data(struct plugin *plugin, void *data TAKES); +void *plugin_get_data_(struct plugin *plugin); +#define plugin_get_data(plugin, type) ((type *)(plugin_get_data_(plugin))) + /* Macro to define arguments */ #define plugin_option_(name, type, description, set, jsonfmt, arg, dev_only, depr_start, depr_end, dynamic) \ (name), \ @@ -504,6 +509,7 @@ void NORETURN LAST_ARG_NULL plugin_main(char *argv[], const char *(*init)(struct plugin *p, const char *buf, const jsmntok_t *), + void *data TAKES, const enum plugin_restartability restartability, bool init_rpc, struct feature_set *features STEALS, diff --git a/plugins/offers.c b/plugins/offers.c index be543a5ea4af..ef75d6c46218 100644 --- a/plugins/offers.c +++ b/plugins/offers.c @@ -1473,7 +1473,7 @@ int main(int argc, char *argv[]) /* We deal in UTC; mktime() uses local time */ setenv("TZ", "", 1); - plugin_main(argv, init, PLUGIN_RESTARTABLE, true, NULL, + plugin_main(argv, init, NULL, PLUGIN_RESTARTABLE, true, NULL, commands, ARRAY_SIZE(commands), notifications, ARRAY_SIZE(notifications), hooks, ARRAY_SIZE(hooks), diff --git a/plugins/pay.c b/plugins/pay.c index 3a33f3d88c6f..41f1d63ddc97 100644 --- a/plugins/pay.c +++ b/plugins/pay.c @@ -1527,7 +1527,7 @@ static const char *notification_topics[] = { int main(int argc, char *argv[]) { setup_locale(); - plugin_main(argv, init, PLUGIN_RESTARTABLE, true, NULL, commands, + plugin_main(argv, init, NULL, PLUGIN_RESTARTABLE, true, NULL, commands, ARRAY_SIZE(commands), NULL, 0, NULL, 0, notification_topics, ARRAY_SIZE(notification_topics), plugin_option("disable-mpp", "flag", diff --git a/plugins/recover.c b/plugins/recover.c index 07149f773c63..b46d55dc74ea 100644 --- a/plugins/recover.c +++ b/plugins/recover.c @@ -288,7 +288,7 @@ int main(int argc, char *argv[]) { setup_locale(); - plugin_main(argv, init, PLUGIN_STATIC, true, NULL, + plugin_main(argv, init, NULL, PLUGIN_STATIC, true, NULL, NULL, 0, NULL, 0, NULL, 0, NULL, 0, /* Notification topics we publish */ diff --git a/plugins/renepay/main.c b/plugins/renepay/main.c index 2aa13dace0c1..5ee7de946ca1 100644 --- a/plugins/renepay/main.c +++ b/plugins/renepay/main.c @@ -411,7 +411,7 @@ int main(int argc, char *argv[]) plugin_main( argv, - init, + init, NULL, PLUGIN_RESTARTABLE, /* init_rpc */ true, /* features */ NULL, diff --git a/plugins/spender/main.c b/plugins/spender/main.c index 23698567aed4..ce0e84d100cb 100644 --- a/plugins/spender/main.c +++ b/plugins/spender/main.c @@ -32,7 +32,7 @@ int main(int argc, char **argv) notifs = tal_arr(NULL, struct plugin_notification, 0); tal_expand(¬ifs, openchannel_notifs, num_openchannel_notifs); - plugin_main(argv, &spender_init, PLUGIN_STATIC, true, + plugin_main(argv, &spender_init, NULL, PLUGIN_STATIC, true, NULL, take(commands), tal_count(commands), take(notifs), tal_count(notifs), diff --git a/plugins/sql.c b/plugins/sql.c index dd5d722aff17..1490b5063955 100644 --- a/plugins/sql.c +++ b/plugins/sql.c @@ -1650,7 +1650,7 @@ int main(int argc, char *argv[]) common_shutdown(); return 0; } - plugin_main(argv, init, PLUGIN_RESTARTABLE, true, NULL, commands, ARRAY_SIZE(commands), + plugin_main(argv, init, NULL, PLUGIN_RESTARTABLE, true, NULL, commands, ARRAY_SIZE(commands), NULL, 0, NULL, 0, NULL, 0, plugin_option_dev("dev-sqlfilename", "string", diff --git a/plugins/topology.c b/plugins/topology.c index d2368bdbf086..8b9ad3d0a711 100644 --- a/plugins/topology.c +++ b/plugins/topology.c @@ -747,6 +747,6 @@ static const struct plugin_command commands[] = { int main(int argc, char *argv[]) { setup_locale(); - plugin_main(argv, init, PLUGIN_STATIC, true, NULL, commands, ARRAY_SIZE(commands), + plugin_main(argv, init, NULL, PLUGIN_STATIC, true, NULL, commands, ARRAY_SIZE(commands), NULL, 0, NULL, 0, NULL, 0, NULL); } diff --git a/plugins/txprepare.c b/plugins/txprepare.c index 360f4277c4ef..3024f305b2fa 100644 --- a/plugins/txprepare.c +++ b/plugins/txprepare.c @@ -668,6 +668,6 @@ static const char *init(struct plugin *p, int main(int argc, char *argv[]) { setup_locale(); - plugin_main(argv, init, PLUGIN_RESTARTABLE, true, NULL, commands, + plugin_main(argv, init, NULL, PLUGIN_RESTARTABLE, true, NULL, commands, ARRAY_SIZE(commands), NULL, 0, NULL, 0, NULL, 0, NULL); } diff --git a/tests/plugins/test_libplugin.c b/tests/plugins/test_libplugin.c index 6a2580ef63cf..71a306b512b2 100644 --- a/tests/plugins/test_libplugin.c +++ b/tests/plugins/test_libplugin.c @@ -6,10 +6,18 @@ #include #include -static char *somearg; -static bool self_disable = false; -static bool dont_shutdown = false; -static u32 dynamic_opt = 7; +/* Stash this in plugin's data */ +struct test_libplugin { + char *somearg; + bool self_disable; + bool dont_shutdown; + u32 dynamic_opt; +}; + +static struct test_libplugin *get_test_libplugin(struct plugin *plugin) +{ + return plugin_get_data(plugin, struct test_libplugin); +} static struct command_result *get_ds_done(struct command *cmd, const char *val, @@ -94,9 +102,10 @@ static struct command_result *json_shutdown(struct command *cmd, const char *buf, const jsmntok_t *params) { + struct test_libplugin *tlp = get_test_libplugin(cmd->plugin); plugin_log(cmd->plugin, LOG_DBG, "shutdown called"); - if (dont_shutdown) + if (tlp->dont_shutdown) return notification_handled(cmd); plugin_exit(cmd->plugin, 0); @@ -202,13 +211,14 @@ static const char *init(struct plugin *p, { const char *name, *err_str, *err_hex; const u8 *binname; + struct test_libplugin *tlp = get_test_libplugin(p); plugin_log(p, LOG_DBG, "test_libplugin initialised!"); - if (somearg) - plugin_log(p, LOG_DBG, "somearg = %s", somearg); - somearg = tal_free(somearg); + if (tlp->somearg) + plugin_log(p, LOG_DBG, "somearg = %s", tlp->somearg); + tlp->somearg = tal_free(tlp->somearg); - if (self_disable) + if (tlp->self_disable) return "Disabled via selfdisable option"; /* Test rpc_scan_datastore funcs */ @@ -277,30 +287,43 @@ static const struct plugin_notification notifs[] = { { int main(int argc, char *argv[]) { setup_locale(); - plugin_main(argv, init, PLUGIN_RESTARTABLE, true, NULL, + /* We allocate now, so we can hand pointers for plugin options, + * but by specifying take() to plugin_main, it reparents it to + * the plugin */ + struct test_libplugin *tlp = tal(NULL, struct test_libplugin); + tlp->somearg = NULL; + tlp->self_disable = false; + tlp->dont_shutdown = false; + tlp->dynamic_opt = 7; + + plugin_main(argv, init, take(tlp), PLUGIN_RESTARTABLE, true, NULL, commands, ARRAY_SIZE(commands), notifs, ARRAY_SIZE(notifs), hooks, ARRAY_SIZE(hooks), NULL, 0, /* Notification topics we publish */ plugin_option("somearg", "string", "Argument to print at init.", - charp_option, charp_jsonfmt, &somearg), + charp_option, charp_jsonfmt, &tlp->somearg), plugin_option_deprecated("somearg-deprecated", "string", "Deprecated arg for init.", CLN_NEXT_VERSION, NULL, - charp_option, charp_jsonfmt, &somearg), + charp_option, charp_jsonfmt, + &tlp->somearg), plugin_option("selfdisable", "flag", "Whether to disable.", - flag_option, flag_jsonfmt, &self_disable), + flag_option, flag_jsonfmt, + &tlp->self_disable), plugin_option("dont_shutdown", "flag", "Whether to timeout when asked to shutdown.", - flag_option, flag_jsonfmt, &dont_shutdown), + flag_option, flag_jsonfmt, + &tlp->dont_shutdown), plugin_option_dynamic("dynamicopt", "int", "Set me!", - set_dynamic, u32_jsonfmt, &dynamic_opt), + set_dynamic, u32_jsonfmt, + &tlp->dynamic_opt), NULL); } From ad2ac928af5e9eef028a53a50e208754ce2cf735 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Wed, 7 Aug 2024 11:19:52 +0930 Subject: [PATCH 04/28] common: new parameter parsing routines. param_u16 (for delay). Signed-off-by: Rusty Russell --- common/json_param.c | 12 ++++++++++++ common/json_param.h | 5 +++++ 2 files changed, 17 insertions(+) diff --git a/common/json_param.c b/common/json_param.c index 6b6196fe3a3c..3a25e1684b2f 100644 --- a/common/json_param.c +++ b/common/json_param.c @@ -520,6 +520,18 @@ struct command_result *param_sha256(struct command *cmd, const char *name, "should be a 32 byte hex value"); } +struct command_result *param_u16(struct command *cmd, const char *name, + const char *buffer, const jsmntok_t *tok, + uint16_t **num) +{ + *num = tal(cmd, uint16_t); + if (json_to_u16(buffer, tok, *num)) + return NULL; + + return command_fail_badparam(cmd, name, buffer, tok, + "should be an unsigned 16 bit integer"); +} + struct command_result *param_u32(struct command *cmd, const char *name, const char *buffer, const jsmntok_t *tok, uint32_t **num) diff --git a/common/json_param.h b/common/json_param.h index a5b65a52c259..83e4f3b3a70a 100644 --- a/common/json_param.h +++ b/common/json_param.h @@ -219,6 +219,11 @@ struct command_result *param_sha256(struct command *cmd, const char *name, const char *buffer, const jsmntok_t *tok, struct sha256 **hash); +/* Extract number from this (may be a string, or a number literal) */ +struct command_result *param_u16(struct command *cmd, const char *name, + const char *buffer, const jsmntok_t *tok, + uint16_t **num); + /* Extract number from this (may be a string, or a number literal) */ struct command_result *param_u32(struct command *cmd, const char *name, const char *buffer, const jsmntok_t *tok, From 60f01897d53bc4bf0840303d64bd1149cc3cc637 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Wed, 7 Aug 2024 11:19:52 +0930 Subject: [PATCH 05/28] bitcoin: add short_channel_id_dir_eq. Signed-off-by: Rusty Russell --- bitcoin/short_channel_id.h | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/bitcoin/short_channel_id.h b/bitcoin/short_channel_id.h index 8dd2dacb5c60..cf382d693804 100644 --- a/bitcoin/short_channel_id.h +++ b/bitcoin/short_channel_id.h @@ -41,6 +41,12 @@ struct short_channel_id_dir { int dir; }; +static inline bool short_channel_id_dir_eq(const struct short_channel_id_dir *a, + const struct short_channel_id_dir *b) +{ + return short_channel_id_eq(a->scid, b->scid) && a->dir == b->dir; +} + static inline u32 short_channel_id_blocknum(struct short_channel_id scid) { return scid.u64 >> 40; From cfe5f05ee81c3184fe4a7d39d407af0b938e3452 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Wed, 7 Aug 2024 11:19:53 +0930 Subject: [PATCH 06/28] common: promote useful routines from renepay. Signed-off-by: Rusty Russell --- common/amount.h | 13 +++++++++++++ common/gossmap.h | 13 +++++++++++++ plugins/renepay/chan_extra.h | 25 ------------------------- plugins/renepay/flow.c | 4 ++-- plugins/renepay/mcf.c | 6 +++--- plugins/renepay/routebuilder.c | 4 ++-- 6 files changed, 33 insertions(+), 32 deletions(-) diff --git a/common/amount.h b/common/amount.h index 5c3d70e04a2e..e94e1d2d85a2 100644 --- a/common/amount.h +++ b/common/amount.h @@ -144,6 +144,19 @@ bool amount_msat_eq_sat(struct amount_msat msat, struct amount_sat sat); /* a / b */ double amount_msat_ratio(struct amount_msat a, struct amount_msat b); +/* min(a,b) and max(a,b) */ +static inline struct amount_msat amount_msat_min(struct amount_msat a, + struct amount_msat b) +{ + return amount_msat_less(a, b) ? a : b; +} + +static inline struct amount_msat amount_msat_max(struct amount_msat a, + struct amount_msat b) +{ + return amount_msat_greater(a, b) ? a : b; +} + /* Check whether this asset is actually the main / fee-paying asset of the * current chain. */ bool amount_asset_is_main(struct amount_asset *asset); diff --git a/common/gossmap.h b/common/gossmap.h index 61ed2bbbbf76..ade384f16465 100644 --- a/common/gossmap.h +++ b/common/gossmap.h @@ -242,6 +242,19 @@ bool gossmap_chan_has_capacity(const struct gossmap_chan *chan, int direction, struct amount_msat amount); +/* Convenience routines to get htlc min/max as amount_msat */ +static inline struct amount_msat +gossmap_chan_htlc_max(const struct gossmap_chan *chan, const int dir) +{ + return amount_msat(fp16_to_u64(chan->half[dir].htlc_max)); +} + +static inline struct amount_msat +gossmap_chan_htlc_min(const struct gossmap_chan *chan, const int dir) +{ + return amount_msat(fp16_to_u64(chan->half[dir].htlc_min)); +} + /* Remove a channel from the map (warning! realloc can move gossmap_chan * and gossmap_node ptrs!) */ void gossmap_remove_chan(struct gossmap *map, struct gossmap_chan *chan); diff --git a/plugins/renepay/chan_extra.h b/plugins/renepay/chan_extra.h index 7a3982358885..ba8227716f63 100644 --- a/plugins/renepay/chan_extra.h +++ b/plugins/renepay/chan_extra.h @@ -115,19 +115,6 @@ struct chan_extra *new_chan_extra(struct chan_extra_map *chan_extra_map, const struct short_channel_id scid, struct amount_msat capacity); -/* Helper to find the min of two amounts */ -static inline struct amount_msat amount_msat_min(struct amount_msat a, - struct amount_msat b) -{ - return amount_msat_less(a, b) ? a : b; -} -/* Helper to find the max of two amounts */ -static inline struct amount_msat amount_msat_max(struct amount_msat a, - struct amount_msat b) -{ - return amount_msat_greater(a, b) ? a : b; -} - /* Update the knowledge that this (channel,direction) can send x msat.*/ enum renepay_errorcode chan_extra_can_send(struct chan_extra_map *chan_extra_map, @@ -190,18 +177,6 @@ enum renepay_errorcode channel_liquidity(struct amount_msat *liquidity, const struct gossmap_chan *chan, const int dir); -/* Helpers to get the htlc_max and htlc_min of a channel. */ -static inline struct amount_msat -channel_htlc_max(const struct gossmap_chan *chan, const int dir) -{ - return amount_msat(fp16_to_u64(chan->half[dir].htlc_max)); -} -static inline struct amount_msat -channel_htlc_min(const struct gossmap_chan *chan, const int dir) -{ - return amount_msat(fp16_to_u64(chan->half[dir].htlc_min)); -} - /* inputs * @chan: a channel * @recv: how much can we send to this channels diff --git a/plugins/renepay/flow.c b/plugins/renepay/flow.c index 39b9864b10f8..fe40eb5f0692 100644 --- a/plugins/renepay/flow.c +++ b/plugins/renepay/flow.c @@ -93,7 +93,7 @@ flow_maximum_deliverable(struct amount_msat *max_deliverable, if(bad_channel)*bad_channel = flow->path[0]; return err; } - x = amount_msat_min(x, channel_htlc_max(flow->path[0], flow->dirs[0])); + x = amount_msat_min(x, gossmap_chan_htlc_max(flow->path[0], flow->dirs[0])); if(amount_msat_zero(x)) { @@ -127,7 +127,7 @@ flow_maximum_deliverable(struct amount_msat *max_deliverable, struct amount_msat x_new = amount_msat_min(forward_cap, liquidity_cap); x_new = amount_msat_min( - x_new, channel_htlc_max(flow->path[i], flow->dirs[i])); + x_new, gossmap_chan_htlc_max(flow->path[i], flow->dirs[i])); /* safety check: amounts decrease along the route */ assert(amount_msat_less_eq(x_new, x)); diff --git a/plugins/renepay/mcf.c b/plugins/renepay/mcf.c index 8c3a7d349da9..f615446a259f 100644 --- a/plugins/renepay/mcf.c +++ b/plugins/renepay/mcf.c @@ -497,7 +497,7 @@ static bool linearize_channel(const struct pay_parameters *params, /* An extra bound on capacity, here we use it to reduce the flow such * that it does not exceed htlcmax. */ s64 cap_on_capacity = - channel_htlc_max(c, dir).millisatoshis/1000; /* Raw: linearize_channel */ + gossmap_chan_htlc_max(c, dir).millisatoshis/1000; /* Raw: linearize_channel */ capacity[0]=a; cost[0]=0; @@ -1363,12 +1363,12 @@ get_flow_paths(const tal_t *ctx, const struct gossmap *gossmap, /* obtain the supremum htlc_min along the route */ sup_htlc_min = amount_msat_max( - sup_htlc_min, channel_htlc_min(c, dir)); + sup_htlc_min, gossmap_chan_htlc_min(c, dir)); /* obtain the infimum htlc_max along the route */ inf_htlc_max = amount_msat_min( - inf_htlc_max, channel_htlc_max(c, dir)); + inf_htlc_max, gossmap_chan_htlc_max(c, dir)); } s64 htlc_max=inf_htlc_max.millisatoshis/1000;/* Raw: need htlc_max in sats to do arithmetic operations.*/ diff --git a/plugins/renepay/routebuilder.c b/plugins/renepay/routebuilder.c index 188ebcde5c6a..43d97e400f5f 100644 --- a/plugins/renepay/routebuilder.c +++ b/plugins/renepay/routebuilder.c @@ -86,9 +86,9 @@ route_check_constraints(struct route *route, struct gossmap *gossmap, // check that we stay within the htlc max and min limits if (amount_msat_greater(hop->amount, - channel_htlc_max(chan, dir)) || + gossmap_chan_htlc_max(chan, dir)) || amount_msat_less(hop->amount, - channel_htlc_min(chan, dir))) { + gossmap_chan_htlc_min(chan, dir))) { bitmap_set_bit(disabled_bitmap, gossmap_chan_idx(gossmap, chan)); return RENEPAY_BAD_CHANNEL; From a611ec26b174379855e67a861cffb67a9e254b20 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Wed, 7 Aug 2024 11:19:53 +0930 Subject: [PATCH 07/28] askrene: skeleton which does JSON API. All the infrastructure and interfaces, but it doesn't do anything yet. Signed-off-by: Rusty Russell --- plugins/Makefile | 4 +- plugins/askrene/Makefile | 10 + plugins/askrene/askrene.c | 403 ++++++++++++++++++++++++++++++++++++++ plugins/askrene/askrene.h | 26 +++ 4 files changed, 442 insertions(+), 1 deletion(-) create mode 100644 plugins/askrene/Makefile create mode 100644 plugins/askrene/askrene.c create mode 100644 plugins/askrene/askrene.h diff --git a/plugins/Makefile b/plugins/Makefile index d996342dd921..398226f99ba3 100644 --- a/plugins/Makefile +++ b/plugins/Makefile @@ -100,7 +100,8 @@ C_PLUGINS := \ plugins/recover \ plugins/txprepare \ plugins/cln-renepay \ - plugins/spenderp + plugins/spenderp \ + plugins/cln-askrene PY_PLUGINS := \ plugins/clnrest/clnrest \ @@ -176,6 +177,7 @@ PLUGIN_COMMON_OBJS := \ wire/tlvstream.o \ wire/towire.o +include plugins/askrene/Makefile include plugins/bkpr/Makefile include plugins/renepay/Makefile diff --git a/plugins/askrene/Makefile b/plugins/askrene/Makefile new file mode 100644 index 000000000000..4b17a02f4e17 --- /dev/null +++ b/plugins/askrene/Makefile @@ -0,0 +1,10 @@ +PLUGIN_ASKRENE_SRC := plugins/askrene/askrene.c +PLUGIN_ASKRENE_HEADER := plugins/askrene/askrene.h +PLUGIN_ASKRENE_OBJS := $(PLUGIN_ASKRENE_SRC:.c=.o) + +$(PLUGIN_ASKRENE_OBJS): $(PLUGIN_ASKRENE_HEADER) + +ALL_C_SOURCES += $(PLUGIN_ASKRENE_SRC) +ALL_C_HEADERS += $(PLUGIN_ASKRENE_HEADER) + +plugins/cln-askrene: $(PLUGIN_ASKRENE_OBJS) $(PLUGIN_LIB_OBJS) $(PLUGIN_COMMON_OBJS) $(JSMN_OBJS) $(CCAN_OBJS) bitcoin/chainparams.o common/gossmap.o common/sciddir_or_pubkey.o common/gossmods_listpeerchannels.o common/fp16.o common/dijkstra.o common/bolt12.o common/bolt12_merkle.o wire/bolt12_wiregen.o wire/onion_wiregen.o diff --git a/plugins/askrene/askrene.c b/plugins/askrene/askrene.c new file mode 100644 index 000000000000..1b5d879c7e3f --- /dev/null +++ b/plugins/askrene/askrene.c @@ -0,0 +1,403 @@ +/* All your payment questions answered! + * + * This powerful oracle combines data from the network, and then + * determines optimal routes. + * + * When you feed it information, these are remembered as "layers", so you + * can ask questions with (or without) certain layers. + */ +#include "config.h" +#include +#include +#include +#include +#include +#include +#include +#include + +static struct askrene *get_askrene(struct plugin *plugin) +{ + return plugin_get_data(plugin, struct askrene); +} + +/* JSON helpers */ +static struct command_result *param_string_array(struct command *cmd, + const char *name, + const char *buffer, + const jsmntok_t *tok, + const char ***arr) +{ + size_t i; + const jsmntok_t *t; + + if (tok->type != JSMN_ARRAY) + return command_fail_badparam(cmd, name, buffer, tok, "should be an array"); + + *arr = tal_arr(cmd, const char *, tok->size); + json_for_each_arr(i, t, tok) { + if (t->type != JSMN_STRING) + return command_fail_badparam(cmd, name, buffer, t, "should be a string"); + (*arr)[i] = json_strdup(*arr, buffer, t); + } + return NULL; +} + +static bool json_to_zero_or_one(const char *buffer, const jsmntok_t *tok, int *num) +{ + u32 v32; + if (!json_to_u32(buffer, tok, &v32)) + return false; + if (v32 != 0 && v32 != 1) + return false; + *num = v32; + return true; +} + +static struct command_result *param_zero_or_one(struct command *cmd, + const char *name, + const char *buffer, + const jsmntok_t *tok, + int **num) +{ + *num = tal(cmd, int); + if (json_to_zero_or_one(buffer, tok, *num)) + return NULL; + + return command_fail_badparam(cmd, name, buffer, tok, + "should be 0 or 1"); +} + +struct reserve_path { + struct short_channel_id_dir *scidds; + struct amount_msat *amounts; +}; + +static struct command_result *parse_reserve_path(struct command *cmd, + const char *name, + const char *buffer, + const jsmntok_t *tok, + struct short_channel_id_dir *scidd, + struct amount_msat *amount) +{ + const char *err; + + err = json_scan(tmpctx, buffer, tok, "{short_channel_id:%,direction:%,amount_msat:%s}", + JSON_SCAN(json_to_short_channel_id, &scidd->scid), + JSON_SCAN(json_to_zero_or_one, &scidd->dir), + JSON_SCAN(json_to_msat, amount)); + if (err) + return command_fail_badparam(cmd, name, buffer, tok, err); + return NULL; +} + +static struct command_result *param_reserve_path(struct command *cmd, + const char *name, + const char *buffer, + const jsmntok_t *tok, + struct reserve_path **path) +{ + size_t i; + const jsmntok_t *t; + + if (tok->type != JSMN_ARRAY) + return command_fail_badparam(cmd, name, buffer, tok, "should be an array"); + + *path = tal(cmd, struct reserve_path); + (*path)->scidds = tal_arr(cmd, struct short_channel_id_dir, tok->size); + (*path)->amounts = tal_arr(cmd, struct amount_msat, tok->size); + json_for_each_arr(i, t, tok) { + struct command_result *ret; + + ret = parse_reserve_path(cmd, name, buffer, t, + &(*path)->scidds[i], + &(*path)->amounts[i]); + if (ret) + return ret; + } + return NULL; +} + +/* Returns an error message, or sets *routes */ +static const char *get_routes(struct command *cmd, + const struct node_id *source, + const struct node_id *dest, + struct amount_msat amount, + const char **layers, + struct route ***routes) +{ + /* FIXME: Do route here! This is a dummy, single "direct" route. */ + *routes = tal_arr(cmd, struct route *, 1); + (*routes)[0]->success_prob = 1; + (*routes)[0]->hops = tal_arr((*routes)[0], struct route_hop, 1); + (*routes)[0]->hops[0].scid.u64 = 0x0000010000020003ULL; + (*routes)[0]->hops[0].direction = 0; + (*routes)[0]->hops[0].node_id = *dest; + (*routes)[0]->hops[0].amount = amount; + (*routes)[0]->hops[0].delay = 6; + + return NULL; +} + +static struct command_result *json_getroutes(struct command *cmd, + const char *buffer, + const jsmntok_t *params) +{ + struct node_id *dest, *source; + const char **layers; + struct amount_msat *amount; + struct route **routes; + struct json_stream *response; + const char *err; + + if (!param(cmd, buffer, params, + p_req("source", param_node_id, &source), + p_req("destination", param_node_id, &dest), + p_req("amount_msat", param_msat, &amount), + p_req("layers", param_string_array, &layers), + NULL)) + return command_param_failed(); + + err = get_routes(cmd, source, dest, *amount, layers, &routes); + if (err) + return command_fail(cmd, PAY_ROUTE_NOT_FOUND, "%s", err); + + response = jsonrpc_stream_success(cmd); + json_object_start(response, "routes"); + json_array_start(response, "routes"); + for (size_t i = 0; i < tal_count(routes); i++) { + json_add_u64(response, "probability_ppm", (u64)(routes[i]->success_prob * 1000000)); + json_array_start(response, "path"); + for (size_t j = 0; j < tal_count(routes[i]->hops); j++) { + const struct route_hop *r = &routes[i]->hops[j]; + json_add_short_channel_id(response, "short_channel_id", r->scid); + json_add_u32(response, "direction", r->direction); + json_add_node_id(response, "node_id", &r->node_id); + json_add_amount_msat(response, "amount", r->amount); + json_add_u32(response, "delay", r->delay); + } + json_array_end(response); + } + json_array_end(response); + json_object_end(response); + return command_finished(cmd, response); +} + +static struct command_result *json_askrene_reserve(struct command *cmd, + const char *buffer, + const jsmntok_t *params) +{ + struct reserve_path *path; + struct json_stream *response; + + if (!param(cmd, buffer, params, + p_req("path", param_reserve_path, &path), + NULL)) + return command_param_failed(); + + response = jsonrpc_stream_success(cmd); + return command_finished(cmd, response); +} + +static struct command_result *json_askrene_unreserve(struct command *cmd, + const char *buffer, + const jsmntok_t *params) +{ + struct reserve_path *path; + struct json_stream *response; + + if (!param(cmd, buffer, params, + p_req("path", param_reserve_path, &path), + NULL)) + return command_param_failed(); + + response = jsonrpc_stream_success(cmd); + return command_finished(cmd, response); +} + +static struct command_result *json_askrene_create_channel(struct command *cmd, + const char *buffer, + const jsmntok_t *params) +{ + const char *layername; + struct node_id *src, *dst; + struct short_channel_id *scid; + struct amount_msat *capacity; + struct json_stream *response; + struct amount_msat *htlc_min, *htlc_max, *base_fee; + u32 *proportional_fee; + u16 *delay; + + if (!param_check(cmd, buffer, params, + p_req("layer", param_string, &layername), + p_req("source", param_node_id, &src), + p_req("destination", param_node_id, &dst), + p_req("short_channel_id", param_short_channel_id, &scid), + p_req("capacity_msat", param_msat, &capacity), + p_req("htlc_minimum_msat", param_msat, &htlc_min), + p_req("htlc_maximum_msat", param_msat, &htlc_max), + p_req("fee_base_msat", param_msat, &base_fee), + p_req("fee_proportional_millionths", param_u32, &proportional_fee), + p_req("delay", param_u16, &delay), + NULL)) + return command_param_failed(); + + if (command_check_only(cmd)) + return command_check_done(cmd); + + response = jsonrpc_stream_success(cmd); + return command_finished(cmd, response); +} + +static struct command_result *json_askrene_inform_channel(struct command *cmd, + const char *buffer, + const jsmntok_t *params) +{ + const char *layername; + struct short_channel_id *scid; + int *direction; + struct json_stream *response; + struct amount_msat *max, *min; + + if (!param_check(cmd, buffer, params, + p_req("layer", param_string, &layername), + p_req("short_channel_id", param_short_channel_id, &scid), + p_req("direction", param_zero_or_one, &direction), + p_opt("minimum_msat", param_msat, &min), + p_opt("maximum_msat", param_msat, &max), + NULL)) + return command_param_failed(); + + if ((!min && !max) || (min && max)) { + return command_fail(cmd, JSONRPC2_INVALID_PARAMS, + "Must specify exactly one of maximum_msat/minimum_msat"); + } + + if (command_check_only(cmd)) + return command_check_done(cmd); + + response = jsonrpc_stream_success(cmd); + return command_finished(cmd, response); +} + +static struct command_result *json_askrene_disable_node(struct command *cmd, + const char *buffer, + const jsmntok_t *params) +{ + struct node_id *node; + const char *layername; + struct json_stream *response; + + if (!param(cmd, buffer, params, + p_req("layer", param_string, &layername), + p_req("node", param_node_id, &node), + NULL)) + return command_param_failed(); + + response = jsonrpc_stream_success(cmd); + return command_finished(cmd, response); +} + +static struct command_result *json_askrene_listlayers(struct command *cmd, + const char *buffer, + const jsmntok_t *params) +{ + const char *layername; + struct json_stream *response; + + if (!param(cmd, buffer, params, + p_req("layer", param_string, &layername), + NULL)) + return command_param_failed(); + + response = jsonrpc_stream_success(cmd); + json_array_start(response, "layers"); + json_object_start(response, NULL); + json_add_string(response, "layer", layername); + json_array_start(response, "disabled_nodes"); + json_array_end(response); + json_array_start(response, "created_channels"); + json_array_end(response); + json_array_start(response, "capacities"); + json_array_end(response); + json_object_end(response); + json_array_end(response); + return command_finished(cmd, response); +} + +static struct command_result *json_askrene_age(struct command *cmd, + const char *buffer, + const jsmntok_t *params) +{ + const char *layername; + struct json_stream *response; + u64 *cutoff; + + if (!param(cmd, buffer, params, + p_req("layer", param_string, &layername), + p_req("cutoff", param_u64, &cutoff), + NULL)) + return command_param_failed(); + + response = jsonrpc_stream_success(cmd); + json_add_string(response, "layer", layername); + return command_finished(cmd, response); +} + +static const struct plugin_command commands[] = { + { + "getroutes", + json_getroutes, + }, + { + "askrene-reserve", + json_askrene_reserve, + }, + { + "askrene-unreserve", + json_askrene_unreserve, + }, + { + "askrene-disable-node", + json_askrene_disable_node, + }, + { + "askrene-create-channel", + json_askrene_create_channel, + }, + { + "askrene-inform-channel", + json_askrene_inform_channel, + }, + { + "askrene-listlayers", + json_askrene_listlayers, + }, + { + "askrene-age", + json_askrene_age, + }, +}; + +static const char *init(struct plugin *plugin, + const char *buf UNUSED, const jsmntok_t *config UNUSED) +{ + struct askrene *askrene = tal(plugin, struct askrene); + askrene->plugin = plugin; + askrene->gossmap = gossmap_load(askrene, GOSSIP_STORE_FILENAME, NULL); + + if (!askrene->gossmap) + plugin_err(plugin, "Could not load gossmap %s: %s", + GOSSIP_STORE_FILENAME, strerror(errno)); + + plugin_set_data(plugin, askrene); + (void)get_askrene(plugin); + return NULL; +} + +int main(int argc, char *argv[]) +{ + setup_locale(); + plugin_main(argv, init, NULL, PLUGIN_RESTARTABLE, true, NULL, commands, ARRAY_SIZE(commands), + NULL, 0, NULL, 0, NULL, 0, NULL); +} diff --git a/plugins/askrene/askrene.h b/plugins/askrene/askrene.h new file mode 100644 index 000000000000..706c7c1abe1e --- /dev/null +++ b/plugins/askrene/askrene.h @@ -0,0 +1,26 @@ +#ifndef LIGHTNING_PLUGINS_ASKRENE_ASKRENE_H +#define LIGHTNING_PLUGINS_ASKRENE_ASKRENE_H +#include "config.h" + +/* We reserve a path being used. This records how many and how much */ +struct reserve { + size_t num_htlcs; + struct short_channel_id_dir sciddir; + struct amount_msat amount; +}; + +/* A single route. */ +struct route { + /* Actual path to take */ + struct route_hop *hops; + /* Probability estimate (0-1) */ + double success_prob; +}; + +/* Grab-bag of "globals" for this plugin */ +struct askrene { + struct plugin *plugin; + struct gossmap *gossmap; +}; + +#endif /* LIGHTNING_PLUGINS_ASKRENE_ASKRENE_H */ From d786110cd27f2afe8142583e7b134e36e3fb5258 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Wed, 7 Aug 2024 11:19:53 +0930 Subject: [PATCH 08/28] askrene: add layers infrastructure. These are the repositories of all information. Signed-off-by: Rusty Russell Header from folded patch 'layers-fixup.patch': fixup! askrene: add layers infrastructure. --- plugins/askrene/Makefile | 4 +- plugins/askrene/askrene.c | 104 +++++++++-- plugins/askrene/askrene.h | 5 + plugins/askrene/layer.c | 376 ++++++++++++++++++++++++++++++++++++++ plugins/askrene/layer.h | 104 +++++++++++ 5 files changed, 575 insertions(+), 18 deletions(-) create mode 100644 plugins/askrene/layer.c create mode 100644 plugins/askrene/layer.h diff --git a/plugins/askrene/Makefile b/plugins/askrene/Makefile index 4b17a02f4e17..dfc719db89ac 100644 --- a/plugins/askrene/Makefile +++ b/plugins/askrene/Makefile @@ -1,5 +1,5 @@ -PLUGIN_ASKRENE_SRC := plugins/askrene/askrene.c -PLUGIN_ASKRENE_HEADER := plugins/askrene/askrene.h +PLUGIN_ASKRENE_SRC := plugins/askrene/askrene.c plugins/askrene/layer.c +PLUGIN_ASKRENE_HEADER := plugins/askrene/askrene.h plugins/askrene/layer.h PLUGIN_ASKRENE_OBJS := $(PLUGIN_ASKRENE_SRC:.c=.o) $(PLUGIN_ASKRENE_OBJS): $(PLUGIN_ASKRENE_HEADER) diff --git a/plugins/askrene/askrene.c b/plugins/askrene/askrene.c index 1b5d879c7e3f..8011d8f63242 100644 --- a/plugins/askrene/askrene.c +++ b/plugins/askrene/askrene.c @@ -8,12 +8,14 @@ */ #include "config.h" #include +#include #include #include #include #include #include #include +#include #include static struct askrene *get_askrene(struct plugin *plugin) @@ -43,6 +45,24 @@ static struct command_result *param_string_array(struct command *cmd, return NULL; } +static struct command_result *param_known_layer(struct command *cmd, + const char *name, + const char *buffer, + const jsmntok_t *tok, + struct layer **layer) +{ + const char *layername; + struct command_result *ret = param_string(cmd, name, buffer, tok, &layername); + if (ret) + return ret; + + *layer = find_layer(get_askrene(cmd->plugin), layername); + tal_free(layername); + if (!*layer) + return command_fail_badparam(cmd, name, buffer, tok, "Unknown layer"); + return NULL; +} + static bool json_to_zero_or_one(const char *buffer, const jsmntok_t *tok, int *num) { u32 v32; @@ -220,6 +240,8 @@ static struct command_result *json_askrene_create_channel(struct command *cmd, const jsmntok_t *params) { const char *layername; + struct layer *layer; + const struct local_channel *lc; struct node_id *src, *dst; struct short_channel_id *scid; struct amount_msat *capacity; @@ -227,6 +249,7 @@ static struct command_result *json_askrene_create_channel(struct command *cmd, struct amount_msat *htlc_min, *htlc_max, *base_fee; u32 *proportional_fee; u16 *delay; + struct askrene *askrene = get_askrene(cmd->plugin); if (!param_check(cmd, buffer, params, p_req("layer", param_string, &layername), @@ -242,9 +265,27 @@ static struct command_result *json_askrene_create_channel(struct command *cmd, NULL)) return command_param_failed(); + /* If it exists, it must match */ + layer = find_layer(askrene, layername); + if (layer) { + lc = layer_find_local_channel(layer, *scid); + if (lc && !layer_check_local_channel(lc, src, dst, *capacity)) { + return command_fail(cmd, JSONRPC2_INVALID_PARAMS, + "channel already exists with different values!"); + } + } else + lc = NULL; + if (command_check_only(cmd)) return command_check_done(cmd); + if (!layer) + layer = new_layer(askrene, layername); + + layer_update_local_channel(layer, src, dst, *scid, *capacity, + *base_fee, *proportional_fee, *delay, + *htlc_min, *htlc_max); + response = jsonrpc_stream_success(cmd); return command_finished(cmd, response); } @@ -253,11 +294,15 @@ static struct command_result *json_askrene_inform_channel(struct command *cmd, const char *buffer, const jsmntok_t *params) { + struct layer *layer; const char *layername; struct short_channel_id *scid; int *direction; struct json_stream *response; struct amount_msat *max, *min; + const struct constraint *c; + struct short_channel_id_dir scidd; + struct askrene *askrene = get_askrene(cmd->plugin); if (!param_check(cmd, buffer, params, p_req("layer", param_string, &layername), @@ -276,7 +321,23 @@ static struct command_result *json_askrene_inform_channel(struct command *cmd, if (command_check_only(cmd)) return command_check_done(cmd); + layer = find_layer(askrene, layername); + if (!layer) + layer = new_layer(askrene, layername); + + /* Calls expect a convenient short_channel_id_dir struct */ + scidd.scid = *scid; + scidd.dir = *direction; + + if (min) { + c = layer_update_constraint(layer, &scidd, CONSTRAINT_MIN, + time_now().ts.tv_sec, *min); + } else { + c = layer_update_constraint(layer, &scidd, CONSTRAINT_MAX, + time_now().ts.tv_sec, *max); + } response = jsonrpc_stream_success(cmd); + json_add_constraint(response, "constraint", c, layer); return command_finished(cmd, response); } @@ -286,7 +347,9 @@ static struct command_result *json_askrene_disable_node(struct command *cmd, { struct node_id *node; const char *layername; + struct layer *layer; struct json_stream *response; + struct askrene *askrene = get_askrene(cmd->plugin); if (!param(cmd, buffer, params, p_req("layer", param_string, &layername), @@ -294,6 +357,14 @@ static struct command_result *json_askrene_disable_node(struct command *cmd, NULL)) return command_param_failed(); + layer = find_layer(askrene, layername); + if (!layer) + layer = new_layer(askrene, layername); + + /* We save this in the layer, because they want us to disable all the channels + * to the node at *use* time (a new channel might be gossiped!). */ + layer_add_disabled_node(layer, node); + response = jsonrpc_stream_success(cmd); return command_finished(cmd, response); } @@ -302,26 +373,17 @@ static struct command_result *json_askrene_listlayers(struct command *cmd, const char *buffer, const jsmntok_t *params) { + struct askrene *askrene = get_askrene(cmd->plugin); const char *layername; struct json_stream *response; if (!param(cmd, buffer, params, - p_req("layer", param_string, &layername), + p_opt("layer", param_string, &layername), NULL)) return command_param_failed(); response = jsonrpc_stream_success(cmd); - json_array_start(response, "layers"); - json_object_start(response, NULL); - json_add_string(response, "layer", layername); - json_array_start(response, "disabled_nodes"); - json_array_end(response); - json_array_start(response, "created_channels"); - json_array_end(response); - json_array_start(response, "capacities"); - json_array_end(response); - json_object_end(response); - json_array_end(response); + json_add_layers(response, askrene, "layers", layername); return command_finished(cmd, response); } @@ -329,18 +391,22 @@ static struct command_result *json_askrene_age(struct command *cmd, const char *buffer, const jsmntok_t *params) { - const char *layername; + struct layer *layer; struct json_stream *response; u64 *cutoff; + size_t num_removed; if (!param(cmd, buffer, params, - p_req("layer", param_string, &layername), + p_req("layer", param_known_layer, &layer), p_req("cutoff", param_u64, &cutoff), NULL)) return command_param_failed(); + num_removed = layer_trim_constraints(layer, *cutoff); + response = jsonrpc_stream_success(cmd); - json_add_string(response, "layer", layername); + json_add_string(response, "layer", layer_name(layer)); + json_add_u64(response, "num_removed", num_removed); return command_finished(cmd, response); } @@ -379,11 +445,17 @@ static const struct plugin_command commands[] = { }, }; +static void askrene_markmem(struct plugin *plugin, struct htable *memtable) +{ + layer_memleak_mark(get_askrene(plugin), memtable); +} + static const char *init(struct plugin *plugin, const char *buf UNUSED, const jsmntok_t *config UNUSED) { struct askrene *askrene = tal(plugin, struct askrene); askrene->plugin = plugin; + list_head_init(&askrene->layers); askrene->gossmap = gossmap_load(askrene, GOSSIP_STORE_FILENAME, NULL); if (!askrene->gossmap) @@ -391,7 +463,7 @@ static const char *init(struct plugin *plugin, GOSSIP_STORE_FILENAME, strerror(errno)); plugin_set_data(plugin, askrene); - (void)get_askrene(plugin); + plugin_set_memleak_handler(plugin, askrene_markmem); return NULL; } diff --git a/plugins/askrene/askrene.h b/plugins/askrene/askrene.h index 706c7c1abe1e..cd8d0f4911dc 100644 --- a/plugins/askrene/askrene.h +++ b/plugins/askrene/askrene.h @@ -1,6 +1,9 @@ #ifndef LIGHTNING_PLUGINS_ASKRENE_ASKRENE_H #define LIGHTNING_PLUGINS_ASKRENE_ASKRENE_H #include "config.h" +#include +#include +#include /* We reserve a path being used. This records how many and how much */ struct reserve { @@ -21,6 +24,8 @@ struct route { struct askrene { struct plugin *plugin; struct gossmap *gossmap; + /* List of layers */ + struct list_head layers; }; #endif /* LIGHTNING_PLUGINS_ASKRENE_ASKRENE_H */ diff --git a/plugins/askrene/layer.c b/plugins/askrene/layer.c new file mode 100644 index 000000000000..28675bfd8841 --- /dev/null +++ b/plugins/askrene/layer.c @@ -0,0 +1,376 @@ +#include "config.h" +#include +#include +#include +#include +#include +#include +#include + +/* A channels which doesn't (necessarily) exist in the gossmap. */ +struct local_channel { + /* Canonical order, n1 < n2 */ + struct node_id n1, n2; + struct short_channel_id scid; + struct amount_msat capacity; + + struct added_channel_half { + /* Other fields only valid if this is true */ + bool enabled; + u16 delay; + u32 proportional_fee; + struct amount_msat base_fee; + struct amount_msat htlc_min, htlc_max; + } half[2]; +}; + +static const struct constraint_key * +constraint_key(const struct constraint *c) +{ + return &c->key; +} + +static size_t hash_constraint_key(const struct constraint_key *key) +{ + /* scids cost money to generate, so simple hash works here */ + return (key->scidd.scid.u64 >> 32) ^ (key->scidd.scid.u64) + ^ (key->scidd.dir << 1) ^ (key->type); +} + +static inline bool constraint_eq_key(const struct constraint *c, + const struct constraint_key *key) +{ + return short_channel_id_dir_eq(&key->scidd, &c->key.scidd) && key->type == c->key.type; +} + +HTABLE_DEFINE_TYPE(struct constraint, constraint_key, hash_constraint_key, + constraint_eq_key, constraint_hash); + +static struct short_channel_id +local_channel_scid(const struct local_channel *lc) +{ + return lc->scid; +} + +static size_t hash_scid(const struct short_channel_id scid) +{ + /* scids cost money to generate, so simple hash works here */ + return (scid.u64 >> 32) ^ (scid.u64 >> 16) ^ scid.u64; +} + +static inline bool local_channel_eq_scid(const struct local_channel *lc, + const struct short_channel_id scid) +{ + return short_channel_id_eq(scid, lc->scid); +} + +HTABLE_DEFINE_TYPE(struct local_channel, local_channel_scid, hash_scid, + local_channel_eq_scid, local_channel_hash); + +struct layer { + /* Inside global list of layers */ + struct list_node list; + + /* Unique identifiers */ + const char *name; + + /* Completely made up local additions, indexed by scid */ + struct local_channel_hash *local_channels; + + /* Additional info, indexed by scid+dir */ + struct constraint_hash *constraints; + + /* Nodes to completely disable (tal_arr) */ + struct node_id *disabled_nodes; +}; + +struct layer *new_layer(struct askrene *askrene, const char *name) +{ + struct layer *l = tal(askrene, struct layer); + + l->name = tal_strdup(l, name); + l->local_channels = tal(l, struct local_channel_hash); + local_channel_hash_init(l->local_channels); + l->constraints = tal(l, struct constraint_hash); + constraint_hash_init(l->constraints); + l->disabled_nodes = tal_arr(l, struct node_id, 0); + + list_add(&askrene->layers, &l->list); + return l; +} + +/* Swap if necessary to make into BOLT-7 order. Return direction. */ +static int canonicalize_node_order(const struct node_id **n1, + const struct node_id **n2) +{ + const struct node_id *tmp; + + if (node_id_cmp(*n1, *n2) < 0) + return 0; + tmp = *n2; + *n2 = *n1; + *n1 = tmp; + return 1; +} + +struct layer *find_layer(struct askrene *askrene, const char *name) +{ + struct layer *l; + list_for_each(&askrene->layers, l, list) { + if (streq(l->name, name)) + return l; + } + return NULL; +} + +const char *layer_name(const struct layer *layer) +{ + return layer->name; +} + +static struct local_channel *new_local_channel(struct layer *layer, + const struct node_id *n1, + const struct node_id *n2, + struct short_channel_id scid, + struct amount_msat capacity) +{ + struct local_channel *lc = tal(layer, struct local_channel); + lc->n1 = *n1; + lc->n2 = *n2; + lc->scid = scid; + lc->capacity = capacity; + + for (size_t i = 0; i < ARRAY_SIZE(lc->half); i++) + lc->half[i].enabled = false; + + local_channel_hash_add(layer->local_channels, lc); + return lc; +} + +bool layer_check_local_channel(const struct local_channel *lc, + const struct node_id *n1, + const struct node_id *n2, + struct amount_msat capacity) +{ + canonicalize_node_order(&n1, &n2); + return node_id_eq(&lc->n1, n1) + && node_id_eq(&lc->n2, n2) + && amount_msat_eq(lc->capacity, capacity); +} + +/* Update a local channel to a layer: fails if you try to change capacity or nodes! */ +void layer_update_local_channel(struct layer *layer, + const struct node_id *src, + const struct node_id *dst, + struct short_channel_id scid, + struct amount_msat capacity, + struct amount_msat base_fee, + u32 proportional_fee, + u16 delay, + struct amount_msat htlc_min, + struct amount_msat htlc_max) +{ + struct local_channel *lc = local_channel_hash_get(layer->local_channels, scid); + int dir = canonicalize_node_order(&src, &dst); + + if (lc) { + assert(layer_check_local_channel(lc, src, dst, capacity)); + } else { + lc = new_local_channel(layer, src, dst, scid, capacity); + } + + lc->half[dir].enabled = true; + lc->half[dir].htlc_min = htlc_min; + lc->half[dir].htlc_max = htlc_max; + lc->half[dir].base_fee = base_fee; + lc->half[dir].proportional_fee = proportional_fee; + lc->half[dir].delay = delay; +} + +struct amount_msat local_channel_capacity(const struct local_channel *lc) +{ + return lc->capacity; +} + +const struct local_channel *layer_find_local_channel(const struct layer *layer, + struct short_channel_id scid) +{ + return local_channel_hash_get(layer->local_channels, scid); +} + +static struct constraint *layer_find_constraint_nonconst(const struct layer *layer, + const struct short_channel_id_dir *scidd, + enum constraint_type type) +{ + struct constraint_key k = { *scidd, type }; + return constraint_hash_get(layer->constraints, &k); +} + +/* Public one returns const */ +const struct constraint *layer_find_constraint(const struct layer *layer, + const struct short_channel_id_dir *scidd, + enum constraint_type type) +{ + return layer_find_constraint_nonconst(layer, scidd, type); +} + +const struct constraint *layer_update_constraint(struct layer *layer, + const struct short_channel_id_dir *scidd, + enum constraint_type type, + u64 timestamp, + struct amount_msat limit) +{ + struct constraint *c = layer_find_constraint_nonconst(layer, scidd, type); + if (!c) { + c = tal(layer, struct constraint); + c->key.scidd = *scidd; + c->key.type = type; + c->limit = limit; + constraint_hash_add(layer->constraints, c); + } else { + switch (type) { + case CONSTRAINT_MIN: + /* Increase minimum? */ + if (amount_msat_greater(limit, c->limit)) + c->limit = limit; + break; + case CONSTRAINT_MAX: + /* Decrease maximum? */ + if (amount_msat_less(limit, c->limit)) + c->limit = limit; + break; + } + } + c->timestamp = timestamp; + return c; +} + +size_t layer_trim_constraints(struct layer *layer, u64 cutoff) +{ + size_t num_removed = 0; + struct constraint_hash_iter conit; + struct constraint *con; + + for (con = constraint_hash_first(layer->constraints, &conit); + con; + con = constraint_hash_next(layer->constraints, &conit)) { + if (con->timestamp < cutoff) { + constraint_hash_delval(layer->constraints, &conit); + tal_free(con); + num_removed++; + } + } + return num_removed; +} + +void layer_add_disabled_node(struct layer *layer, const struct node_id *node) +{ + tal_arr_expand(&layer->disabled_nodes, *node); +} + +static void json_add_local_channel(struct json_stream *response, + const char *fieldname, + const struct local_channel *lc, + int dir) +{ + json_object_start(response, fieldname); + + if (dir == 0) { + json_add_node_id(response, "source", &lc->n1); + json_add_node_id(response, "destination", &lc->n2); + } else { + json_add_node_id(response, "source", &lc->n2); + json_add_node_id(response, "destination", &lc->n1); + } + json_add_short_channel_id(response, "short_channel_id", lc->scid); + json_add_amount_msat(response, "capacity_msat", lc->capacity); + json_add_amount_msat(response, "htlc_minimum_msat", lc->half[dir].htlc_min); + json_add_amount_msat(response, "htlc_maximum_msat", lc->half[dir].htlc_max); + json_add_amount_msat(response, "fee_base_msat", lc->half[dir].base_fee); + json_add_u32(response, "fee_proportional_millionths", lc->half[dir].proportional_fee); + json_add_u32(response, "delay", lc->half[dir].delay); + + json_object_end(response); +} + +void json_add_constraint(struct json_stream *js, + const char *fieldname, + const struct constraint *c, + const struct layer *layer) +{ + json_object_start(js, fieldname); + if (layer) + json_add_string(js, "layer", layer->name); + json_add_short_channel_id(js, "short_channel_id", c->key.scidd.scid); + json_add_u32(js, "direction", c->key.scidd.dir); + json_add_u64(js, "timestamp", c->timestamp); + switch (c->key.type) { + case CONSTRAINT_MIN: + json_add_amount_msat(js, "minimum_msat", c->limit); + break; + case CONSTRAINT_MAX: + json_add_amount_msat(js, "maximum_msat", c->limit); + break; + } + json_object_end(js); +} + +static void json_add_layer(struct json_stream *js, + const char *fieldname, + const struct layer *layer) +{ + struct local_channel_hash_iter lcit; + const struct local_channel *lc; + struct constraint_hash_iter conit; + const struct constraint *c; + + json_object_start(js, fieldname); + json_add_string(js, "layer", layer->name); + json_array_start(js, "disabled_nodes"); + for (size_t i = 0; i < tal_count(layer->disabled_nodes); i++) + json_add_node_id(js, NULL, &layer->disabled_nodes[i]); + json_array_end(js); + json_array_start(js, "created_channels"); + for (lc = local_channel_hash_first(layer->local_channels, &lcit); + lc; + lc = local_channel_hash_next(layer->local_channels, &lcit)) { + for (size_t i = 0; i < ARRAY_SIZE(lc->half); i++) { + if (lc->half[i].enabled) + json_add_local_channel(js, NULL, lc, i); + } + } + json_array_end(js); + json_array_start(js, "constraints"); + for (c = constraint_hash_first(layer->constraints, &conit); + c; + c = constraint_hash_next(layer->constraints, &conit)) { + json_add_constraint(js, NULL, c, NULL); + } + json_array_end(js); + json_object_end(js); +} + +void json_add_layers(struct json_stream *js, + struct askrene *askrene, + const char *fieldname, + const char *layername) +{ + struct layer *l; + + json_array_start(js, fieldname); + list_for_each(&askrene->layers, l, list) { + if (layername && !streq(l->name, layername)) + continue; + json_add_layer(js, NULL, l); + } + json_array_end(js); +} + +void layer_memleak_mark(struct askrene *askrene, struct htable *memtable) +{ + struct layer *l; + list_for_each(&askrene->layers, l, list) { + memleak_scan_htable(memtable, &l->constraints->raw); + memleak_scan_htable(memtable, &l->local_channels->raw); + } +} diff --git a/plugins/askrene/layer.h b/plugins/askrene/layer.h new file mode 100644 index 000000000000..2eb5cc412ceb --- /dev/null +++ b/plugins/askrene/layer.h @@ -0,0 +1,104 @@ +#ifndef LIGHTNING_PLUGINS_ASKRENE_LAYER_H +#define LIGHTNING_PLUGINS_ASKRENE_LAYER_H +/* A layer is the group of information maintained by askrene. The caller + * specifies which layers to use when asking for a route, and tell askrene + * what layer to add new information to. + * + * Layers can be used to shape local decisions (for this payment, add these + * connections, or disable all connections to this node). You can also, + * in theory, export a layer, or import a layer from another source, to see + * what the results are when that layer is included. */ +#include "config.h" +#include +#include +#include + +struct askrene; +struct layer; +struct json_stream; + +enum constraint_type { + CONSTRAINT_MIN, + CONSTRAINT_MAX, +}; + +struct constraint_key { + struct short_channel_id_dir scidd; + enum constraint_type type; +}; + +/* A constraint reflects something we learned about a channel */ +struct constraint { + struct constraint_key key; + /* Time this constraint was last updated */ + u64 timestamp; + struct amount_msat limit; +}; + +/* Look up a layer by name. */ +struct layer *find_layer(struct askrene *askrene, const char *name); + +/* Create new layer by name. */ +struct layer *new_layer(struct askrene *askrene, const char *name); + +/* Get the name of the layer */ +const char *layer_name(const struct layer *layer); + +/* Find a local channel in a layer */ +const struct local_channel *layer_find_local_channel(const struct layer *layer, + struct short_channel_id scid); + +/* Get capacity of that channel. */ +struct amount_msat local_channel_capacity(const struct local_channel *lc); + +/* Check local channel matches these */ +bool layer_check_local_channel(const struct local_channel *lc, + const struct node_id *n1, + const struct node_id *n2, + struct amount_msat capacity); + +/* Update a local channel to a layer: fails if you try to change capacity or nodes! */ +void layer_update_local_channel(struct layer *layer, + const struct node_id *src, + const struct node_id *dst, + struct short_channel_id scid, + struct amount_msat capacity, + struct amount_msat base_fee, + u32 proportional_fee, + u16 delay, + struct amount_msat htlc_min, + struct amount_msat htlc_max); + +/* Find a constraint in a layer. */ +const struct constraint *layer_find_constraint(const struct layer *layer, + const struct short_channel_id_dir *scidd, + enum constraint_type type); + +/* Add/update a constraint on a layer. */ +const struct constraint *layer_update_constraint(struct layer *layer, + const struct short_channel_id_dir *scidd, + enum constraint_type type, + u64 timestamp, + struct amount_msat limit); + +/* Remove constraints older then cutoff: returns num removed. */ +size_t layer_trim_constraints(struct layer *layer, u64 cutoff); + +/* Add a disabled node to a layer. */ +void layer_add_disabled_node(struct layer *layer, const struct node_id *node); + +/* Print out a json object per layer, or all if layer is NULL */ +void json_add_layers(struct json_stream *js, + struct askrene *askrene, + const char *fieldname, + const char *layername); + +/* Print a single constraint */ +void json_add_constraint(struct json_stream *js, + const char *fieldname, + const struct constraint *c, + const struct layer *layer); + +/* Scan for memleaks */ +void layer_memleak_mark(struct askrene *askrene, struct htable *memtable); +#endif /* LIGHTNING_PLUGINS_ASKRENE_LAYER_H */ From 11fd7842617871198633ef6142a888484d7262fb Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Wed, 7 Aug 2024 11:19:53 +0930 Subject: [PATCH 09/28] askrene: reservation implementation. They tell us what paths they're using, so we can adjust capacity estimates accordingly. Signed-off-by: Rusty Russell Header from folded patch 'reserve-fixup.patch': fixup! askrene: reservation implementation. --- plugins/askrene/Makefile | 4 +- plugins/askrene/askrene.c | 31 ++++++++++ plugins/askrene/askrene.h | 9 +-- plugins/askrene/reserve.c | 119 ++++++++++++++++++++++++++++++++++++++ plugins/askrene/reserve.h | 38 ++++++++++++ 5 files changed, 192 insertions(+), 9 deletions(-) create mode 100644 plugins/askrene/reserve.c create mode 100644 plugins/askrene/reserve.h diff --git a/plugins/askrene/Makefile b/plugins/askrene/Makefile index dfc719db89ac..228390068841 100644 --- a/plugins/askrene/Makefile +++ b/plugins/askrene/Makefile @@ -1,5 +1,5 @@ -PLUGIN_ASKRENE_SRC := plugins/askrene/askrene.c plugins/askrene/layer.c -PLUGIN_ASKRENE_HEADER := plugins/askrene/askrene.h plugins/askrene/layer.h +PLUGIN_ASKRENE_SRC := plugins/askrene/askrene.c plugins/askrene/layer.c plugins/askrene/reserve.c +PLUGIN_ASKRENE_HEADER := plugins/askrene/askrene.h plugins/askrene/layer.h plugins/askrene/reserve.h PLUGIN_ASKRENE_OBJS := $(PLUGIN_ASKRENE_SRC:.c=.o) $(PLUGIN_ASKRENE_OBJS): $(PLUGIN_ASKRENE_HEADER) diff --git a/plugins/askrene/askrene.c b/plugins/askrene/askrene.c index 8011d8f63242..d561d15f9503 100644 --- a/plugins/askrene/askrene.c +++ b/plugins/askrene/askrene.c @@ -16,6 +16,7 @@ #include #include #include +#include #include static struct askrene *get_askrene(struct plugin *plugin) @@ -209,12 +210,26 @@ static struct command_result *json_askrene_reserve(struct command *cmd, { struct reserve_path *path; struct json_stream *response; + size_t num; + struct askrene *askrene = get_askrene(cmd->plugin); if (!param(cmd, buffer, params, p_req("path", param_reserve_path, &path), NULL)) return command_param_failed(); + num = reserves_add(askrene->reserved, path->scidds, path->amounts, + tal_count(path->scidds)); + if (num != tal_count(path->scidds)) { + const struct reserve *r = find_reserve(askrene->reserved, &path->scidds[num]); + return command_fail(cmd, JSONRPC2_INVALID_PARAMS, + "Overflow reserving %zu: %s amount %s (%s reserved already)", + num, + fmt_short_channel_id_dir(tmpctx, &path->scidds[num]), + fmt_amount_msat(tmpctx, path->amounts[num]), + r ? fmt_amount_msat(tmpctx, r->amount) : "none"); + } + response = jsonrpc_stream_success(cmd); return command_finished(cmd, response); } @@ -225,12 +240,27 @@ static struct command_result *json_askrene_unreserve(struct command *cmd, { struct reserve_path *path; struct json_stream *response; + size_t num; + struct askrene *askrene = get_askrene(cmd->plugin); if (!param(cmd, buffer, params, p_req("path", param_reserve_path, &path), NULL)) return command_param_failed(); + num = reserves_remove(askrene->reserved, path->scidds, path->amounts, + tal_count(path->scidds)); + if (num != tal_count(path->scidds)) { + const struct reserve *r = find_reserve(askrene->reserved, &path->scidds[num]); + return command_fail(cmd, JSONRPC2_INVALID_PARAMS, + "Underflow unreserving %zu: %s amount %s (%zu reserved, amount %s)", + num, + fmt_short_channel_id_dir(tmpctx, &path->scidds[num]), + fmt_amount_msat(tmpctx, path->amounts[num]), + r ? r->num_htlcs : 0, + r ? fmt_amount_msat(tmpctx, r->amount) : "none"); + } + response = jsonrpc_stream_success(cmd); return command_finished(cmd, response); } @@ -456,6 +486,7 @@ static const char *init(struct plugin *plugin, struct askrene *askrene = tal(plugin, struct askrene); askrene->plugin = plugin; list_head_init(&askrene->layers); + askrene->reserved = new_reserve_hash(askrene); askrene->gossmap = gossmap_load(askrene, GOSSIP_STORE_FILENAME, NULL); if (!askrene->gossmap) diff --git a/plugins/askrene/askrene.h b/plugins/askrene/askrene.h index cd8d0f4911dc..02d9e54fdcce 100644 --- a/plugins/askrene/askrene.h +++ b/plugins/askrene/askrene.h @@ -5,13 +5,6 @@ #include #include -/* We reserve a path being used. This records how many and how much */ -struct reserve { - size_t num_htlcs; - struct short_channel_id_dir sciddir; - struct amount_msat amount; -}; - /* A single route. */ struct route { /* Actual path to take */ @@ -26,6 +19,8 @@ struct askrene { struct gossmap *gossmap; /* List of layers */ struct list_head layers; + /* In-flight payment attempts */ + struct reserve_hash *reserved; }; #endif /* LIGHTNING_PLUGINS_ASKRENE_ASKRENE_H */ diff --git a/plugins/askrene/reserve.c b/plugins/askrene/reserve.c new file mode 100644 index 000000000000..19ab56c302e4 --- /dev/null +++ b/plugins/askrene/reserve.c @@ -0,0 +1,119 @@ +#include "config.h" +#include +#include +#include + +/* Hash table for reservations */ +static const struct short_channel_id_dir * +reserve_scidd(const struct reserve *r) +{ + return &r->scidd; +} + +static size_t hash_scidd(const struct short_channel_id_dir *scidd) +{ + /* scids cost money to generate, so simple hash works here */ + return (scidd->scid.u64 >> 32) ^ (scidd->scid.u64 >> 16) ^ (scidd->scid.u64 << 1) ^ scidd->dir; +} + +static bool reserve_eq_scidd(const struct reserve *r, + const struct short_channel_id_dir *scidd) +{ + return short_channel_id_dir_eq(scidd, &r->scidd); +} + +HTABLE_DEFINE_TYPE(struct reserve, reserve_scidd, hash_scidd, + reserve_eq_scidd, reserve_hash); + +struct reserve_hash *new_reserve_hash(const tal_t *ctx) +{ + struct reserve_hash *reserved = tal(ctx, struct reserve_hash); + reserve_hash_init(reserved); + return reserved; +} + +/* Find a reservation for this scidd (if any!) */ +const struct reserve *find_reserve(const struct reserve_hash *reserved, + const struct short_channel_id_dir *scidd) +{ + return reserve_hash_get(reserved, scidd); +} + +/* Create a new (empty) reservation */ +static struct reserve *new_reserve(struct reserve_hash *reserved, + const struct short_channel_id_dir *scidd) +{ + struct reserve *r = tal(reserved, struct reserve); + + r->num_htlcs = 0; + r->amount = AMOUNT_MSAT(0); + r->scidd = *scidd; + + reserve_hash_add(reserved, r); + return r; +} + +static void del_reserve(struct reserve_hash *reserved, struct reserve *r) +{ + assert(r->num_htlcs == 0); + + reserve_hash_del(reserved, r); + tal_free(r); +} + +/* Add to existing reservation (false if would overflow). */ +static bool add(struct reserve *r, struct amount_msat amount) +{ + if (!amount_msat_add(&r->amount, r->amount, amount)) + return false; + r->num_htlcs++; + return true; +} + +static bool remove(struct reserve *r, struct amount_msat amount) +{ + if (r->num_htlcs == 0) + return false; + if (!amount_msat_sub(&r->amount, r->amount, amount)) + return false; + r->num_htlcs--; + return true; +} + +/* Atomically add to reserves, or fail. + * Returns offset of failure, or num on success */ +size_t reserves_add(struct reserve_hash *reserved, + const struct short_channel_id_dir *scidds, + const struct amount_msat *amounts, + size_t num) +{ + for (size_t i = 0; i < num; i++) { + struct reserve *r = reserve_hash_get(reserved, &scidds[i]); + if (!r) + r = new_reserve(reserved, &scidds[i]); + if (!add(r, amounts[i])) { + reserves_remove(reserved, scidds, amounts, i); + return i; + } + } + return num; +} + +/* Atomically remove from reserves, to fail. + * Returns offset of failure or tal_count(scidds) */ +size_t reserves_remove(struct reserve_hash *reserved, + const struct short_channel_id_dir *scidds, + const struct amount_msat *amounts, + size_t num) +{ + for (size_t i = 0; i < num; i++) { + struct reserve *r = reserve_hash_get(reserved, &scidds[i]); + if (!r || !remove(r, amounts[i])) { + reserves_add(reserved, scidds, amounts, i); + return i; + } + if (r->num_htlcs == 0) + del_reserve(reserved, r); + } + return num; +} diff --git a/plugins/askrene/reserve.h b/plugins/askrene/reserve.h new file mode 100644 index 000000000000..bd8d088bffa3 --- /dev/null +++ b/plugins/askrene/reserve.h @@ -0,0 +1,38 @@ +#ifndef LIGHTNING_PLUGINS_ASKRENE_RESERVE_H +#define LIGHTNING_PLUGINS_ASKRENE_RESERVE_H +/* We have to know what payments are in progress, so we can take into + * account the reduced capacity of channels. We do this by telling + * everyone to reserve / unreserve paths as they use them. */ +#include "config.h" +#include +#include + +/* We reserve a path being used. This records how many and how much */ +struct reserve { + size_t num_htlcs; + struct short_channel_id_dir scidd; + struct amount_msat amount; +}; + +/* Initialize hash table for reservations */ +struct reserve_hash *new_reserve_hash(const tal_t *ctx); + +/* Find a reservation for this scidd (if any!) */ +const struct reserve *find_reserve(const struct reserve_hash *reserved, + const struct short_channel_id_dir *scidd); + +/* Atomically add to reserves, or fail. + * Returns offset of failure, or num on success */ +size_t reserves_add(struct reserve_hash *reserved, + const struct short_channel_id_dir *scidds, + const struct amount_msat *amounts, + size_t num); + +/* Atomically remove from reserves, to fail. + * Returns offset of failure or tal_count(scidds) */ +size_t reserves_remove(struct reserve_hash *reserved, + const struct short_channel_id_dir *scidds, + const struct amount_msat *amounts, + size_t num); + +#endif /* LIGHTNING_PLUGINS_ASKRENE_RESERVE_H */ From 99ad986e293051b213b0e5a0b12fd793235a3d93 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Wed, 7 Aug 2024 11:19:53 +0930 Subject: [PATCH 10/28] askrene: flesh out getroutes() a little. We apply all the gossmods for the layers they specified, and create a naive routine to give the capacity of a channel given those layers. Signed-off-by: Rusty Russell --- plugins/askrene/askrene.c | 92 +++++++++++++++++++++++++++++++++++++++ plugins/askrene/askrene.h | 24 ++++++++++ plugins/askrene/layer.c | 56 ++++++++++++++++++++++++ plugins/askrene/layer.h | 5 +++ 4 files changed, 177 insertions(+) diff --git a/plugins/askrene/askrene.c b/plugins/askrene/askrene.c index d561d15f9503..d37d9404b0c8 100644 --- a/plugins/askrene/askrene.c +++ b/plugins/askrene/askrene.c @@ -147,6 +147,32 @@ static const char *get_routes(struct command *cmd, const char **layers, struct route ***routes) { + struct askrene *askrene = get_askrene(cmd->plugin); + struct route_query *rq = tal(cmd, struct route_query); + struct gossmap_localmods *localmods; + + gossmap_refresh(askrene->gossmap, NULL); + + rq->plugin = cmd->plugin; + rq->gossmap = askrene->gossmap; + rq->reserved = askrene->reserved; + rq->layers = tal_arr(rq, const struct layer *, 0); + localmods = gossmap_localmods_new(rq); + + /* Layers don't have to exist: they might be empty! */ + for (size_t i = 0; i < tal_count(layers); i++) { + struct layer *l = find_layer(askrene, layers[i]); + if (!l) + continue; + + tal_arr_expand(&rq->layers, l); + /* FIXME: Implement localmods_merge, and cache this in layer? */ + layer_add_localmods(l, rq->gossmap, localmods); + } + + gossmap_apply_localmods(askrene->gossmap, localmods); + (void)rq; + /* FIXME: Do route here! This is a dummy, single "direct" route. */ *routes = tal_arr(cmd, struct route *, 1); (*routes)[0]->success_prob = 1; @@ -157,9 +183,75 @@ static const char *get_routes(struct command *cmd, (*routes)[0]->hops[0].amount = amount; (*routes)[0]->hops[0].delay = 6; + gossmap_remove_localmods(askrene->gossmap, localmods); return NULL; } +void get_constraints(const struct route_query *rq, + const struct gossmap_chan *chan, + int dir, + struct amount_msat *min, + struct amount_msat *max) +{ + struct short_channel_id_dir scidd; + const struct reserve *reserve; + + *min = AMOUNT_MSAT(0); + *max = AMOUNT_MSAT(-1ULL); + + /* Naive implementation! */ + scidd.scid = gossmap_chan_scid(rq->gossmap, chan); + scidd.dir = dir; + + /* Look through layers for any constraints */ + for (size_t i = 0; i < tal_count(rq->layers); i++) { + const struct constraint *cmin, *cmax; + cmin = layer_find_constraint(rq->layers[i], &scidd, CONSTRAINT_MIN); + if (cmin && amount_msat_greater(cmin->limit, *min)) + *min = cmin->limit; + cmax = layer_find_constraint(rq->layers[i], &scidd, CONSTRAINT_MAX); + if (cmax && amount_msat_less(cmax->limit, *max)) + *max = cmax->limit; + } + + /* If we know nothing, use the raw channel capacity */ + if (amount_msat_eq(*max, AMOUNT_MSAT(-1ULL))) { + struct amount_sat cap; + if (gossmap_chan_get_capacity(rq->gossmap, chan, &cap)) { + /* Shouldn't happen! */ + if (!amount_sat_to_msat(max, cap)) { + plugin_log(rq->plugin, LOG_BROKEN, + "Local channel %s with capacity %s?", + fmt_short_channel_id(tmpctx, scidd.scid), + fmt_amount_sat(tmpctx, cap)); + } + } else { + /* Local channel? */ + const struct local_channel *lc; + + /* In case it's not, set max to htlc max. */ + *max = amount_msat(fp16_to_u64(chan->half[dir].htlc_max)); + for (size_t i = 0; i < tal_count(rq->layers); i++) { + lc = layer_find_local_channel(rq->layers[i], scidd.scid); + if (lc) { + *max = local_channel_capacity(lc); + break; + } + } + } + } + + /* Finally, if any is in use, subtract that! */ + reserve = find_reserve(rq->reserved, &scidd); + if (reserve) { + /* They can definitely *try* to push too much through a channel! */ + if (!amount_msat_sub(min, *min, reserve->amount)) + *min = AMOUNT_MSAT(0); + if (!amount_msat_sub(max, *max, reserve->amount)) + *max = AMOUNT_MSAT(0); + } +} + static struct command_result *json_getroutes(struct command *cmd, const char *buffer, const jsmntok_t *params) diff --git a/plugins/askrene/askrene.h b/plugins/askrene/askrene.h index 02d9e54fdcce..961c7c3c65e2 100644 --- a/plugins/askrene/askrene.h +++ b/plugins/askrene/askrene.h @@ -5,6 +5,8 @@ #include #include +struct gossmap_chan; + /* A single route. */ struct route { /* Actual path to take */ @@ -23,4 +25,26 @@ struct askrene { struct reserve_hash *reserved; }; +/* Information for a single route query. */ +struct route_query { + /* Plugin pointer, for logging mainly */ + struct plugin *plugin; + + /* This is *not* updated during a query! Has all layers applied. */ + const struct gossmap *gossmap; + + /* We need to take in-flight payments into account */ + const struct reserve_hash *reserved; + + /* Array of layers we're applying */ + const struct layer **layers; +}; + +/* Given a gossmap channel, get the current known min/max */ +void get_constraints(const struct route_query *rq, + const struct gossmap_chan *chan, + int dir, + struct amount_msat *min, + struct amount_msat *max); + #endif /* LIGHTNING_PLUGINS_ASKRENE_ASKRENE_H */ diff --git a/plugins/askrene/layer.c b/plugins/askrene/layer.c index 28675bfd8841..ca4a2a9e6d62 100644 --- a/plugins/askrene/layer.c +++ b/plugins/askrene/layer.c @@ -2,6 +2,7 @@ #include #include #include +#include #include #include #include @@ -268,6 +269,61 @@ void layer_add_disabled_node(struct layer *layer, const struct node_id *node) tal_arr_expand(&layer->disabled_nodes, *node); } +void layer_add_localmods(struct layer *layer, + const struct gossmap *gossmap, + struct gossmap_localmods *localmods) +{ + struct local_channel *lc; + struct local_channel_hash_iter lcit; + + /* First, disable all channels into blocked nodes (local updates + * can add new ones)! */ + for (size_t i = 0; i < tal_count(layer->disabled_nodes); i++) { + const struct gossmap_node *node; + + node = gossmap_find_node(gossmap, &layer->disabled_nodes[i]); + if (!node) + continue; + for (size_t n = 0; n < node->num_chans; n++) { + struct short_channel_id scid; + struct gossmap_chan *c; + int dir; + c = gossmap_nth_chan(gossmap, node, n, &dir); + scid = gossmap_chan_scid(gossmap, c); + + /* Disabled zero-capacity on incoming */ + gossmap_local_updatechan(localmods, + scid, + AMOUNT_MSAT(0), + AMOUNT_MSAT(0), + 0, + 0, + 0, + false, + !dir); + } + } + + for (lc = local_channel_hash_first(layer->local_channels, &lcit); + lc; + lc = local_channel_hash_next(layer->local_channels, &lcit)) { + gossmap_local_addchan(localmods, + &lc->n1, &lc->n2, lc->scid, NULL); + for (size_t i = 0; i < ARRAY_SIZE(lc->half); i++) { + if (!lc->half[i].enabled) + continue; + gossmap_local_updatechan(localmods, lc->scid, + lc->half[i].htlc_min, + lc->half[i].htlc_max, + lc->half[i].base_fee.millisatoshis /* Raw: gossmap */, + lc->half[i].proportional_fee, + lc->half[i].delay, + true, + i); + } + } +} + static void json_add_local_channel(struct json_stream *response, const char *fieldname, const struct local_channel *lc, diff --git a/plugins/askrene/layer.h b/plugins/askrene/layer.h index 2eb5cc412ceb..94984d6279da 100644 --- a/plugins/askrene/layer.h +++ b/plugins/askrene/layer.h @@ -81,6 +81,11 @@ const struct constraint *layer_update_constraint(struct layer *layer, u64 timestamp, struct amount_msat limit); +/* Add local channels from this layer */ +void layer_add_localmods(struct layer *layer, + const struct gossmap *gossmap, + struct gossmap_localmods *localmods); + /* Remove constraints older then cutoff: returns num removed. */ size_t layer_trim_constraints(struct layer *layer, u64 cutoff); From 0e7073b3e7f2a122c15c1381527dc288ac65e5c9 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Wed, 7 Aug 2024 11:19:53 +0930 Subject: [PATCH 11/28] askrene: always set a dummy constraint when we add a local channel. This means we never have to look up a local channel when asked the capacity. We mark these dummy constraints with an MAX timestamp. Signed-off-by: Rusty Russell --- plugins/askrene/askrene.c | 16 ++++------------ plugins/askrene/layer.c | 10 ++++++++++ 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/plugins/askrene/askrene.c b/plugins/askrene/askrene.c index d37d9404b0c8..4e257423212b 100644 --- a/plugins/askrene/askrene.c +++ b/plugins/askrene/askrene.c @@ -226,18 +226,10 @@ void get_constraints(const struct route_query *rq, fmt_amount_sat(tmpctx, cap)); } } else { - /* Local channel? */ - const struct local_channel *lc; - - /* In case it's not, set max to htlc max. */ - *max = amount_msat(fp16_to_u64(chan->half[dir].htlc_max)); - for (size_t i = 0; i < tal_count(rq->layers); i++) { - lc = layer_find_local_channel(rq->layers[i], scidd.scid); - if (lc) { - *max = local_channel_capacity(lc); - break; - } - } + /* Shouldn't happen: local channels have explicit constraints */ + plugin_log(rq->plugin, LOG_BROKEN, + "Channel %s without capacity?", + fmt_short_channel_id(tmpctx, scidd.scid)); } } diff --git a/plugins/askrene/layer.c b/plugins/askrene/layer.c index ca4a2a9e6d62..2469a2df3c63 100644 --- a/plugins/askrene/layer.c +++ b/plugins/askrene/layer.c @@ -173,6 +173,7 @@ void layer_update_local_channel(struct layer *layer, { struct local_channel *lc = local_channel_hash_get(layer->local_channels, scid); int dir = canonicalize_node_order(&src, &dst); + struct short_channel_id_dir scidd; if (lc) { assert(layer_check_local_channel(lc, src, dst, capacity)); @@ -186,6 +187,12 @@ void layer_update_local_channel(struct layer *layer, lc->half[dir].base_fee = base_fee; lc->half[dir].proportional_fee = proportional_fee; lc->half[dir].delay = delay; + + /* We always add an explicit constraint for local channels, to simplify + * lookups. You can tell it's a fake one by the timestamp. */ + scidd.scid = scid; + scidd.dir = dir; + layer_update_constraint(layer, &scidd, CONSTRAINT_MAX, UINT64_MAX, capacity); } struct amount_msat local_channel_capacity(const struct local_channel *lc) @@ -400,6 +407,9 @@ static void json_add_layer(struct json_stream *js, for (c = constraint_hash_first(layer->constraints, &conit); c; c = constraint_hash_next(layer->constraints, &conit)) { + /* Don't show ones we generated internally */ + if (c->timestamp == UINT64_MAX) + continue; json_add_constraint(js, NULL, c, NULL); } json_array_end(js); From f93324e57972fa340f14480f8f59e1591ec31f0d Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Wed, 7 Aug 2024 11:19:53 +0930 Subject: [PATCH 12/28] askrene: fast lookup for capacities. We don't know anything about most channels, so we create an array of fp16_t containing them. We zero out ones where we do know something, and use the previous code as the slow path. Signed-off-by: Rusty Russell --- plugins/askrene/askrene.c | 51 ++++++++++++++++++++++++++++++++++++--- plugins/askrene/askrene.h | 6 +++++ plugins/askrene/layer.c | 20 +++++++++++++++ plugins/askrene/layer.h | 6 +++++ plugins/askrene/reserve.c | 21 ++++++++++++++++ plugins/askrene/reserve.h | 5 ++++ 6 files changed, 106 insertions(+), 3 deletions(-) diff --git a/plugins/askrene/askrene.c b/plugins/askrene/askrene.c index 4e257423212b..99a743c913fe 100644 --- a/plugins/askrene/askrene.c +++ b/plugins/askrene/askrene.c @@ -139,6 +139,30 @@ static struct command_result *param_reserve_path(struct command *cmd, return NULL; } +static fp16_t *get_capacities(const tal_t *ctx, + struct plugin *plugin, struct gossmap *gossmap) +{ + fp16_t *caps; + struct gossmap_chan *c; + + caps = tal_arrz(ctx, fp16_t, gossmap_max_chan_idx(gossmap)); + + for (c = gossmap_first_chan(gossmap); + c; + c = gossmap_next_chan(gossmap, c)) { + struct amount_sat cap; + + if (!gossmap_chan_get_capacity(gossmap, c, &cap)) { + plugin_log(plugin, LOG_BROKEN, + "get_capacity failed for channel?"); + cap = AMOUNT_SAT(0); + } + caps[gossmap_chan_idx(gossmap, c)] + = u64_to_fp16(cap.satoshis, true); /* Raw: fp16 */ + } + return caps; +} + /* Returns an error message, or sets *routes */ static const char *get_routes(struct command *cmd, const struct node_id *source, @@ -151,12 +175,17 @@ static const char *get_routes(struct command *cmd, struct route_query *rq = tal(cmd, struct route_query); struct gossmap_localmods *localmods; - gossmap_refresh(askrene->gossmap, NULL); + if (gossmap_refresh(askrene->gossmap, NULL)) { + /* FIXME: gossmap_refresh callbacks to we can update in place */ + tal_free(askrene->capacities); + askrene->capacities = get_capacities(askrene, askrene->plugin, askrene->gossmap); + } rq->plugin = cmd->plugin; rq->gossmap = askrene->gossmap; rq->reserved = askrene->reserved; rq->layers = tal_arr(rq, const struct layer *, 0); + rq->capacities = tal_dup_talarr(rq, fp16_t, askrene->capacities); localmods = gossmap_localmods_new(rq); /* Layers don't have to exist: they might be empty! */ @@ -168,8 +197,16 @@ static const char *get_routes(struct command *cmd, tal_arr_expand(&rq->layers, l); /* FIXME: Implement localmods_merge, and cache this in layer? */ layer_add_localmods(l, rq->gossmap, localmods); + + /* Clear any entries in capacities array if we + * override them (incl local channels) */ + layer_clear_overridden_capacities(l, askrene->gossmap, rq->capacities); } + /* Clear scids with reservations, too, so we don't have to look up + * all the time! */ + reserves_clear_capacities(askrene->reserved, askrene->gossmap, rq->capacities); + gossmap_apply_localmods(askrene->gossmap, localmods); (void)rq; @@ -195,13 +232,20 @@ void get_constraints(const struct route_query *rq, { struct short_channel_id_dir scidd; const struct reserve *reserve; + size_t idx = gossmap_chan_idx(rq->gossmap, chan); *min = AMOUNT_MSAT(0); - *max = AMOUNT_MSAT(-1ULL); + + /* Fast path: no information known, no reserve. */ + if (idx < tal_count(rq->capacities) && rq->capacities[idx] != 0) { + *max = amount_msat(fp16_to_u64(rq->capacities[idx]) * 1000); + return; + } /* Naive implementation! */ scidd.scid = gossmap_chan_scid(rq->gossmap, chan); scidd.dir = dir; + *max = AMOUNT_MSAT(-1ULL); /* Look through layers for any constraints */ for (size_t i = 0; i < tal_count(rq->layers); i++) { @@ -214,7 +258,7 @@ void get_constraints(const struct route_query *rq, *max = cmax->limit; } - /* If we know nothing, use the raw channel capacity */ + /* Might be here because it's reserved, but capacity is normal. */ if (amount_msat_eq(*max, AMOUNT_MSAT(-1ULL))) { struct amount_sat cap; if (gossmap_chan_get_capacity(rq->gossmap, chan, &cap)) { @@ -576,6 +620,7 @@ static const char *init(struct plugin *plugin, if (!askrene->gossmap) plugin_err(plugin, "Could not load gossmap %s: %s", GOSSIP_STORE_FILENAME, strerror(errno)); + askrene->capacities = get_capacities(askrene, askrene->plugin, askrene->gossmap); plugin_set_data(plugin, askrene); plugin_set_memleak_handler(plugin, askrene_markmem); diff --git a/plugins/askrene/askrene.h b/plugins/askrene/askrene.h index 961c7c3c65e2..693e70ca7cc0 100644 --- a/plugins/askrene/askrene.h +++ b/plugins/askrene/askrene.h @@ -4,6 +4,7 @@ #include #include #include +#include struct gossmap_chan; @@ -23,6 +24,8 @@ struct askrene { struct list_head layers; /* In-flight payment attempts */ struct reserve_hash *reserved; + /* Compact cache of gossmap capacities */ + fp16_t *capacities; }; /* Information for a single route query. */ @@ -38,6 +41,9 @@ struct route_query { /* Array of layers we're applying */ const struct layer **layers; + + /* Cache of channel capacities for non-reserved, unknown channels. */ + fp16_t *capacities; }; /* Given a gossmap channel, get the current known min/max */ diff --git a/plugins/askrene/layer.c b/plugins/askrene/layer.c index 2469a2df3c63..ea1296bac5c1 100644 --- a/plugins/askrene/layer.c +++ b/plugins/askrene/layer.c @@ -253,6 +253,26 @@ const struct constraint *layer_update_constraint(struct layer *layer, return c; } +void layer_clear_overridden_capacities(const struct layer *layer, + const struct gossmap *gossmap, + fp16_t *capacities) +{ + struct constraint_hash_iter conit; + struct constraint *con; + + for (con = constraint_hash_first(layer->constraints, &conit); + con; + con = constraint_hash_next(layer->constraints, &conit)) { + struct gossmap_chan *c = gossmap_find_chan(gossmap, &con->key.scidd.scid); + size_t idx; + if (!c) + continue; + idx = gossmap_chan_idx(gossmap, c); + if (idx < tal_count(capacities)) + capacities[idx] = 0; + } +} + size_t layer_trim_constraints(struct layer *layer, u64 cutoff) { size_t num_removed = 0; diff --git a/plugins/askrene/layer.h b/plugins/askrene/layer.h index 94984d6279da..5fa45ffee7bd 100644 --- a/plugins/askrene/layer.h +++ b/plugins/askrene/layer.h @@ -69,6 +69,12 @@ void layer_update_local_channel(struct layer *layer, struct amount_msat htlc_min, struct amount_msat htlc_max); +/* If any capacities of channels are limited, unset the corresponding element in + * the capacities[] array */ +void layer_clear_overridden_capacities(const struct layer *layer, + const struct gossmap *gossmap, + fp16_t *capacities); + /* Find a constraint in a layer. */ const struct constraint *layer_find_constraint(const struct layer *layer, const struct short_channel_id_dir *scidd, diff --git a/plugins/askrene/reserve.c b/plugins/askrene/reserve.c index 19ab56c302e4..9d1807e8c124 100644 --- a/plugins/askrene/reserve.c +++ b/plugins/askrene/reserve.c @@ -1,6 +1,7 @@ #include "config.h" #include #include +#include #include /* Hash table for reservations */ @@ -117,3 +118,23 @@ size_t reserves_remove(struct reserve_hash *reserved, } return num; } + +void reserves_clear_capacities(struct reserve_hash *reserved, + const struct gossmap *gossmap, + fp16_t *capacities) +{ + struct reserve *r; + struct reserve_hash_iter rit; + + for (r = reserve_hash_first(reserved, &rit); + r; + r = reserve_hash_next(reserved, &rit)) { + struct gossmap_chan *c = gossmap_find_chan(gossmap, &r->scidd.scid); + size_t idx; + if (!c) + continue; + idx = gossmap_chan_idx(gossmap, c); + if (idx < tal_count(capacities)) + capacities[idx] = 0; + } +} diff --git a/plugins/askrene/reserve.h b/plugins/askrene/reserve.h index bd8d088bffa3..93ceace26606 100644 --- a/plugins/askrene/reserve.h +++ b/plugins/askrene/reserve.h @@ -6,6 +6,7 @@ #include "config.h" #include #include +#include /* We reserve a path being used. This records how many and how much */ struct reserve { @@ -35,4 +36,8 @@ size_t reserves_remove(struct reserve_hash *reserved, const struct amount_msat *amounts, size_t num); +/* Clear capacities array where we have reserves */ +void reserves_clear_capacities(struct reserve_hash *reserved, + const struct gossmap *gossmap, + fp16_t *capacities); #endif /* LIGHTNING_PLUGINS_ASKRENE_RESERVE_H */ From 15cbc1472d557292d2fe204b84a0a084271fce82 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Wed, 7 Aug 2024 11:19:53 +0930 Subject: [PATCH 13/28] pytest: test file for askrene. Signed-off-by: Rusty Russell --- tests/test_askrene.py | 104 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 tests/test_askrene.py diff --git a/tests/test_askrene.py b/tests/test_askrene.py new file mode 100644 index 000000000000..94683110f302 --- /dev/null +++ b/tests/test_askrene.py @@ -0,0 +1,104 @@ +from fixtures import * # noqa: F401,F403 +from utils import ( + only_one, first_scid +) +import time + + +def test_layers(node_factory): + """Test manipulating information in layers""" + l1, l2, l3 = node_factory.line_graph(3, wait_for_announce=True) + + assert l2.rpc.askrene_listlayers() == {'layers': []} + assert l2.rpc.askrene_listlayers('test_layers') == {'layers': []} + + expect = {'layer': 'test_layers', + 'disabled_nodes': [], + 'created_channels': [], + 'constraints': []} + l2.rpc.askrene_disable_node('test_layers', l1.info['id']) + expect['disabled_nodes'].append(l1.info['id']) + assert l2.rpc.askrene_listlayers('test_layers') == {'layers': [expect]} + assert l2.rpc.askrene_listlayers() == {'layers': [expect]} + assert l2.rpc.askrene_listlayers('test_layers2') == {'layers': []} + + # Tell it l3 connects to l1! + l2.rpc.askrene_create_channel('test_layers', + l3.info['id'], + l1.info['id'], + '0x0x1', + '1000000sat', + 100, '900000sat', + 1, 2, 18) + expect['created_channels'].append({'source': l3.info['id'], + 'destination': l1.info['id'], + 'short_channel_id': '0x0x1', + 'capacity_msat': 1000000000, + 'htlc_minimum_msat': 100, + 'htlc_maximum_msat': 900000000, + 'fee_base_msat': 1, + 'fee_proportional_millionths': 2, + 'delay': 18}) + assert l2.rpc.askrene_listlayers('test_layers') == {'layers': [expect]} + + # We can tell it about made up channels... + first_timestamp = int(time.time()) + l2.rpc.askrene_inform_channel('test_layers', + '0x0x1', + 1, + 100000) + last_timestamp = int(time.time()) + 1 + expect['constraints'].append({'short_channel_id': '0x0x1', + 'direction': 1, + 'minimum_msat': 100000}) + # Check timestamp first. + listlayers = l2.rpc.askrene_listlayers('test_layers') + ts1 = only_one(only_one(listlayers['layers'])['constraints'])['timestamp'] + assert first_timestamp <= ts1 <= last_timestamp + expect['constraints'][0]['timestamp'] = ts1 + assert listlayers == {'layers': [expect]} + + # Make sure timestamps differ! + time.sleep(2) + + # We can tell it about existing channels... + scid12 = first_scid(l1, l2) + first_timestamp = int(time.time()) + l2.rpc.askrene_inform_channel(layer='test_layers', + short_channel_id=scid12, + # This is l2 -> l1 + direction=0, + maximum_msat=12341234) + last_timestamp = int(time.time()) + 1 + expect['constraints'].append({'short_channel_id': scid12, + 'direction': 0, + 'timestamp': first_timestamp, + 'maximum_msat': 12341234}) + # Check timestamp first. + listlayers = l2.rpc.askrene_listlayers('test_layers') + ts2 = only_one([c['timestamp'] for c in only_one(listlayers['layers'])['constraints'] if c['short_channel_id'] == scid12]) + assert first_timestamp <= ts2 <= last_timestamp + expect['constraints'][1]['timestamp'] = ts2 + + # Could be either order! + actual = expect.copy() + if only_one(listlayers['layers'])['constraints'][0]['short_channel_id'] == scid12: + actual['constraints'] = [expect['constraints'][1], expect['constraints'][0]] + assert listlayers == {'layers': [actual]} + + # Now test aging: ts1 does nothing. + assert l2.rpc.askrene_age('test_layers', ts1) == {'layer': 'test_layers', 'num_removed': 0} + listlayers = l2.rpc.askrene_listlayers('test_layers') + assert listlayers == {'layers': [actual]} + + # ts1+1 removes first inform + assert l2.rpc.askrene_age('test_layers', ts1 + 1) == {'layer': 'test_layers', 'num_removed': 1} + del expect['constraints'][0] + listlayers = l2.rpc.askrene_listlayers('test_layers') + assert listlayers == {'layers': [expect]} + + # ts2+1 removes other. + assert l2.rpc.askrene_age('test_layers', ts2 + 1) == {'layer': 'test_layers', 'num_removed': 1} + del expect['constraints'][0] + listlayers = l2.rpc.askrene_listlayers('test_layers') + assert listlayers == {'layers': [expect]} From 46d00edad712d742bf30fdc226ca4e19303620f2 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Wed, 7 Aug 2024 11:19:54 +0930 Subject: [PATCH 14/28] askrene: copy mcf.[ch] from renepay with minimal modifications. Signed-off-by: Rusty Russell --- plugins/askrene/mcf.c | 1835 +++++++++++++++++++++++++++++++++++++++++ plugins/askrene/mcf.h | 68 ++ 2 files changed, 1903 insertions(+) create mode 100644 plugins/askrene/mcf.c create mode 100644 plugins/askrene/mcf.h diff --git a/plugins/askrene/mcf.c b/plugins/askrene/mcf.c new file mode 100644 index 000000000000..8c3a7d349da9 --- /dev/null +++ b/plugins/askrene/mcf.c @@ -0,0 +1,1835 @@ +#include "config.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* # Optimal payments + * + * In this module we reduce the routing optimization problem to a linear + * cost optimization problem and find a solution using MCF algorithms. + * The optimization of the routing itself doesn't need a precise numerical + * solution, since we can be happy near optimal results; e.g. paying 100 msat or + * 101 msat for fees doesn't make any difference if we wish to deliver 1M sats. + * On the other hand, we are now also considering Pickhard's + * [1] model to improve payment reliability, + * hence our optimization moves to a 2D space: either we like to maximize the + * probability of success of a payment or minimize the routing fees, or + * alternatively we construct a function of the two that gives a good compromise. + * + * Therefore from now own, the definition of optimal is a matter of choice. + * To simplify the API of this module, we think the best way to state the + * problem is: + * + * Find a routing solution that pays the least of fees while keeping + * the probability of success above a certain value `min_probability`. + * + * + * # Fee Cost + * + * Routing fees is non-linear function of the payment flow x, that's true even + * without the base fee: + * + * fee_msat = base_msat + floor(millionths*x_msat / 10^6) + * + * We approximate this fee into a linear function by computing a slope `c_fee` such + * that: + * + * fee_microsat = c_fee * x_sat + * + * Function `linear_fee_cost` computes `c_fee` based on the base and + * proportional fees of a channel. + * The final product if microsat because if only + * the proportional fee was considered we can have c_fee = millionths. + * Moving to costs based in msats means we have to either truncate payments + * below 1ksats or estimate as 0 cost for channels with less than 1000ppm. + * + * TODO(eduardo): shall we build a linear cost function in msats? + * + * # Probability cost + * + * The probability of success P of the payment is the product of the prob. of + * success of forwarding parts of the payment over all routing channels. This + * problem is separable if we log it, and since we would like to increase P, + * then we can seek to minimize -log(P), and that's our prob. cost function [1]. + * + * - log P = sum_{i} - log P_i + * + * The probability of success `P_i` of sending some flow `x` on a channel with + * liquidity l in the range a<=l a + * = 1. ; for x <= a + * + * Notice that unlike the similar formula in [1], the one we propose does not + * contain the quantization shot noise for counting states. The formula remains + * valid independently of the liquidity units (sats or msats). + * + * The cost associated to probability P is then -k log P, where k is some + * constant. For k=1 we get the following table: + * + * prob | cost + * ----------- + * 0.01 | 4.6 + * 0.02 | 3.9 + * 0.05 | 3.0 + * 0.10 | 2.3 + * 0.20 | 1.6 + * 0.50 | 0.69 + * 0.80 | 0.22 + * 0.90 | 0.10 + * 0.95 | 0.05 + * 0.98 | 0.02 + * 0.99 | 0.01 + * + * Clearly -log P(x) is non-linear; we try to linearize it piecewise: + * split the channel into 4 arcs representing 4 liquidity regions: + * + * arc_0 -> [0, a) + * arc_1 -> [a, a+(b-a)*f1) + * arc_2 -> [a+(b-a)*f1, a+(b-a)*f2) + * arc_3 -> [a+(b-a)*f2, a+(b-a)*f3) + * + * where f1 = 0.5, f2 = 0.8, f3 = 0.95; + * We fill arc_0's capacity with complete certainty P=1, then if more flow is + * needed we start filling the capacity in arc_1 until the total probability + * of success reaches P=0.5, then arc_2 until P=1-0.8=0.2, and finally arc_3 until + * P=1-0.95=0.05. We don't go further than 5% prob. of success per channel. + + * TODO(eduardo): this channel linearization is hard coded into + * `CHANNEL_PIVOTS`, maybe we can parametrize this to take values from the config file. + * + * With this choice, the slope of the linear cost function becomes: + * + * m_0 = 0 + * m_1 = 1.38 k /(b-a) + * m_2 = 3.05 k /(b-a) + * m_3 = 9.24 k /(b-a) + * + * Notice that one of the assumptions in [2] for the MCF problem is that flows + * and the slope of the costs functions are integer numbers. The only way we + * have at hand to make it so, is to choose a universal value of `k` that scales + * up the slopes so that floor(m_i) is not zero for every arc. + * + * # Combine fee and prob. costs + * + * We attempt to solve the original problem of finding the solution that + * pays the least fees while keeping the prob. of success above a certain value, + * by constructing a cost function which is a linear combination of fee and + * prob. costs. + * TODO(eduardo): investigate how this procedure is justified, + * possibly with the use of Lagrange optimization theory. + * + * At first, prob. and fee costs live in different dimensions, they cannot be + * summed, it's like comparing apples and oranges. + * However we propose to scale the prob. cost by a global factor k that + * translates into the monetization of prob. cost. + * + * k/1000, for instance, becomes the equivalent monetary cost + * of increasing the probability of success by 0.1% for P~100%. + * + * The input parameter `prob_cost_factor` in the function `minflow` is defined + * as the PPM from the delivery amount `T` we are *willing to pay* to increase the + * prob. of success by 0.1%: + * + * k_microsat = floor(1000*prob_cost_factor * T_sat) + * + * Is this enough to make integer prob. cost per unit flow? + * For `prob_cost_factor=10`; i.e. we pay 10ppm for increasing the prob. by + * 0.1%, we get that + * + * -> any arc with (b-a) > 10000 T, will have zero prob. cost, which is + * reasonable because even if all the flow passes through that arc, we get + * a 1.3 T/(b-a) ~ 0.01% prob. of failure at most. + * + * -> if (b-a) ~ 10000 T, then the arc will have unit cost, or just that we + * pay 1 microsat for every sat we send through this arc. + * + * -> it would be desirable to have a high proportional fee when (b-a)~T, + * because prob. of failure start to become very high. + * In this case we get to pay 10000 microsats for every sat. + * + * Once `k` is fixed then we can combine the linear prob. and fee costs, both + * are in monetary units. + * + * Note: with costs in microsats, because slopes represent ppm and flows are in + * sats, then our integer bounds with 64 bits are such that we can move as many + * as 10'000 BTC without overflow: + * + * 10^6 (max ppm) * 10^8 (sats per BTC) * 10^4 = 10^18 + * + * # References + * + * [1] Pickhardt and Richter, https://arxiv.org/abs/2107.05322 + * [2] R.K. Ahuja, T.L. Magnanti, and J.B. Orlin. Network Flows: + * Theory, Algorithms, and Applications. Prentice Hall, 1993. + * + * + * TODO(eduardo) it would be interesting to see: + * how much do we pay for reliability? + * Cost_fee(most reliable solution) - Cost_fee(cheapest solution) + * + * TODO(eduardo): it would be interesting to see: + * how likely is the most reliable path with respect to the cheapest? + * Prob(reliable)/Prob(cheapest) = Exp(Cost_prob(cheapest)-Cost_prob(reliable)) + * + * */ + +#define PARTS_BITS 2 +#define CHANNEL_PARTS (1 << PARTS_BITS) + +// These are the probability intervals we use to decompose a channel into linear +// cost function arcs. +static const double CHANNEL_PIVOTS[]={0,0.5,0.8,0.95}; + +static const s64 INFINITE = INT64_MAX; +static const u64 INFINITE_MSAT = UINT64_MAX; +static const u32 INVALID_INDEX = 0xffffffff; +static const s64 MU_MAX = 128; + +/* Let's try this encoding of arcs: + * Each channel `c` has two possible directions identified by a bit + * `half` or `!half`, and each one of them has to be + * decomposed into 4 liquidity parts in order to + * linearize the cost function, but also to solve MCF + * problem we need to keep track of flows in the + * residual network hence we need for each directed arc + * in the network there must be another arc in the + * opposite direction refered to as it's dual. In total + * 1+2+1 additional bits of information: + * + * (chan_idx)(half)(part)(dual) + * + * That means, for each channel we need to store the + * information of 16 arcs. If we implement a convex-cost + * solver then we can reduce that number to size(half)size(dual)=4. + * + * In the adjacency of a `node` we are going to store + * the outgoing arcs. If we ever need to loop over the + * incoming arcs then we will define a reverse adjacency + * API. + * Then for each outgoing channel `(c,half)` there will + * be 4 parts for the actual residual capacity, hence + * with the dual bit set to 0: + * + * (c,half,0,0) + * (c,half,1,0) + * (c,half,2,0) + * (c,half,3,0) + * + * and also we need to consider the dual arcs + * corresponding to the channel direction `(c,!half)` + * (the dual has reverse direction): + * + * (c,!half,0,1) + * (c,!half,1,1) + * (c,!half,2,1) + * (c,!half,3,1) + * + * These are the 8 outgoing arcs relative to `node` and + * associated with channel `c`. The incoming arcs will + * be: + * + * (c,!half,0,0) + * (c,!half,1,0) + * (c,!half,2,0) + * (c,!half,3,0) + * + * (c,half,0,1) + * (c,half,1,1) + * (c,half,2,1) + * (c,half,3,1) + * + * but they will be stored as outgoing arcs on the peer + * node `next`. + * + * I hope this will clarify my future self when I forget. + * + * */ + +/* + * We want to use the whole number here for convenience, but + * we can't us a union, since bit order is implementation-defined and + * we want chanidx on the highest bits: + * + * [ 0 1 2 3 4 5 6 ... 31 ] + * dual part chandir chanidx + */ +struct arc { + u32 idx; +}; + +#define ARC_DUAL_BITOFF (0) +#define ARC_PART_BITOFF (1) +#define ARC_CHANDIR_BITOFF (1 + PARTS_BITS) +#define ARC_CHANIDX_BITOFF (1 + PARTS_BITS + 1) +#define ARC_CHANIDX_BITS (32 - ARC_CHANIDX_BITOFF) + +/* How many arcs can we have for a single channel? + * linearization parts, both directions, and dual */ +#define ARCS_PER_CHANNEL ((size_t)1 << (PARTS_BITS + 1 + 1)) + +static inline void arc_to_parts(struct arc arc, + u32 *chanidx, + int *chandir, + u32 *part, + bool *dual) +{ + if (chanidx) + *chanidx = (arc.idx >> ARC_CHANIDX_BITOFF); + if (chandir) + *chandir = (arc.idx >> ARC_CHANDIR_BITOFF) & 1; + if (part) + *part = (arc.idx >> ARC_PART_BITOFF) & ((1 << PARTS_BITS)-1); + if (dual) + *dual = (arc.idx >> ARC_DUAL_BITOFF) & 1; +} + +static inline struct arc arc_from_parts(u32 chanidx, int chandir, u32 part, bool dual) +{ + struct arc arc; + + assert(part < CHANNEL_PARTS); + assert(chandir == 0 || chandir == 1); + assert(chanidx < (1U << ARC_CHANIDX_BITS)); + arc.idx = ((u32)dual << ARC_DUAL_BITOFF) + | (part << ARC_PART_BITOFF) + | ((u32)chandir << ARC_CHANDIR_BITOFF) + | (chanidx << ARC_CHANIDX_BITOFF); + return arc; +} + +#define MAX(x, y) (((x) > (y)) ? (x) : (y)) +#define MIN(x, y) (((x) < (y)) ? (x) : (y)) + +struct pay_parameters { + /* The gossmap we are using */ + struct gossmap *gossmap; + const struct gossmap_node *source; + const struct gossmap_node *target; + + /* Extra information we intuited about the channels */ + struct chan_extra_map *chan_extra_map; + + /* Optional bitarray of disabled channels. */ + const bitmap *disabled; + + // how much we pay + struct amount_msat amount; + + // channel linearization parameters + double cap_fraction[CHANNEL_PARTS], + cost_fraction[CHANNEL_PARTS]; + + struct amount_msat max_fee; + double min_probability; + double delay_feefactor; + double base_fee_penalty; + u32 prob_cost_factor; +}; + +/* Representation of the linear MCF network. + * This contains the topology of the extended network (after linearization and + * addition of arc duality). + * This contains also the arc probability and linear fee cost, as well as + * capacity; these quantities remain constant during MCF execution. */ +struct linear_network +{ + u32 *arc_tail_node; + // notice that a tail node is not needed, + // because the tail of arc is the head of dual(arc) + + struct arc *node_adjacency_next_arc; + struct arc *node_adjacency_first_arc; + + // probability and fee cost associated to an arc + s64 *arc_prob_cost, *arc_fee_cost; + s64 *capacity; + + size_t max_num_arcs,max_num_nodes; +}; + +/* This is the structure that keeps track of the network properties while we + * seek for a solution. */ +struct residual_network { + /* residual capacity on arcs */ + s64 *cap; + + /* some combination of prob. cost and fee cost on arcs */ + s64 *cost; + + /* potential function on nodes */ + s64 *potential; +}; + +/* Helper function. + * Given an arc idx, return the dual's idx in the residual network. */ +static struct arc arc_dual(struct arc arc) +{ + arc.idx ^= (1U << ARC_DUAL_BITOFF); + return arc; +} +/* Helper function. */ +static bool arc_is_dual(struct arc arc) +{ + bool dual; + arc_to_parts(arc, NULL, NULL, NULL, &dual); + return dual; +} + +/* Helper function. + * Given an arc of the network (not residual) give me the flow. */ +static s64 get_arc_flow( + const struct residual_network *network, + const struct arc arc) +{ + assert(!arc_is_dual(arc)); + assert(arc_dual(arc).idx < tal_count(network->cap)); + return network->cap[ arc_dual(arc).idx ]; +} + +/* Helper function. + * Given an arc idx, return the node from which this arc emanates in the residual network. */ +static u32 arc_tail(const struct linear_network *linear_network, + const struct arc arc) +{ + assert(arc.idx < tal_count(linear_network->arc_tail_node)); + return linear_network->arc_tail_node[ arc.idx ]; +} +/* Helper function. + * Given an arc idx, return the node that this arc is pointing to in the residual network. */ +static u32 arc_head(const struct linear_network *linear_network, + const struct arc arc) +{ + const struct arc dual = arc_dual(arc); + assert(dual.idx < tal_count(linear_network->arc_tail_node)); + return linear_network->arc_tail_node[dual.idx]; +} + +/* Helper function. + * Given node idx `node`, return the idx of the first arc whose tail is `node`. + * */ +static struct arc node_adjacency_begin( + const struct linear_network * linear_network, + const u32 node) +{ + assert(node < tal_count(linear_network->node_adjacency_first_arc)); + return linear_network->node_adjacency_first_arc[node]; +} + +/* Helper function. + * Is this the end of the adjacency list. */ +static bool node_adjacency_end(const struct arc arc) +{ + return arc.idx == INVALID_INDEX; +} + +/* Helper function. + * Given node idx `node` and `arc`, returns the idx of the next arc whose tail is `node`. */ +static struct arc node_adjacency_next( + const struct linear_network *linear_network, + const struct arc arc) +{ + assert(arc.idx < tal_count(linear_network->node_adjacency_next_arc)); + return linear_network->node_adjacency_next_arc[arc.idx]; +} + +static bool channel_is_available(const struct gossmap_chan *c, int dir, + const struct gossmap *gossmap, + const bitmap *disabled) +{ + if (!gossmap_chan_set(c, dir)) + return false; + + const u32 chan_id = gossmap_chan_idx(gossmap, c); + return !bitmap_test_bit(disabled, chan_id); +} + +// TODO(eduardo): unit test this +/* Split a directed channel into parts with linear cost function. */ +static bool linearize_channel(const struct pay_parameters *params, + const struct gossmap_chan *c, const int dir, + s64 *capacity, s64 *cost) +{ + struct chan_extra_half *extra_half = get_chan_extra_half_by_chan( + params->gossmap, + params->chan_extra_map, + c, + dir); + + if (!extra_half) { + return false; + } + + assert( + amount_msat_less_eq(extra_half->htlc_total, extra_half->known_max)); + assert( + amount_msat_less_eq(extra_half->known_min, extra_half->known_max)); + + s64 h = extra_half->htlc_total.millisatoshis/1000; /* Raw: linearize_channel */ + s64 a = extra_half->known_min.millisatoshis/1000, /* Raw: linearize_channel */ + b = 1 + extra_half->known_max.millisatoshis/1000; /* Raw: linearize_channel */ + + /* If HTLCs add up to more than the known_max it means we have a + * completely wrong knowledge. */ + // assert(ha it doesn't mean automatically that our + * known_min should have been updated, because we reserve this HTLC + * after sendpay behind the scenes it might happen that sendpay failed + * because of insufficient funds we haven't noticed yet. */ + // assert(h<=a); + + /* We reduce this channel capacity because HTLC are reserving liquidity. */ + a -= h; + b -= h; + a = MAX(a,0); + b = MAX(a+1,b); + + /* An extra bound on capacity, here we use it to reduce the flow such + * that it does not exceed htlcmax. */ + s64 cap_on_capacity = + channel_htlc_max(c, dir).millisatoshis/1000; /* Raw: linearize_channel */ + + capacity[0]=a; + cost[0]=0; + for(size_t i=1;icap_fraction[i]*(b-a), cap_on_capacity); + cap_on_capacity -= capacity[i]; + assert(cap_on_capacity>=0); + + cost[i] = params->cost_fraction[i] + *params->amount.millisatoshis /* Raw: linearize_channel */ + *params->prob_cost_factor*1.0/(b-a); + } + return true; +} + +static struct residual_network * +alloc_residual_network(const tal_t *ctx, const size_t max_num_nodes, + const size_t max_num_arcs) +{ + struct residual_network *residual_network = + tal(ctx, struct residual_network); + if (!residual_network) + goto function_fail; + + residual_network->cap = tal_arrz(residual_network, s64, max_num_arcs); + residual_network->cost = tal_arrz(residual_network, s64, max_num_arcs); + residual_network->potential = + tal_arrz(residual_network, s64, max_num_nodes); + + if (!residual_network->cap || !residual_network->cost || + !residual_network->potential) { + goto function_fail; + } + return residual_network; + + function_fail: + return tal_free(residual_network); +} + +static void init_residual_network( + const struct linear_network * linear_network, + struct residual_network* residual_network) +{ + const size_t max_num_arcs = linear_network->max_num_arcs; + const size_t max_num_nodes = linear_network->max_num_nodes; + + for(struct arc arc = {0};arc.idx < max_num_arcs; ++arc.idx) + { + if(arc_is_dual(arc)) + continue; + + struct arc dual = arc_dual(arc); + residual_network->cap[arc.idx]=linear_network->capacity[arc.idx]; + residual_network->cap[dual.idx]=0; + + residual_network->cost[arc.idx]=residual_network->cost[dual.idx]=0; + } + for(u32 i=0;ipotential[i]=0; + } +} + +static void combine_cost_function( + const struct linear_network* linear_network, + struct residual_network *residual_network, + s64 mu) +{ + for(struct arc arc = {0};arc.idx < linear_network->max_num_arcs; ++arc.idx) + { + if(arc_tail(linear_network,arc)==INVALID_INDEX) + continue; + + const s64 pcost = linear_network->arc_prob_cost[arc.idx], + fcost = linear_network->arc_fee_cost[arc.idx]; + + const s64 combined = pcost==INFINITE || fcost==INFINITE ? INFINITE : + mu*fcost + (MU_MAX-1-mu)*pcost; + + residual_network->cost[arc.idx] + = mu==0 ? pcost : + (mu==(MU_MAX-1) ? fcost : combined); + } +} + +static void linear_network_add_adjacenct_arc( + struct linear_network *linear_network, + const u32 node_idx, + const struct arc arc) +{ + assert(arc.idx < tal_count(linear_network->arc_tail_node)); + linear_network->arc_tail_node[arc.idx] = node_idx; + + assert(node_idx < tal_count(linear_network->node_adjacency_first_arc)); + const struct arc first_arc = linear_network->node_adjacency_first_arc[node_idx]; + + assert(arc.idx < tal_count(linear_network->node_adjacency_next_arc)); + linear_network->node_adjacency_next_arc[arc.idx]=first_arc; + + assert(node_idx < tal_count(linear_network->node_adjacency_first_arc)); + linear_network->node_adjacency_first_arc[node_idx]=arc; +} + +/* Get the fee cost associated to this directed channel. + * Cost is expressed as PPM of the payment. + * + * Choose and integer `c_fee` to linearize the following fee function + * + * fee_msat = base_msat + floor(millionths*x_msat / 10^6) + * + * into + * + * fee_microsat = c_fee * x_sat + * + * use `base_fee_penalty` to weight the base fee and `delay_feefactor` to + * weight the CLTV delay. + * */ +static s64 linear_fee_cost( + const struct gossmap_chan *c, + const int dir, + double base_fee_penalty, + double delay_feefactor) +{ + assert(c); + assert(dir==0 || dir==1); + s64 pfee = c->half[dir].proportional_fee, + bfee = c->half[dir].base_fee, + delay = c->half[dir].delay; + + return pfee + bfee* base_fee_penalty+ delay*delay_feefactor; +} + +static struct linear_network * +init_linear_network(const tal_t *ctx, const struct pay_parameters *params, + char **fail) +{ + tal_t *this_ctx = tal(ctx,tal_t); + + struct linear_network * linear_network = tal(ctx, struct linear_network); + if (!linear_network) { + if (fail) + *fail = tal_fmt(ctx, "bad allocation of linear_network"); + goto function_fail; + } + + const size_t max_num_chans = gossmap_max_chan_idx(params->gossmap); + const size_t max_num_arcs = max_num_chans * ARCS_PER_CHANNEL; + const size_t max_num_nodes = gossmap_max_node_idx(params->gossmap); + + linear_network->max_num_arcs = max_num_arcs; + linear_network->max_num_nodes = max_num_nodes; + + linear_network->arc_tail_node = tal_arr(linear_network,u32,max_num_arcs); + if(!linear_network->arc_tail_node) + { + if (fail) + *fail = tal_fmt(ctx, "bad allocation of arc_tail_node"); + goto function_fail; + } + for(size_t i=0;iarc_tail_node);++i) + linear_network->arc_tail_node[i]=INVALID_INDEX; + + linear_network->node_adjacency_next_arc = tal_arr(linear_network,struct arc,max_num_arcs); + if(!linear_network->node_adjacency_next_arc) + { + if (fail) + *fail = tal_fmt(ctx, "bad allocation of node_adjacency_next_arc"); + goto function_fail; + } + for(size_t i=0;inode_adjacency_next_arc);++i) + linear_network->node_adjacency_next_arc[i].idx=INVALID_INDEX; + + linear_network->node_adjacency_first_arc = tal_arr(linear_network,struct arc,max_num_nodes); + if(!linear_network->node_adjacency_first_arc) + { + if (fail) + *fail = tal_fmt(ctx, "bad allocation of node_adjacency_first_arc"); + goto function_fail; + } + for(size_t i=0;inode_adjacency_first_arc);++i) + linear_network->node_adjacency_first_arc[i].idx=INVALID_INDEX; + + linear_network->arc_prob_cost = tal_arr(linear_network,s64,max_num_arcs); + if(!linear_network->arc_prob_cost) + { + if (fail) + *fail = tal_fmt(ctx, "bad allocation of arc_prob_cost"); + goto function_fail; + } + for(size_t i=0;iarc_prob_cost);++i) + linear_network->arc_prob_cost[i]=INFINITE; + + linear_network->arc_fee_cost = tal_arr(linear_network,s64,max_num_arcs); + if(!linear_network->arc_fee_cost) + { + if (fail) + *fail = tal_fmt(ctx, "bad allocation of arc_fee_cost"); + goto function_fail; + } + for(size_t i=0;iarc_fee_cost);++i) + linear_network->arc_fee_cost[i]=INFINITE; + + linear_network->capacity = tal_arrz(linear_network,s64,max_num_arcs); + if(!linear_network->capacity) + { + if (fail) + *fail = tal_fmt(ctx, "bad allocation of capacity"); + goto function_fail; + } + + for(struct gossmap_node *node = gossmap_first_node(params->gossmap); + node; + node=gossmap_next_node(params->gossmap,node)) + { + const u32 node_id = gossmap_node_idx(params->gossmap,node); + + for(size_t j=0;jnum_chans;++j) + { + int half; + const struct gossmap_chan *c = gossmap_nth_chan(params->gossmap, + node, j, &half); + + if (!channel_is_available(c, half, params->gossmap, + params->disabled)) + continue; + + const u32 chan_id = gossmap_chan_idx(params->gossmap, c); + + const struct gossmap_node *next = gossmap_nth_node(params->gossmap, + c,!half); + + const u32 next_id = gossmap_node_idx(params->gossmap,next); + + if(node_id==next_id) + continue; + + // `cost` is the word normally used to denote cost per + // unit of flow in the context of MCF. + s64 prob_cost[CHANNEL_PARTS], capacity[CHANNEL_PARTS]; + + // split this channel direction to obtain the arcs + // that are outgoing to `node` + if (!linearize_channel(params, c, half, + capacity, prob_cost)) { + if(fail) + *fail = + tal_fmt(ctx, "linearize_channel failed"); + goto function_fail; + } + + const s64 fee_cost = linear_fee_cost(c,half, + params->base_fee_penalty, + params->delay_feefactor); + + // let's subscribe the 4 parts of the channel direction + // (c,half), the dual of these guys will be subscribed + // when the `i` hits the `next` node. + for(size_t k=0;kcapacity[arc.idx] = capacity[k]; + linear_network->arc_prob_cost[arc.idx] = prob_cost[k]; + + linear_network->arc_fee_cost[arc.idx] = fee_cost; + + // + the respective dual + struct arc dual = arc_dual(arc); + + linear_network_add_adjacenct_arc(linear_network,next_id,dual); + + linear_network->capacity[dual.idx] = 0; + linear_network->arc_prob_cost[dual.idx] = -prob_cost[k]; + + linear_network->arc_fee_cost[dual.idx] = -fee_cost; + } + } + } + + tal_free(this_ctx); + return linear_network; + + function_fail: + tal_free(this_ctx); + return tal_free(linear_network); +} + +/* Simple queue to traverse the network. */ +struct queue_data +{ + u32 idx; + struct lqueue_link ql; +}; + +// TODO(eduardo): unit test this +/* Finds an admissible path from source to target, traversing arcs in the + * residual network with capacity greater than 0. + * The path is encoded into prev, which contains the idx of the arcs that are + * traversed. */ +static bool +find_admissible_path(const tal_t *ctx, + const struct linear_network *linear_network, + const struct residual_network *residual_network, + const u32 source, const u32 target, struct arc *prev) +{ + tal_t *this_ctx = tal(ctx,tal_t); + + bool target_found = false; + + for(size_t i=0;iidx = source; + lqueue_enqueue(&myqueue,qdata); + + while(!lqueue_empty(&myqueue)) + { + qdata = lqueue_dequeue(&myqueue); + u32 cur = qdata->idx; + + tal_free(qdata); + + if(cur==target) + { + target_found = true; + break; + } + + for(struct arc arc = node_adjacency_begin(linear_network,cur); + !node_adjacency_end(arc); + arc = node_adjacency_next(linear_network,arc)) + { + // check if this arc is traversable + if(residual_network->cap[arc.idx] <= 0) + continue; + + u32 next = arc_head(linear_network,arc); + + assert(next < tal_count(prev)); + + // if that node has been seen previously + if(prev[next].idx!=INVALID_INDEX) + continue; + + prev[next] = arc; + + qdata = tal(this_ctx,struct queue_data); + qdata->idx = next; + lqueue_enqueue(&myqueue,qdata); + } + } + tal_free(this_ctx); + return target_found; +} + +/* Get the max amount of flow one can send from source to target along the path + * encoded in `prev`. */ +static s64 get_augmenting_flow( + const struct linear_network* linear_network, + const struct residual_network *residual_network, + const u32 source, + const u32 target, + const struct arc *prev) +{ + s64 flow = INFINITE; + + u32 cur = target; + while(cur!=source) + { + assert(curcap[arc.idx]); + + // we are traversing in the opposite direction to the flow, + // hence the next node is at the tail of the arc. + cur = arc_tail(linear_network,arc); + } + + assert(flow0); + return flow; +} + +/* Augment a `flow` amount along the path defined by `prev`.*/ +static void augment_flow( + const struct linear_network *linear_network, + struct residual_network *residual_network, + const u32 source, + const u32 target, + const struct arc *prev, + s64 flow) +{ + u32 cur = target; + + while(cur!=source) + { + assert(cur < tal_count(prev)); + const struct arc arc = prev[cur]; + const struct arc dual = arc_dual(arc); + + assert(arc.idx < tal_count(residual_network->cap)); + assert(dual.idx < tal_count(residual_network->cap)); + + residual_network->cap[arc.idx] -= flow; + residual_network->cap[dual.idx] += flow; + + assert(residual_network->cap[arc.idx] >=0 ); + + // we are traversing in the opposite direction to the flow, + // hence the next node is at the tail of the arc. + cur = arc_tail(linear_network,arc); + } +} + + +// TODO(eduardo): unit test this +/* Finds any flow that satisfy the capacity and balance constraints of the + * uncertainty network. For the balance function condition we have: + * balance(source) = - balance(target) = amount + * balance(node) = 0 , for every other node + * Returns an error code if no feasible flow is found. + * + * 13/04/2023 This implementation uses a simple augmenting path approach. + * */ +static bool find_feasible_flow(const tal_t *ctx, + const struct linear_network *linear_network, + struct residual_network *residual_network, + const u32 source, const u32 target, s64 amount, + char **fail) +{ + assert(amount>=0); + tal_t *this_ctx = tal(ctx,tal_t); + + /* path information + * prev: is the id of the arc that lead to the node. */ + struct arc *prev = tal_arr(this_ctx,struct arc,linear_network->max_num_nodes); + if(!prev) + { + if(fail) + *fail = tal_fmt(ctx, "bad allocation of prev"); + goto function_fail; + } + + while(amount>0) + { + // find a path from source to target + if (!find_admissible_path(this_ctx, linear_network, + residual_network, source, target, + prev)) + + { + if(fail) + *fail = tal_fmt(ctx, "find_admissible_path failed"); + goto function_fail; + } + + // traverse the path and see how much flow we can send + s64 delta = get_augmenting_flow(linear_network, + residual_network, + source,target,prev); + + // commit that flow to the path + delta = MIN(amount,delta); + assert(delta>0 && delta<=amount); + + augment_flow(linear_network,residual_network,source,target,prev,delta); + amount -= delta; + } + + tal_free(this_ctx); + return true; + + function_fail: + tal_free(this_ctx); + return false; +} + +// TODO(eduardo): unit test this +/* Similar to `find_admissible_path` but use Dijkstra to optimize the distance + * label. Stops when the target is hit. */ +static bool find_optimal_path(const tal_t *ctx, struct dijkstra *dijkstra, + const struct linear_network *linear_network, + const struct residual_network *residual_network, + const u32 source, const u32 target, + struct arc *prev, char **fail) +{ + tal_t *this_ctx = tal(ctx,tal_t); + bool target_found = false; + + bitmap *visited = tal_arrz(this_ctx, bitmap, + BITMAP_NWORDS(linear_network->max_num_nodes)); + + if(!visited) + { + if(fail) + *fail = tal_fmt(ctx, "bad allocation of visited"); + goto finish; + } + + + for(size_t i=0;icap[arc.idx] <= 0) + continue; + + u32 next = arc_head(linear_network,arc); + + s64 cij = residual_network->cost[arc.idx] + - residual_network->potential[cur] + + residual_network->potential[next]; + + // Dijkstra only works with non-negative weights + assert(cij>=0); + + if(distance[next]<=distance[cur]+cij) + continue; + + dijkstra_update(dijkstra,next,distance[cur]+cij); + prev[next]=arc; + } + } + + if (!target_found && fail) + *fail = tal_fmt(ctx, "no route to destination"); + + finish: + tal_free(this_ctx); + return target_found; +} + +/* Set zero flow in the residual network. */ +static void zero_flow( + const struct linear_network *linear_network, + struct residual_network *residual_network) +{ + for(u32 node=0;nodemax_num_nodes;++node) + { + residual_network->potential[node]=0; + for(struct arc arc=node_adjacency_begin(linear_network,node); + !node_adjacency_end(arc); + arc = node_adjacency_next(linear_network,arc)) + { + if(arc_is_dual(arc))continue; + + struct arc dual = arc_dual(arc); + + residual_network->cap[arc.idx] = linear_network->capacity[arc.idx]; + residual_network->cap[dual.idx] = 0; + } + } +} + +// TODO(eduardo): unit test this +/* Starting from a feasible flow (satisfies the balance and capacity + * constraints), find a solution that minimizes the network->cost function. + * + * TODO(eduardo) The MCF must be called several times until we get a good + * compromise between fees and probabilities. Instead of re-computing the MCF at + * each step, we might use the previous flow result, which is not optimal in the + * current iteration but I might be not too far from the truth. + * It comes to mind to use cycle cancelling. */ +static bool optimize_mcf(const tal_t *ctx, struct dijkstra *dijkstra, + const struct linear_network *linear_network, + struct residual_network *residual_network, + const u32 source, const u32 target, const s64 amount, + char **fail) +{ + assert(amount>=0); + tal_t *this_ctx = tal(ctx,tal_t); + char *errmsg; + + zero_flow(linear_network,residual_network); + struct arc *prev = tal_arr(this_ctx,struct arc,linear_network->max_num_nodes); + + const s64 *const distance = dijkstra_distance_data(dijkstra); + + s64 remaining_amount = amount; + + while(remaining_amount>0) + { + if (!find_optimal_path(this_ctx, dijkstra, linear_network, + residual_network, source, target, prev, + &errmsg)) { + if (fail) + *fail = + tal_fmt(ctx, "find_optimal_path failed: %s", + errmsg); + goto function_fail; + } + + // traverse the path and see how much flow we can send + s64 delta = get_augmenting_flow(linear_network,residual_network,source,target,prev); + + // commit that flow to the path + delta = MIN(remaining_amount,delta); + assert(delta>0 && delta<=remaining_amount); + + augment_flow(linear_network,residual_network,source,target,prev,delta); + remaining_amount -= delta; + + // update potentials + for(u32 n=0;nmax_num_nodes;++n) + { + // see page 323 of Ahuja-Magnanti-Orlin + residual_network->potential[n] -= MIN(distance[target],distance[n]); + + /* Notice: + * if node i is permanently labeled we have + * d_i<=d_t + * which implies + * MIN(d_i,d_t) = d_i + * if node i is temporarily labeled we have + * d_i>=d_t + * which implies + * MIN(d_i,d_t) = d_t + * */ + } + } + tal_free(this_ctx); + return true; + + function_fail: + + tal_free(this_ctx); + return false; +} + +// flow on directed channels +struct chan_flow +{ + s64 half[2]; +}; + +/* Search in the network a path of positive flow until we reach a node with + * positive balance. */ +static u32 find_positive_balance( + const struct gossmap *gossmap, + const bitmap *disabled, + const struct chan_flow *chan_flow, + const u32 start_idx, + const s64 *balance, + + const struct gossmap_chan **prev_chan, + int *prev_dir, + u32 *prev_idx) +{ + u32 final_idx = start_idx; + + /* TODO(eduardo) + * This is guaranteed to halt if there are no directed flow cycles. + * There souldn't be any. In fact if cost is strickly + * positive, then flow cycles do not exist at all in the + * MCF solution. But if cost is allowed to be zero for + * some arcs, then we might have flow cyles in the final + * solution. We must somehow ensure that the MCF + * algorithm does not come up with spurious flow cycles. */ + while(balance[final_idx]<=0) + { + // printf("%s: node = %d\n",__PRETTY_FUNCTION__,final_idx); + u32 updated_idx=INVALID_INDEX; + struct gossmap_node *cur + = gossmap_node_byidx(gossmap,final_idx); + + for(size_t i=0;inum_chans;++i) + { + int dir; + const struct gossmap_chan *c + = gossmap_nth_chan(gossmap, + cur,i,&dir); + + if (!channel_is_available(c, dir, gossmap, disabled)) + continue; + + const u32 c_idx = gossmap_chan_idx(gossmap,c); + + // follow the flow + if(chan_flow[c_idx].half[dir]>0) + { + const struct gossmap_node *next + = gossmap_nth_node(gossmap,c,!dir); + u32 next_idx = gossmap_node_idx(gossmap,next); + + + prev_dir[next_idx] = dir; + prev_chan[next_idx] = c; + prev_idx[next_idx] = final_idx; + + updated_idx = next_idx; + break; + } + } + + assert(updated_idx!=INVALID_INDEX); + assert(updated_idx!=final_idx); + + final_idx = updated_idx; + } + return final_idx; +} + +struct list_data +{ + struct list_node list; + struct flow *flow_path; +}; + +static inline uint64_t pseudorand_interval(uint64_t a, uint64_t b) +{ + if (a == b) + return b; + assert(b > a); + return a + pseudorand(b - a); +} + +/* Given a flow in the residual network, build a set of payment flows in the + * gossmap that corresponds to this flow. */ +static struct flow ** +get_flow_paths(const tal_t *ctx, const struct gossmap *gossmap, + const bitmap *disabled, + + // chan_extra_map cannot be const because we use it to keep + // track of htlcs and in_flight sats. + struct chan_extra_map *chan_extra_map, + const struct linear_network *linear_network, + const struct residual_network *residual_network, + + // how many msats in excess we paid for not having msat accuracy + // in the MCF solver + struct amount_msat excess, + + // error message + char **fail) +{ + tal_t *this_ctx = tal(ctx,tal_t); + struct flow **flows = tal_arr(ctx,struct flow*,0); + + assert(amount_msat_less(excess, AMOUNT_MSAT(1000))); + + const size_t max_num_chans = gossmap_max_chan_idx(gossmap); + struct chan_flow *chan_flow = tal_arrz(this_ctx,struct chan_flow,max_num_chans); + + const size_t max_num_nodes = gossmap_max_node_idx(gossmap); + s64 *balance = tal_arrz(this_ctx,s64,max_num_nodes); + + const struct gossmap_chan **prev_chan + = tal_arr(this_ctx,const struct gossmap_chan *,max_num_nodes); + + + int *prev_dir = tal_arr(this_ctx,int,max_num_nodes); + u32 *prev_idx = tal_arr(this_ctx,u32,max_num_nodes); + + if (!chan_flow || !balance || !prev_chan || !prev_idx || !prev_dir) { + if (fail) + *fail = tal_fmt(ctx, "bad allocation"); + goto function_fail; + } + + // Convert the arc based residual network flow into a flow in the + // directed channel network. + // Compute balance on the nodes. + for(u32 n = 0;n htlc_max) { + /* htlc_min is too big or htlc_max is too small, + * we cannot send `delta` along this route. + * + * FIXME: We try anyways because failing + * channels will be blacklisted downstream. */ + htlc_min = 0; + } + + /* If we divide this route into different flows make it + * random to avoid routing nodes making correlations. */ + if (delta > htlc_max) { + // FIXME: choosing a number in the range + // [htlc_min, htlc_max] or + // [0.5 htlc_max, htlc_max] + // The choice of the fraction was completely + // arbitrary. + delta = pseudorand_interval( + MAX(htlc_min, (htlc_max * 50) / 100), + htlc_max); + } + + struct flow *fp = tal(this_ctx,struct flow); + fp->path = tal_arr(fp,const struct gossmap_chan *,length); + fp->dirs = tal_arr(fp,int,length); + + balance[node_idx] += delta; + balance[final_idx]-= delta; + + // walk backwards, substract flow + for(u32 cur_idx = final_idx; + cur_idx!=node_idx; + cur_idx=prev_idx[cur_idx]) + { + assert(cur_idx!=INVALID_INDEX); + + const int dir = prev_dir[cur_idx]; + const struct gossmap_chan *const c = prev_chan[cur_idx]; + const u32 c_idx = gossmap_chan_idx(gossmap,c); + + length--; + fp->path[length]=c; + fp->dirs[length]=dir; + // notice: fp->path and fp->dirs have the path + // in the correct order. + + chan_flow[c_idx].half[prev_dir[cur_idx]]-=delta; + } + + assert(delta>0); + + // substract the excess of msats for not having msat + // accuracy + struct amount_msat delivered = amount_msat(delta*1000); + if (!amount_msat_sub(&delivered, delivered, excess)) { + if (fail) + *fail = tal_fmt( + ctx, "unable to substract excess"); + goto function_fail; + } + excess = amount_msat(0); + fp->amount = delivered; + + fp->success_prob = + flow_probability(fp, gossmap, chan_extra_map); + if (fp->success_prob < 0) { + if (fail) + *fail = + tal_fmt(ctx, "failed to compute " + "flow probability"); + goto function_fail; + } + + // add fp to flows + tal_arr_expand(&flows, fp); + } + } + + /* Establish ownership. */ + for(size_t i=0;i= min_probability; + bool B_prob_pass = B_prob >= min_probability; + + // all bounds are met + if(A_fee_pass && B_fee_pass && A_prob_pass && B_prob_pass) + { + // prefer lower fees + goto fees_or_prob; + } + + // prefer the solution that satisfies both bounds + if(!(A_fee_pass && A_prob_pass) && (B_fee_pass && B_prob_pass)) + { + return false; + } + // prefer the solution that satisfies both bounds + if((A_fee_pass && A_prob_pass) && !(B_fee_pass && B_prob_pass)) + { + return true; + } + + // no solution satisfies both bounds + + // bound on fee is met + if(A_fee_pass && B_fee_pass) + { + // pick the highest prob. + return A_prob > B_prob; + } + + // bound on prob. is met + if(A_prob_pass && B_prob_pass) + { + goto fees_or_prob; + } + + // prefer the solution that satisfies the bound on fees + if(A_fee_pass && !B_fee_pass) + { + return true; + } + if(B_fee_pass && !A_fee_pass) + { + return false; + } + + // none of them satisfy the fee bound + + // prefer the solution that satisfies the bound on prob. + if(A_prob_pass && !B_prob_pass) + { + return true; + } + if(B_prob_pass && !A_prob_pass) + { + return true; + } + + // no bound whatsoever is satisfied + + fees_or_prob: + + // fees are the same, wins the highest prob. + if(amount_msat_eq(A_fee,B_fee)) + { + return A_prob > B_prob; + } + + // go for fees + return amount_msat_less_eq(A_fee,B_fee); +} + +/* Channels that are not in the chan_extra_map should be disabled. */ +static bool check_disabled(const bitmap *disabled, + const struct gossmap *gossmap, + const struct chan_extra_map *chan_extra_map) +{ + assert(disabled); + assert(gossmap); + assert(chan_extra_map); + + if(tal_bytelen(disabled) != bitmap_sizeof(gossmap_max_chan_idx(gossmap))) + return false; + + for (struct gossmap_chan *chan = gossmap_first_chan(gossmap); chan; + chan = gossmap_next_chan(gossmap, chan)) { + const u32 chan_id = gossmap_chan_idx(gossmap, chan); + if (bitmap_test_bit(disabled, chan_id)) + continue; + + struct short_channel_id scid = gossmap_chan_scid(gossmap, chan); + struct chan_extra *ce = + chan_extra_map_get(chan_extra_map, scid); + if (!ce) + return false; + } + return true; +} + +// TODO(eduardo): choose some default values for the minflow parameters +/* eduardo: I think it should be clear that this module deals with linear + * flows, ie. base fees are not considered. Hence a flow along a path is + * described with a sequence of directed channels and one amount. + * In the `pay_flow` module there are dedicated routes to compute the actual + * amount to be forward on each hop. + * + * TODO(eduardo): notice that we don't pay fees to forward payments with local + * channels and we can tell with absolute certainty the liquidity on them. + * Check that local channels have fee costs = 0 and bounds with certainty (min=max). */ +// TODO(eduardo): we should LOG_DBG the process of finding the MCF while +// adjusting the frugality factor. +struct flow **minflow(const tal_t *ctx, struct gossmap *gossmap, + const struct gossmap_node *source, + const struct gossmap_node *target, + struct chan_extra_map *chan_extra_map, + const bitmap *disabled, struct amount_msat amount, + struct amount_msat max_fee, double min_probability, + double delay_feefactor, double base_fee_penalty, + u32 prob_cost_factor, char **fail) +{ + tal_t *this_ctx = tal(ctx,tal_t); + char *errmsg; + struct flow **best_flow_paths = NULL; + + struct pay_parameters *params = tal(this_ctx,struct pay_parameters); + struct dijkstra *dijkstra; + + params->gossmap = gossmap; + params->source = source; + params->target = target; + params->chan_extra_map = chan_extra_map; + + params->disabled = disabled; + + if (!check_disabled(disabled, gossmap, chan_extra_map)) { + if (fail) + *fail = tal_fmt(ctx, "Invalid disabled bitmap."); + goto function_fail; + } + + params->amount = amount; + + // template the channel partition into linear arcs + params->cap_fraction[0]=0; + params->cost_fraction[0]=0; + for(size_t i =1;icap_fraction[i]=CHANNEL_PIVOTS[i]-CHANNEL_PIVOTS[i-1]; + params->cost_fraction[i]= + log((1-CHANNEL_PIVOTS[i-1])/(1-CHANNEL_PIVOTS[i])) + /params->cap_fraction[i]; + + // printf("channel part: %ld, fraction: %lf, cost_fraction: %lf\n", + // i,params->cap_fraction[i],params->cost_fraction[i]); + } + + params->max_fee = max_fee; + params->min_probability = min_probability; + params->delay_feefactor = delay_feefactor; + params->base_fee_penalty = base_fee_penalty; + params->prob_cost_factor = prob_cost_factor; + + // build the uncertainty network with linearization and residual arcs + struct linear_network *linear_network= init_linear_network(this_ctx, params, &errmsg); + if (!linear_network) { + if(fail) + *fail = tal_fmt(ctx, "init_linear_network failed: %s", + errmsg); + goto function_fail; + } + + struct residual_network *residual_network = + alloc_residual_network(this_ctx, linear_network->max_num_nodes, + linear_network->max_num_arcs); + if (!residual_network) { + if (fail) + *fail = tal_fmt( + ctx, "failed to allocate the residual network"); + goto function_fail; + } + + dijkstra = dijkstra_new(this_ctx, gossmap_max_node_idx(params->gossmap)); + + const u32 target_idx = gossmap_node_idx(params->gossmap,target); + const u32 source_idx = gossmap_node_idx(params->gossmap,source); + + init_residual_network(linear_network,residual_network); + + struct amount_msat best_fee; + double best_prob_success; + + /* TODO(eduardo): + * Some MCF algorithms' performance depend on the size of maxflow. If we + * were to work in units of msats we 1. risking overflow when computing + * costs and 2. we risk a performance overhead for no good reason. + * + * Working in units of sats was my first choice, but maybe working in + * units of 10, or 100 sats could be even better. + * + * IDEA: define the size of our precision as some parameter got at + * runtime that depends on the size of the payment and adjust the MCF + * accordingly. + * For example if we are trying to pay 1M sats our precision could be + * set to 1000sat, then channels that had capacity for 3M sats become 3k + * flow units. */ + const u64 pay_amount_msats = params->amount.millisatoshis % 1000; /* Raw: minflow */ + const u64 pay_amount_sats = params->amount.millisatoshis/1000 /* Raw: minflow */ + + (pay_amount_msats ? 1 : 0); + const struct amount_msat excess + = amount_msat(pay_amount_msats ? 1000 - pay_amount_msats : 0); + + if (!find_feasible_flow(this_ctx, linear_network, residual_network, + source_idx, target_idx, pay_amount_sats, + &errmsg)) { + // there is no flow that satisfy the constraints, we stop here + if(fail) + *fail = tal_fmt(ctx, "failed to find a feasible flow: %s", errmsg); + goto function_fail; + } + + // first flow found + best_flow_paths = get_flow_paths( + this_ctx, params->gossmap, params->disabled, params->chan_extra_map, + linear_network, residual_network, excess, &errmsg); + if (!best_flow_paths) { + if (fail) + *fail = + tal_fmt(ctx, "get_flow_paths failed: %s", errmsg); + goto function_fail; + } + best_flow_paths = tal_steal(ctx, best_flow_paths); + + best_prob_success = + flowset_probability(this_ctx, best_flow_paths, params->gossmap, + params->chan_extra_map, &errmsg); + if (best_prob_success < 0) { + if (fail) + *fail = + tal_fmt(ctx, "flowset_probability failed: %s", errmsg); + goto function_fail; + } + if (!flowset_fee(&best_fee, best_flow_paths)) { + if (fail) + *fail = + tal_fmt(ctx, "flowset_fee failed on MaxFlow phase"); + goto function_fail; + } + + // binary search for a value of `mu` that fits our fee and prob. + // constraints. + // mu=0 corresponds to only probabilities + // mu=MU_MAX-1 corresponds to only fee + s64 mu_left = 0, mu_right = MU_MAX; + while(mu_leftgossmap, params->disabled, + params->chan_extra_map, linear_network, + residual_network, excess, &errmsg); + if(!flow_paths) + { + // get_flow_paths doesn't fail unless there is a bug. + if (fail) + *fail = + tal_fmt(ctx, "get_flow_paths failed: %s", errmsg); + goto function_fail; + } + + double prob_success = + flowset_probability(this_ctx, flow_paths, params->gossmap, + params->chan_extra_map, &errmsg); + if (prob_success < 0) { + // flowset_probability doesn't fail unless there is a bug. + if (fail) + *fail = + tal_fmt(ctx, "flowset_probability: %s", errmsg); + goto function_fail; + } + + struct amount_msat fee; + if (!flowset_fee(&fee, flow_paths)) { + // flowset_fee doesn't fail unless there is a bug. + if (fail) + *fail = + tal_fmt(ctx, "flowset_fee failed evaluating MinCostFlow candidate"); + goto function_fail; + } + + /* Is this better than the previous one? */ + if(!best_flow_paths || + is_better(params->max_fee,params->min_probability, + fee,prob_success, + best_fee, best_prob_success)) + { + + best_flow_paths = tal_free(best_flow_paths); + best_flow_paths = tal_steal(ctx,flow_paths); + + best_fee = fee; + best_prob_success=prob_success; + flow_paths = NULL; + } + /* I don't like this candidate. */ + else + tal_free(flow_paths); + + if(amount_msat_greater(fee,params->max_fee)) + { + // too expensive + mu_left = mu+1; + + }else if(prob_success < params->min_probability) + { + // too unlikely + mu_right = mu; + }else + { + // with mu constraints are satisfied, now let's optimize + // the fees + mu_left = mu+1; + } + } + + tal_free(this_ctx); + return best_flow_paths; + + function_fail: + tal_free(this_ctx); + return tal_free(best_flow_paths); +} + diff --git a/plugins/askrene/mcf.h b/plugins/askrene/mcf.h new file mode 100644 index 000000000000..165d7bd42ea6 --- /dev/null +++ b/plugins/askrene/mcf.h @@ -0,0 +1,68 @@ +#ifndef LIGHTNING_PLUGINS_ASKRENE_MCF_H +#define LIGHTNING_PLUGINS_ASKRENE_MCF_H +/* Eduardo Quintela's (lagrang3@protonmail.com) Min Cost Flow implementation + * from renepay, as modified to fit askrene */ +#include "config.h" +#include +#include +#include + +struct chan_extra_map; + +enum { + RENEPAY_ERR_OK, + // No feasible flow found, either there is not enough known liquidity (or capacity) + // in the channels to complete the payment + RENEPAY_ERR_NOFEASIBLEFLOW, + // There is at least one feasible flow, but the the cheapest solution that we + // found is too expensive, we return the result anyways. + RENEPAY_ERR_NOCHEAPFLOW +}; + + + +/** + * optimal_payment_flow - API for min cost flow function(s). + * @ctx: context to allocate returned flows from + * @gossmap: the gossip map + * @source: the source to start from + * @target: the target to pay + * @chan_extra_map: hashtable of extra per-channel information + * @disabled: NULL, or a bitmap by channel index of channels not to use. + * @amount: the amount we want to reach @target + * + * @max_fee: the maximum allowed in fees + * + * @min_probability: minimum probability accepted + * + * @delay_feefactor converts 1 block delay into msat, as if it were an additional + * fee. So if a CLTV delay on a node is 5 blocks, that's treated as if it + * were a fee of 5 * @delay_feefactor. + * + * @base_fee_penalty: factor to compute additional proportional cost from each + * unit of base fee. So #base_fee_penalty will be added to the effective + * proportional fee for each msat of base fee. + * + * effective_ppm = proportional_fee + base_fee_msat * base_fee_penalty + * + * @prob_cost_factor: factor used to monetize the probability cost. It is + * defined as the number of ppm (parts per million of the total payment) we + * are willing to pay to improve the probability of success by 0.1%. + * + * k_microsat = floor(1000*prob_cost_factor * payment_sat) + * + * this k is used to compute a prob. cost in units of microsats + * + * cost(payment) = - k_microsat * log Prob(payment) + * + * Return a series of subflows which deliver amount to target, or NULL. + */ +struct flow **minflow(const tal_t *ctx, struct gossmap *gossmap, + const struct gossmap_node *source, + const struct gossmap_node *target, + struct chan_extra_map *chan_extra_map, + const bitmap *disabled, struct amount_msat amount, + struct amount_msat max_fee, double min_probability, + double delay_feefactor, double base_fee_penalty, + u32 prob_cost_factor, char **fail); +#endif /* LIGHTNING_PLUGINS_ASKRENE_MCF_H */ From 5393432db92bab942c13dd0dc2a003f8e83edd4a Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Wed, 7 Aug 2024 11:19:54 +0930 Subject: [PATCH 15/28] askrene: simplify minflow() We let the caller choose mu, and iterate if necessary: it can also check its limits for fees, etc. Rationalize it to 0-100 inclusive for human consumption. This means we don't loop internally, and in fact there's only one failure mode: we cannot find enough capacity. Signed-off-by: Rusty Russell --- plugins/askrene/mcf.c | 279 ++++-------------------------------------- plugins/askrene/mcf.h | 17 +-- 2 files changed, 28 insertions(+), 268 deletions(-) diff --git a/plugins/askrene/mcf.c b/plugins/askrene/mcf.c index 8c3a7d349da9..da0568a752a2 100644 --- a/plugins/askrene/mcf.c +++ b/plugins/askrene/mcf.c @@ -7,9 +7,9 @@ #include #include #include +#include #include #include -#include #include /* # Optimal payments @@ -193,7 +193,7 @@ static const double CHANNEL_PIVOTS[]={0,0.5,0.8,0.95}; static const s64 INFINITE = INT64_MAX; static const u64 INFINITE_MSAT = UINT64_MAX; static const u32 INVALID_INDEX = 0xffffffff; -static const s64 MU_MAX = 128; +static const s64 MU_MAX = 101; /* Let's try this encoding of arcs: * Each channel `c` has two possible directions identified by a bit @@ -1468,119 +1468,6 @@ get_flow_paths(const tal_t *ctx, const struct gossmap *gossmap, return tal_free(flows); } -/* Given the constraints on max fee and min prob., - * is the flow A better than B? */ -static bool is_better( - struct amount_msat max_fee, - double min_probability, - - struct amount_msat A_fee, - double A_prob, - - struct amount_msat B_fee, - double B_prob) -{ - bool A_fee_pass = amount_msat_less_eq(A_fee,max_fee); - bool B_fee_pass = amount_msat_less_eq(B_fee,max_fee); - bool A_prob_pass = A_prob >= min_probability; - bool B_prob_pass = B_prob >= min_probability; - - // all bounds are met - if(A_fee_pass && B_fee_pass && A_prob_pass && B_prob_pass) - { - // prefer lower fees - goto fees_or_prob; - } - - // prefer the solution that satisfies both bounds - if(!(A_fee_pass && A_prob_pass) && (B_fee_pass && B_prob_pass)) - { - return false; - } - // prefer the solution that satisfies both bounds - if((A_fee_pass && A_prob_pass) && !(B_fee_pass && B_prob_pass)) - { - return true; - } - - // no solution satisfies both bounds - - // bound on fee is met - if(A_fee_pass && B_fee_pass) - { - // pick the highest prob. - return A_prob > B_prob; - } - - // bound on prob. is met - if(A_prob_pass && B_prob_pass) - { - goto fees_or_prob; - } - - // prefer the solution that satisfies the bound on fees - if(A_fee_pass && !B_fee_pass) - { - return true; - } - if(B_fee_pass && !A_fee_pass) - { - return false; - } - - // none of them satisfy the fee bound - - // prefer the solution that satisfies the bound on prob. - if(A_prob_pass && !B_prob_pass) - { - return true; - } - if(B_prob_pass && !A_prob_pass) - { - return true; - } - - // no bound whatsoever is satisfied - - fees_or_prob: - - // fees are the same, wins the highest prob. - if(amount_msat_eq(A_fee,B_fee)) - { - return A_prob > B_prob; - } - - // go for fees - return amount_msat_less_eq(A_fee,B_fee); -} - -/* Channels that are not in the chan_extra_map should be disabled. */ -static bool check_disabled(const bitmap *disabled, - const struct gossmap *gossmap, - const struct chan_extra_map *chan_extra_map) -{ - assert(disabled); - assert(gossmap); - assert(chan_extra_map); - - if(tal_bytelen(disabled) != bitmap_sizeof(gossmap_max_chan_idx(gossmap))) - return false; - - for (struct gossmap_chan *chan = gossmap_first_chan(gossmap); chan; - chan = gossmap_next_chan(gossmap, chan)) { - const u32 chan_id = gossmap_chan_idx(gossmap, chan); - if (bitmap_test_bit(disabled, chan_id)) - continue; - - struct short_channel_id scid = gossmap_chan_scid(gossmap, chan); - struct chan_extra *ce = - chan_extra_map_get(chan_extra_map, scid); - if (!ce) - return false; - } - return true; -} - // TODO(eduardo): choose some default values for the minflow parameters /* eduardo: I think it should be clear that this module deals with linear * flows, ie. base fees are not considered. Hence a flow along a path is @@ -1595,16 +1482,14 @@ static bool check_disabled(const bitmap *disabled, // adjusting the frugality factor. struct flow **minflow(const tal_t *ctx, struct gossmap *gossmap, const struct gossmap_node *source, - const struct gossmap_node *target, - struct chan_extra_map *chan_extra_map, - const bitmap *disabled, struct amount_msat amount, - struct amount_msat max_fee, double min_probability, + struct amount_msat amount, + u32 mu, double delay_feefactor, double base_fee_penalty, - u32 prob_cost_factor, char **fail) + u32 prob_cost_factor) { tal_t *this_ctx = tal(ctx,tal_t); char *errmsg; - struct flow **best_flow_paths = NULL; + struct flow **flow_paths; struct pay_parameters *params = tal(this_ctx,struct pay_parameters); struct dijkstra *dijkstra; @@ -1612,16 +1497,6 @@ struct flow **minflow(const tal_t *ctx, struct gossmap *gossmap, params->gossmap = gossmap; params->source = source; params->target = target; - params->chan_extra_map = chan_extra_map; - - params->disabled = disabled; - - if (!check_disabled(disabled, gossmap, chan_extra_map)) { - if (fail) - *fail = tal_fmt(ctx, "Invalid disabled bitmap."); - goto function_fail; - } - params->amount = amount; // template the channel partition into linear arcs @@ -1638,8 +1513,6 @@ struct flow **minflow(const tal_t *ctx, struct gossmap *gossmap, // i,params->cap_fraction[i],params->cost_fraction[i]); } - params->max_fee = max_fee; - params->min_probability = min_probability; params->delay_feefactor = delay_feefactor; params->base_fee_penalty = base_fee_penalty; params->prob_cost_factor = prob_cost_factor; @@ -1670,9 +1543,6 @@ struct flow **minflow(const tal_t *ctx, struct gossmap *gossmap, init_residual_network(linear_network,residual_network); - struct amount_msat best_fee; - double best_prob_success; - /* TODO(eduardo): * Some MCF algorithms' performance depend on the size of maxflow. If we * were to work in units of msats we 1. risking overflow when computing @@ -1701,135 +1571,30 @@ struct flow **minflow(const tal_t *ctx, struct gossmap *gossmap, *fail = tal_fmt(ctx, "failed to find a feasible flow: %s", errmsg); goto function_fail; } + combine_cost_function(linear_network, residual_network, mu); - // first flow found - best_flow_paths = get_flow_paths( - this_ctx, params->gossmap, params->disabled, params->chan_extra_map, - linear_network, residual_network, excess, &errmsg); - if (!best_flow_paths) { - if (fail) - *fail = - tal_fmt(ctx, "get_flow_paths failed: %s", errmsg); - goto function_fail; - } - best_flow_paths = tal_steal(ctx, best_flow_paths); - - best_prob_success = - flowset_probability(this_ctx, best_flow_paths, params->gossmap, - params->chan_extra_map, &errmsg); - if (best_prob_success < 0) { - if (fail) - *fail = - tal_fmt(ctx, "flowset_probability failed: %s", errmsg); - goto function_fail; - } - if (!flowset_fee(&best_fee, best_flow_paths)) { - if (fail) - *fail = - tal_fmt(ctx, "flowset_fee failed on MaxFlow phase"); - goto function_fail; - } - - // binary search for a value of `mu` that fits our fee and prob. - // constraints. - // mu=0 corresponds to only probabilities - // mu=MU_MAX-1 corresponds to only fee - s64 mu_left = 0, mu_right = MU_MAX; - while(mu_leftgossmap, params->disabled, - params->chan_extra_map, linear_network, - residual_network, excess, &errmsg); - if(!flow_paths) - { - // get_flow_paths doesn't fail unless there is a bug. - if (fail) - *fail = - tal_fmt(ctx, "get_flow_paths failed: %s", errmsg); - goto function_fail; - } - - double prob_success = - flowset_probability(this_ctx, flow_paths, params->gossmap, - params->chan_extra_map, &errmsg); - if (prob_success < 0) { - // flowset_probability doesn't fail unless there is a bug. - if (fail) - *fail = - tal_fmt(ctx, "flowset_probability: %s", errmsg); - goto function_fail; - } - - struct amount_msat fee; - if (!flowset_fee(&fee, flow_paths)) { - // flowset_fee doesn't fail unless there is a bug. - if (fail) + // optimize_mcf doesn't fail unless there is a bug. + if (fail) *fail = - tal_fmt(ctx, "flowset_fee failed evaluating MinCostFlow candidate"); - goto function_fail; - } - - /* Is this better than the previous one? */ - if(!best_flow_paths || - is_better(params->max_fee,params->min_probability, - fee,prob_success, - best_fee, best_prob_success)) - { - - best_flow_paths = tal_free(best_flow_paths); - best_flow_paths = tal_steal(ctx,flow_paths); - - best_fee = fee; - best_prob_success=prob_success; - flow_paths = NULL; - } - /* I don't like this candidate. */ - else - tal_free(flow_paths); - - if(amount_msat_greater(fee,params->max_fee)) - { - // too expensive - mu_left = mu+1; - - }else if(prob_success < params->min_probability) - { - // too unlikely - mu_right = mu; - }else - { - // with mu constraints are satisfied, now let's optimize - // the fees - mu_left = mu+1; - } + tal_fmt(ctx, "optimize_mcf failed: %s", errmsg); + goto function_fail; } + /* We dissect the solution of the MCF into payment routes. + * Actual amounts considering fees are computed for every + * channel in the routes. */ + flow_paths = get_flow_paths(this_ctx, params->gossmap, params->disabled, + params->chan_extra_map, linear_network, + residual_network, excess, &errmsg); tal_free(this_ctx); - return best_flow_paths; + return flow_paths; function_fail: tal_free(this_ctx); - return tal_free(best_flow_paths); + return NULL; } diff --git a/plugins/askrene/mcf.h b/plugins/askrene/mcf.h index 165d7bd42ea6..08037aeab2b3 100644 --- a/plugins/askrene/mcf.h +++ b/plugins/askrene/mcf.h @@ -27,13 +27,8 @@ enum { * @gossmap: the gossip map * @source: the source to start from * @target: the target to pay - * @chan_extra_map: hashtable of extra per-channel information - * @disabled: NULL, or a bitmap by channel index of channels not to use. * @amount: the amount we want to reach @target - * - * @max_fee: the maximum allowed in fees - * - * @min_probability: minimum probability accepted + * @mu: 0 = corresponds to only probabilities, 100 corresponds to only fee. * * @delay_feefactor converts 1 block delay into msat, as if it were an additional * fee. So if a CLTV delay on a node is 5 blocks, that's treated as if it @@ -60,9 +55,9 @@ enum { struct flow **minflow(const tal_t *ctx, struct gossmap *gossmap, const struct gossmap_node *source, const struct gossmap_node *target, - struct chan_extra_map *chan_extra_map, - const bitmap *disabled, struct amount_msat amount, - struct amount_msat max_fee, double min_probability, - double delay_feefactor, double base_fee_penalty, - u32 prob_cost_factor, char **fail); + struct amount_msat amount, + u32 mu, + double delay_feefactor, + double base_fee_penalty, + u32 prob_cost_factor); #endif /* LIGHTNING_PLUGINS_ASKRENE_MCF_H */ From 4cd80daa5a1d232ba0f64fc371a25bf79be8eab3 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Wed, 7 Aug 2024 11:19:54 +0930 Subject: [PATCH 16/28] askrene: copy flow and dijkstra from renepay. Still don't actually try compiling them. Signed-off-by: Rusty Russell --- plugins/askrene/dijkstra.c | 186 +++++++++++++++ plugins/askrene/dijkstra.h | 30 +++ plugins/askrene/flow.c | 451 +++++++++++++++++++++++++++++++++++++ plugins/askrene/flow.h | 99 ++++++++ plugins/askrene/mcf.c | 4 +- 5 files changed, 768 insertions(+), 2 deletions(-) create mode 100644 plugins/askrene/dijkstra.c create mode 100644 plugins/askrene/dijkstra.h create mode 100644 plugins/askrene/flow.c create mode 100644 plugins/askrene/flow.h diff --git a/plugins/askrene/dijkstra.c b/plugins/askrene/dijkstra.c new file mode 100644 index 000000000000..b3f9d39deb7b --- /dev/null +++ b/plugins/askrene/dijkstra.c @@ -0,0 +1,186 @@ +#define NDEBUG 1 +#include "config.h" +#include + +/* In the heap we keep node idx, but in this structure we keep the distance + * value associated to every node, and their position in the heap as a pointer + * so that we can update the nodes inside the heap when the distance label is + * changed. + * + * Therefore this is no longer a multipurpose heap, the node_idx must be an + * index between 0 and less than max_num_nodes. */ +struct dijkstra { + // + s64 *distance; + u32 *base; + u32 **heapptr; + size_t heapsize; + struct gheap_ctx gheap_ctx; +}; + +static const s64 INFINITE = INT64_MAX; + +/* Required a global dijkstra for gheap. */ +static struct dijkstra *global_dijkstra; + +/* The heap comparer for Dijkstra search. Since the top element must be the one + * with the smallest distance, we use the operator >, rather than <. */ +static int dijkstra_less_comparer( + const void *const ctx UNUSED, + const void *const a, + const void *const b) +{ + return global_dijkstra->distance[*(u32*)a] + > global_dijkstra->distance[*(u32*)b]; +} + +/* The heap move operator for Dijkstra search. */ +static void dijkstra_item_mover(void *const dst, const void *const src) +{ + u32 src_idx = *(u32*)src; + *(u32*)dst = src_idx; + + // we keep track of the pointer position of each element in the heap, + // for easy update. + global_dijkstra->heapptr[src_idx] = dst; +} + +/* Allocation of resources for the heap. */ +struct dijkstra *dijkstra_new(const tal_t *ctx, size_t max_num_nodes) +{ + struct dijkstra *dijkstra = tal(ctx, struct dijkstra); + + dijkstra->distance = tal_arr(dijkstra,s64,max_num_nodes); + dijkstra->base = tal_arr(dijkstra,u32,max_num_nodes); + dijkstra->heapptr = tal_arrz(dijkstra,u32*,max_num_nodes); + + dijkstra->heapsize=0; + + dijkstra->gheap_ctx.fanout=2; + dijkstra->gheap_ctx.page_chunks=1024; + dijkstra->gheap_ctx.item_size=sizeof(dijkstra->base[0]); + dijkstra->gheap_ctx.less_comparer=dijkstra_less_comparer; + dijkstra->gheap_ctx.less_comparer_ctx=NULL; + dijkstra->gheap_ctx.item_mover=dijkstra_item_mover; + + return dijkstra; +} + + +void dijkstra_init(struct dijkstra *dijkstra) +{ + const size_t max_num_nodes = tal_count(dijkstra->distance); + dijkstra->heapsize=0; + for(size_t i=0;idistance[i]=INFINITE; + dijkstra->heapptr[i] = NULL; + } +} +size_t dijkstra_size(const struct dijkstra *dijkstra) +{ + return dijkstra->heapsize; +} + +size_t dijkstra_maxsize(const struct dijkstra *dijkstra) +{ + return tal_count(dijkstra->distance); +} + +static void dijkstra_append(struct dijkstra *dijkstra, u32 node_idx, s64 distance) +{ + assert(dijkstra_size(dijkstra) < dijkstra_maxsize(dijkstra)); + assert(node_idx < dijkstra_maxsize(dijkstra)); + + const size_t pos = dijkstra->heapsize; + + dijkstra->base[pos]=node_idx; + dijkstra->distance[node_idx]=distance; + dijkstra->heapptr[node_idx] = &(dijkstra->base[pos]); + dijkstra->heapsize++; +} + +void dijkstra_update(struct dijkstra *dijkstra, u32 node_idx, s64 distance) +{ + assert(node_idx < dijkstra_maxsize(dijkstra)); + + if(!dijkstra->heapptr[node_idx]) + { + // not in the heap + dijkstra_append(dijkstra, node_idx,distance); + global_dijkstra = dijkstra; + gheap_restore_heap_after_item_increase( + &dijkstra->gheap_ctx, + dijkstra->base, + dijkstra->heapsize, + dijkstra->heapptr[node_idx] + - dijkstra->base); + global_dijkstra = NULL; + return; + } + + if(dijkstra->distance[node_idx] > distance) + { + // distance decrease + dijkstra->distance[node_idx] = distance; + + global_dijkstra = dijkstra; + gheap_restore_heap_after_item_increase( + &dijkstra->gheap_ctx, + dijkstra->base, + dijkstra->heapsize, + dijkstra->heapptr[node_idx] + - dijkstra->base); + global_dijkstra = NULL; + }else + { + // distance increase + dijkstra->distance[node_idx] = distance; + + global_dijkstra = dijkstra; + gheap_restore_heap_after_item_decrease( + &dijkstra->gheap_ctx, + dijkstra->base, + dijkstra->heapsize, + dijkstra->heapptr[node_idx] + - dijkstra->base); + global_dijkstra = NULL; + + } + // assert(gheap_is_heap(&dijkstra->gheap_ctx, + // dijkstra->base, + // dijkstra_size())); +} + +u32 dijkstra_top(const struct dijkstra *dijkstra) +{ + return dijkstra->base[0]; +} + +bool dijkstra_empty(const struct dijkstra *dijkstra) +{ + return dijkstra->heapsize==0; +} + +void dijkstra_pop(struct dijkstra *dijkstra) +{ + if(dijkstra->heapsize==0) + return; + + const u32 top = dijkstra_top(dijkstra); + assert(dijkstra->heapptr[top]==dijkstra->base); + + global_dijkstra = dijkstra; + gheap_pop_heap( + &dijkstra->gheap_ctx, + dijkstra->base, + dijkstra->heapsize--); + global_dijkstra = NULL; + + dijkstra->heapptr[top]=NULL; +} + +const s64* dijkstra_distance_data(const struct dijkstra *dijkstra) +{ + return dijkstra->distance; +} diff --git a/plugins/askrene/dijkstra.h b/plugins/askrene/dijkstra.h new file mode 100644 index 000000000000..f8ff62a8ad7f --- /dev/null +++ b/plugins/askrene/dijkstra.h @@ -0,0 +1,30 @@ +#ifndef LIGHTNING_PLUGINS_ASKRENE_DIJKSTRA_H +#define LIGHTNING_PLUGINS_ASKRENE_DIJKSTRA_H +#include "config.h" +#include +#include +#include + +/* Allocation of resources for the heap. */ +struct dijkstra *dijkstra_new(const tal_t *ctx, size_t max_num_nodes); + +/* Initialization of the heap for a new Dijkstra search. */ +void dijkstra_init(struct dijkstra *dijkstra); + +/* Inserts a new element in the heap. If node_idx was already in the heap then + * its distance value is updated. */ +void dijkstra_update(struct dijkstra *dijkstra, u32 node_idx, s64 distance); + +u32 dijkstra_top(const struct dijkstra *dijkstra); +bool dijkstra_empty(const struct dijkstra *dijkstra); +void dijkstra_pop(struct dijkstra *dijkstra); + +const s64* dijkstra_distance_data(const struct dijkstra *dijkstra); + +/* Number of elements on the heap. */ +size_t dijkstra_size(const struct dijkstra *dijkstra); + +/* Maximum number of elements the heap can host */ +size_t dijkstra_maxsize(const struct dijkstra *dijkstra); + +#endif /* LIGHTNING_PLUGINS_ASKRENE_DIJKSTRA_H */ diff --git a/plugins/askrene/flow.c b/plugins/askrene/flow.c new file mode 100644 index 000000000000..2f67fb3bebbc --- /dev/null +++ b/plugins/askrene/flow.c @@ -0,0 +1,451 @@ +#include "config.h" +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef SUPERVERBOSE +#define SUPERVERBOSE(...) +#else +#define SUPERVERBOSE_ENABLED 1 +#endif + +struct amount_msat *tal_flow_amounts(const tal_t *ctx, const struct flow *flow) +{ + const size_t pathlen = tal_count(flow->path); + struct amount_msat *amounts = tal_arr(ctx, struct amount_msat, pathlen); + amounts[pathlen - 1] = flow->amount; + + for (int i = (int)pathlen - 2; i >= 0; i--) { + const struct half_chan *h = flow_edge(flow, i + 1); + amounts[i] = amounts[i + 1]; + if (!amount_msat_add_fee(&amounts[i], h->base_fee, + h->proportional_fee)) + goto function_fail; + } + + return amounts; + +function_fail: + return tal_free(amounts); +} + +const char *fmt_flows(const tal_t *ctx, const struct gossmap *gossmap, + struct chan_extra_map *chan_extra_map, + struct flow **flows) +{ + tal_t *this_ctx = tal(ctx, tal_t); + double tot_prob = + flowset_probability(tmpctx, flows, gossmap, chan_extra_map, NULL); + assert(tot_prob >= 0); + char *buff = tal_fmt(ctx, "%zu subflows, prob %2lf\n", tal_count(flows), + tot_prob); + for (size_t i = 0; i < tal_count(flows); i++) { + struct amount_msat fee, delivered; + tal_append_fmt(&buff, " "); + for (size_t j = 0; j < tal_count(flows[i]->path); j++) { + struct short_channel_id scid = + gossmap_chan_scid(gossmap, flows[i]->path[j]); + tal_append_fmt(&buff, "%s%s", j ? "->" : "", + fmt_short_channel_id(this_ctx, scid)); + } + delivered = flows[i]->amount; + if (!flow_fee(&fee, flows[i])) { + abort(); + } + tal_append_fmt(&buff, " prob %.2f, %s delivered with fee %s\n", + flows[i]->success_prob, + fmt_amount_msat(this_ctx, delivered), + fmt_amount_msat(this_ctx, fee)); + } + + tal_free(this_ctx); + return buff; +} + +/* Returns the greatest amount we can deliver to the destination using this + * route. It takes into account the current knowledge, pending HTLC, + * htlc_max and fees. + * + * It fails if the maximum that we can + * deliver at node i is smaller than the minimum required to forward the least + * amount greater than zero to the next node. */ +enum askrene_errorcode +flow_maximum_deliverable(struct amount_msat *max_deliverable, + const struct flow *flow, + const struct gossmap *gossmap, + struct chan_extra_map *chan_extra_map, + const struct gossmap_chan **bad_channel) +{ + assert(tal_count(flow->path) > 0); + assert(tal_count(flow->dirs) > 0); + assert(tal_count(flow->path) == tal_count(flow->dirs)); + struct amount_msat x; + enum askrene_errorcode err; + + err = channel_liquidity(&x, gossmap, chan_extra_map, flow->path[0], + flow->dirs[0]); + if(err){ + if(bad_channel)*bad_channel = flow->path[0]; + return err; + } + x = amount_msat_min(x, channel_htlc_max(flow->path[0], flow->dirs[0])); + + if(amount_msat_zero(x)) + { + if(bad_channel)*bad_channel = flow->path[0]; + return ASKRENE_BAD_CHANNEL; + } + + for (size_t i = 1; i < tal_count(flow->path); ++i) { + // ith node can forward up to 'liquidity_cap' because of the ith + // channel liquidity bound + struct amount_msat liquidity_cap; + + err = channel_liquidity(&liquidity_cap, gossmap, chan_extra_map, + flow->path[i], flow->dirs[i]); + if(err) { + if(bad_channel)*bad_channel = flow->path[i]; + return err; + } + + /* ith node can receive up to 'x', therefore he will not forward + * more than 'forward_cap' that we compute below inverting the + * fee equation. */ + struct amount_msat forward_cap; + err = channel_maximum_forward(&forward_cap, flow->path[i], + flow->dirs[i], x); + if(err) + { + if(bad_channel)*bad_channel = flow->path[i]; + return err; + } + struct amount_msat x_new = + amount_msat_min(forward_cap, liquidity_cap); + x_new = amount_msat_min( + x_new, channel_htlc_max(flow->path[i], flow->dirs[i])); + + /* safety check: amounts decrease along the route */ + assert(amount_msat_less_eq(x_new, x)); + + if(amount_msat_zero(x_new)) + { + if(bad_channel)*bad_channel = flow->path[i]; + return ASKRENE_BAD_CHANNEL; + } + + /* safety check: the max liquidity in the next hop + fees cannot + be greater than the max liquidity in the current hop, IF the + next hop is non-zero. */ + struct amount_msat x_check = x_new; + assert( + amount_msat_add_fee(&x_check, flow_edge(flow, i)->base_fee, + flow_edge(flow, i)->proportional_fee)); + assert(amount_msat_less_eq(x_check, x)); + + x = x_new; + } + assert(!amount_msat_zero(x)); + *max_deliverable = x; + return ASKRENE_NOERROR; +} + +/* Returns the smallest amount we can send so that the destination can get one + * HTLC of any size. It takes into account htlc_min and fees. + * */ +// static enum askrene_errorcode +// flow_minimum_sendable(struct amount_msat *min_sendable UNUSED, +// const struct flow *flow UNUSED, +// const struct gossmap *gossmap UNUSED, +// struct chan_extra_map *chan_extra_map UNUSED) +// { +// // TODO +// return ASKRENE_NOERROR; +// } + +/* How much do we deliver to destination using this set of routes */ +bool flowset_delivers(struct amount_msat *delivers, struct flow **flows) +{ + struct amount_msat final = AMOUNT_MSAT(0); + for (size_t i = 0; i < tal_count(flows); i++) { + if (!amount_msat_add(&final, flows[i]->amount, final)) + return false; + } + *delivers = final; + return true; +} + +/* Checks if the flows satisfy the liquidity bounds imposed by the known maximum + * liquidity and pending HTLCs. + * + * FIXME The function returns false even in the case of failure. The caller has + * no way of knowing the difference between a failure of evaluation and a + * negative answer. */ +// static bool check_liquidity_bounds(struct flow **flows, +// const struct gossmap *gossmap, +// struct chan_extra_map *chan_extra_map) +// { +// bool check = true; +// for (size_t i = 0; i < tal_count(flows); ++i) { +// struct amount_msat max_deliverable; +// if (!flow_maximum_deliverable(&max_deliverable, flows[i], +// gossmap, chan_extra_map)) +// return false; +// struct amount_msat delivers = flow_delivers(flows[i]); +// check &= amount_msat_less_eq(delivers, max_deliverable); +// } +// return check; +// } + +/* Compute the prob. of success of a set of concurrent set of flows. + * + * IMPORTANT: this is not simply the multiplication of the prob. of success of + * all of them, because they're not independent events. A flow that passes + * through a channel c changes that channel's liquidity and then if another flow + * passes through that same channel the previous liquidity change must be taken + * into account. + * + * P(A and B) != P(A) * P(B), + * + * but + * + * P(A and B) = P(A) * P(B | A) + * + * also due to the linear form of P() we have + * + * P(A and B) = P(A + B) + * */ +struct chan_inflight_flow +{ + struct amount_msat half[2]; +}; + +// TODO(eduardo): here chan_extra_map should be const +// TODO(eduardo): here flows should be const +double flowset_probability(const tal_t *ctx, struct flow **flows, + const struct gossmap *const gossmap, + struct chan_extra_map *chan_extra_map, char **fail) +{ + assert(flows); + assert(gossmap); + assert(chan_extra_map); + tal_t *this_ctx = tal(ctx, tal_t); + double prob = 1.0; + + // TODO(eduardo): should it be better to use a map instead of an array + // here? + const size_t max_num_chans = gossmap_max_chan_idx(gossmap); + struct chan_inflight_flow *in_flight = + tal_arr(this_ctx, struct chan_inflight_flow, max_num_chans); + + for (size_t i = 0; i < max_num_chans; ++i) { + in_flight[i].half[0] = in_flight[i].half[1] = AMOUNT_MSAT(0); + } + + for (size_t i = 0; i < tal_count(flows); ++i) { + const struct flow *f = flows[i]; + const size_t pathlen = tal_count(f->path); + struct amount_msat *amounts = tal_flow_amounts(this_ctx, f); + if (!amounts) + { + if (fail) + *fail = tal_fmt( + ctx, + "failed to compute amounts along the path"); + goto function_fail; + } + + for (size_t j = 0; j < pathlen; ++j) { + const struct chan_extra_half *h = + get_chan_extra_half_by_chan(gossmap, chan_extra_map, + f->path[j], f->dirs[j]); + if (!h) { + if (fail) + *fail = tal_fmt( + ctx, + "channel not found in chan_extra_map"); + goto function_fail; + } + const u32 c_idx = gossmap_chan_idx(gossmap, f->path[j]); + const int c_dir = f->dirs[j]; + + const struct amount_msat deliver = amounts[j]; + + struct amount_msat prev_flow; + if (!amount_msat_add(&prev_flow, h->htlc_total, + in_flight[c_idx].half[c_dir])) { + if (fail) + *fail = tal_fmt( + ctx, "in-flight amount_msat overflow"); + goto function_fail; + } + + double edge_prob = + edge_probability(h->known_min, h->known_max, + prev_flow, deliver); + if (edge_prob < 0) { + if (fail) + *fail = tal_fmt(ctx, + "edge_probability failed"); + goto function_fail; + } + prob *= edge_prob; + + if (!amount_msat_add(&in_flight[c_idx].half[c_dir], + in_flight[c_idx].half[c_dir], + deliver)) { + if (fail) + *fail = tal_fmt( + ctx, "in-flight amount_msat overflow"); + goto function_fail; + } + } + } + tal_free(this_ctx); + return prob; + + function_fail: + tal_free(this_ctx); + return -1; +} + +bool flow_spend(struct amount_msat *ret, struct flow *flow) +{ + assert(ret); + assert(flow); + const size_t pathlen = tal_count(flow->path); + struct amount_msat spend = flow->amount; + + for (int i = (int)pathlen - 2; i >= 0; i--) { + const struct half_chan *h = flow_edge(flow, i + 1); + if (!amount_msat_add_fee(&spend, h->base_fee, + h->proportional_fee)) + goto function_fail; + } + + *ret = spend; + return true; + +function_fail: + return false; +} + +bool flow_fee(struct amount_msat *ret, struct flow *flow) +{ + assert(ret); + assert(flow); + struct amount_msat fee; + struct amount_msat spend; + if (!flow_spend(&spend, flow)) + goto function_fail; + if (!amount_msat_sub(&fee, spend, flow->amount)) + goto function_fail; + + *ret = fee; + return true; + +function_fail: + return false; +} + +bool flowset_fee(struct amount_msat *ret, struct flow **flows) +{ + assert(ret); + assert(flows); + struct amount_msat fee = AMOUNT_MSAT(0); + for (size_t i = 0; i < tal_count(flows); i++) { + struct amount_msat this_fee; + if (!flow_fee(&this_fee, flows[i])) + return false; + if (!amount_msat_add(&fee, this_fee, fee)) + return false; + } + *ret = fee; + return true; +} + +/* Helper to access the half chan at flow index idx */ +const struct half_chan *flow_edge(const struct flow *flow, size_t idx) +{ + assert(flow); + assert(idx < tal_count(flow->path)); + return &flow->path[idx]->half[flow->dirs[idx]]; +} + +/* Assign the delivered amount to the flow if it fits + the path maximum capacity. */ +bool flow_assign_delivery(struct flow *flow, const struct gossmap *gossmap, + struct chan_extra_map *chan_extra_map, + struct amount_msat requested_amount) +{ + struct amount_msat max_deliverable = AMOUNT_MSAT(0); + if (flow_maximum_deliverable(&max_deliverable, flow, gossmap, + chan_extra_map, NULL)) + return false; + assert(!amount_msat_zero(max_deliverable)); + flow->amount = amount_msat_min(requested_amount, max_deliverable); + return true; +} + +/* Helper function to find the success_prob for a single flow + * + * IMPORTANT: flow->success_prob is misleading, because that's the prob. of + * success provided that there are no other flows in the current MPP flow set. + * */ +double flow_probability(struct flow *flow, const struct gossmap *gossmap, + struct chan_extra_map *chan_extra_map) +{ + assert(flow); + assert(gossmap); + assert(chan_extra_map); + const size_t pathlen = tal_count(flow->path); + struct amount_msat spend = flow->amount; + double prob = 1.0; + + for (int i = (int)pathlen - 1; i >= 0; i--) { + const struct half_chan *h = flow_edge(flow, i); + const struct chan_extra_half *eh = get_chan_extra_half_by_chan( + gossmap, chan_extra_map, flow->path[i], flow->dirs[i]); + + prob *= edge_probability(eh->known_min, eh->known_max, + eh->htlc_total, spend); + + if (prob < 0) + goto function_fail; + if (!amount_msat_add_fee(&spend, h->base_fee, + h->proportional_fee)) + goto function_fail; + } + + return prob; + +function_fail: + return -1.; +} + +u64 flow_delay(const struct flow *flow) +{ + u64 delay = 0; + for (size_t i = 0; i < tal_count(flow->path); i++) + delay += flow_edge(flow, i)->delay; + return delay; +} + +u64 flows_worst_delay(struct flow **flows) +{ + u64 maxdelay = 0; + for (size_t i = 0; i < tal_count(flows); i++) { + u64 delay = flow_delay(flows[i]); + if (delay > maxdelay) + maxdelay = delay; + } + return maxdelay; +} + +#ifndef SUPERVERBOSE_ENABLED +#undef SUPERVERBOSE +#endif diff --git a/plugins/askrene/flow.h b/plugins/askrene/flow.h new file mode 100644 index 000000000000..7c90e327bc19 --- /dev/null +++ b/plugins/askrene/flow.h @@ -0,0 +1,99 @@ +#ifndef LIGHTNING_PLUGINS_ASKRENE_FLOW_H +#define LIGHTNING_PLUGINS_ASKRENE_FLOW_H +#include "config.h" +#include +#include +#include +#include + +/* An actual partial flow. */ +struct flow { + const struct gossmap_chan **path; + /* The directions to traverse. */ + int *dirs; + /* Amounts for this flow (fees mean this shrinks across path). */ + double success_prob; + struct amount_msat amount; +}; + +const char *fmt_flows(const tal_t *ctx, const struct gossmap *gossmap, + struct chan_extra_map *chan_extra_map, + struct flow **flows); + +/* Helper to access the half chan at flow index idx */ +const struct half_chan *flow_edge(const struct flow *flow, size_t idx); + +/* A big number, meaning "don't bother" (not infinite, since you may add) */ +#define FLOW_INF_COST 100000000.0 + +/* Cost function to send @f msat through @c in direction @dir, + * given we already have a flow of prev_flow. */ +double flow_edge_cost(const struct gossmap *gossmap, + const struct gossmap_chan *c, int dir, + const struct amount_msat known_min, + const struct amount_msat known_max, + struct amount_msat prev_flow, + struct amount_msat f, + double mu, + double basefee_penalty, + double delay_riskfactor); + +/* Compute the prob. of success of a set of concurrent set of flows. */ +double flowset_probability(const tal_t *ctx, struct flow **flows, + const struct gossmap *const gossmap, + struct chan_extra_map *chan_extra_map, char **fail); + +/* How much do we need to send to make this flow arrive. */ +bool flow_spend(struct amount_msat *ret, struct flow *flow); + +/* How much do we pay in fees to make this flow arrive. */ +bool flow_fee(struct amount_msat *ret, struct flow *flow); + +bool flowset_fee(struct amount_msat *fee, struct flow **flows); + +bool flowset_delivers(struct amount_msat *delivers, struct flow **flows); + +static inline struct amount_msat flow_delivers(const struct flow *flow) +{ + return flow->amount; +} + +struct amount_msat *tal_flow_amounts(const tal_t *ctx, const struct flow *flow); + +/* FIXME: remove */ +enum askrene_errorcode { + ASKRENE_NOERROR = 0, + + ASKRENE_AMOUNT_OVERFLOW, + ASKRENE_CHANNEL_NOT_FOUND, + ASKRENE_BAD_CHANNEL, + ASKRENE_BAD_ALLOCATION, + ASKRENE_PRECONDITION_ERROR, + ASKRENE_UNEXPECTED, +}; + +enum askrene_errorcode +flow_maximum_deliverable(struct amount_msat *max_deliverable, + const struct flow *flow, + const struct gossmap *gossmap, + struct chan_extra_map *chan_extra_map, + const struct gossmap_chan **bad_channel); + +/* Assign the delivered amount to the flow if it fits + the path maximum capacity. */ +bool flow_assign_delivery(struct flow *flow, const struct gossmap *gossmap, + struct chan_extra_map *chan_extra_map, + struct amount_msat requested_amount); + +double flow_probability(struct flow *flow, const struct gossmap *gossmap, + struct chan_extra_map *chan_extra_map); + +u64 flow_delay(const struct flow *flow); +u64 flows_worst_delay(struct flow **flows); + +struct flow ** +flows_ensure_liquidity_constraints(const tal_t *ctx, struct flow **flows TAKES, + const struct gossmap *gossmap, + struct chan_extra_map *chan_extra_map); + +#endif /* LIGHTNING_PLUGINS_ASKRENE_FLOW_H */ diff --git a/plugins/askrene/mcf.c b/plugins/askrene/mcf.c index da0568a752a2..1755378ae330 100644 --- a/plugins/askrene/mcf.c +++ b/plugins/askrene/mcf.c @@ -8,8 +8,8 @@ #include #include #include -#include -#include +#include +#include #include /* # Optimal payments From 19e3495cab28399254ec0d2d6def7371d7dc2ac5 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Wed, 7 Aug 2024 11:19:54 +0930 Subject: [PATCH 17/28] askrene/flow: don't omit initial hop in flow_spend. That will be done in the caller, not here. Signed-off-by: Rusty Russell --- plugins/askrene/flow.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/askrene/flow.c b/plugins/askrene/flow.c index 2f67fb3bebbc..343d15de9d3d 100644 --- a/plugins/askrene/flow.c +++ b/plugins/askrene/flow.c @@ -320,8 +320,8 @@ bool flow_spend(struct amount_msat *ret, struct flow *flow) const size_t pathlen = tal_count(flow->path); struct amount_msat spend = flow->amount; - for (int i = (int)pathlen - 2; i >= 0; i--) { - const struct half_chan *h = flow_edge(flow, i + 1); + for (int i = (int)pathlen - 1; i >= 0; i--) { + const struct half_chan *h = flow_edge(flow, i); if (!amount_msat_add_fee(&spend, h->base_fee, h->proportional_fee)) goto function_fail; From a28759b784d4cfb412ce181ef57708aad95c4b96 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Wed, 7 Aug 2024 11:19:54 +0930 Subject: [PATCH 18/28] askrene: remove code which tries to handle tal failures. tal does not fail: the default handler (which we use) aborts. Signed-off-by: Rusty Russell --- plugins/askrene/mcf.c | 89 ------------------------------------------- 1 file changed, 89 deletions(-) diff --git a/plugins/askrene/mcf.c b/plugins/askrene/mcf.c index 1755378ae330..12af73fc517a 100644 --- a/plugins/askrene/mcf.c +++ b/plugins/askrene/mcf.c @@ -520,22 +520,13 @@ alloc_residual_network(const tal_t *ctx, const size_t max_num_nodes, { struct residual_network *residual_network = tal(ctx, struct residual_network); - if (!residual_network) - goto function_fail; residual_network->cap = tal_arrz(residual_network, s64, max_num_arcs); residual_network->cost = tal_arrz(residual_network, s64, max_num_arcs); residual_network->potential = tal_arrz(residual_network, s64, max_num_nodes); - if (!residual_network->cap || !residual_network->cost || - !residual_network->potential) { - goto function_fail; - } return residual_network; - - function_fail: - return tal_free(residual_network); } static void init_residual_network( @@ -635,14 +626,7 @@ static struct linear_network * init_linear_network(const tal_t *ctx, const struct pay_parameters *params, char **fail) { - tal_t *this_ctx = tal(ctx,tal_t); - struct linear_network * linear_network = tal(ctx, struct linear_network); - if (!linear_network) { - if (fail) - *fail = tal_fmt(ctx, "bad allocation of linear_network"); - goto function_fail; - } const size_t max_num_chans = gossmap_max_chan_idx(params->gossmap); const size_t max_num_arcs = max_num_chans * ARCS_PER_CHANNEL; @@ -652,62 +636,26 @@ init_linear_network(const tal_t *ctx, const struct pay_parameters *params, linear_network->max_num_nodes = max_num_nodes; linear_network->arc_tail_node = tal_arr(linear_network,u32,max_num_arcs); - if(!linear_network->arc_tail_node) - { - if (fail) - *fail = tal_fmt(ctx, "bad allocation of arc_tail_node"); - goto function_fail; - } for(size_t i=0;iarc_tail_node);++i) linear_network->arc_tail_node[i]=INVALID_INDEX; linear_network->node_adjacency_next_arc = tal_arr(linear_network,struct arc,max_num_arcs); - if(!linear_network->node_adjacency_next_arc) - { - if (fail) - *fail = tal_fmt(ctx, "bad allocation of node_adjacency_next_arc"); - goto function_fail; - } for(size_t i=0;inode_adjacency_next_arc);++i) linear_network->node_adjacency_next_arc[i].idx=INVALID_INDEX; linear_network->node_adjacency_first_arc = tal_arr(linear_network,struct arc,max_num_nodes); - if(!linear_network->node_adjacency_first_arc) - { - if (fail) - *fail = tal_fmt(ctx, "bad allocation of node_adjacency_first_arc"); - goto function_fail; - } for(size_t i=0;inode_adjacency_first_arc);++i) linear_network->node_adjacency_first_arc[i].idx=INVALID_INDEX; linear_network->arc_prob_cost = tal_arr(linear_network,s64,max_num_arcs); - if(!linear_network->arc_prob_cost) - { - if (fail) - *fail = tal_fmt(ctx, "bad allocation of arc_prob_cost"); - goto function_fail; - } for(size_t i=0;iarc_prob_cost);++i) linear_network->arc_prob_cost[i]=INFINITE; linear_network->arc_fee_cost = tal_arr(linear_network,s64,max_num_arcs); - if(!linear_network->arc_fee_cost) - { - if (fail) - *fail = tal_fmt(ctx, "bad allocation of arc_fee_cost"); - goto function_fail; - } for(size_t i=0;iarc_fee_cost);++i) linear_network->arc_fee_cost[i]=INFINITE; linear_network->capacity = tal_arrz(linear_network,s64,max_num_arcs); - if(!linear_network->capacity) - { - if (fail) - *fail = tal_fmt(ctx, "bad allocation of capacity"); - goto function_fail; - } for(struct gossmap_node *node = gossmap_first_node(params->gossmap); node; @@ -782,11 +730,9 @@ init_linear_network(const tal_t *ctx, const struct pay_parameters *params, } } - tal_free(this_ctx); return linear_network; function_fail: - tal_free(this_ctx); return tal_free(linear_network); } @@ -944,12 +890,6 @@ static bool find_feasible_flow(const tal_t *ctx, /* path information * prev: is the id of the arc that lead to the node. */ struct arc *prev = tal_arr(this_ctx,struct arc,linear_network->max_num_nodes); - if(!prev) - { - if(fail) - *fail = tal_fmt(ctx, "bad allocation of prev"); - goto function_fail; - } while(amount>0) { @@ -999,15 +939,6 @@ static bool find_optimal_path(const tal_t *ctx, struct dijkstra *dijkstra, bitmap *visited = tal_arrz(this_ctx, bitmap, BITMAP_NWORDS(linear_network->max_num_nodes)); - - if(!visited) - { - if(fail) - *fail = tal_fmt(ctx, "bad allocation of visited"); - goto finish; - } - - for(size_t i=0;imax_num_nodes, linear_network->max_num_arcs); - if (!residual_network) { - if (fail) - *fail = tal_fmt( - ctx, "failed to allocate the residual network"); - goto function_fail; - } - dijkstra = dijkstra_new(this_ctx, gossmap_max_node_idx(params->gossmap)); const u32 target_idx = gossmap_node_idx(params->gossmap,target); From 2c92d23bd73d9a569c5b7117cecb0cf802f1c501 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Wed, 7 Aug 2024 11:19:54 +0930 Subject: [PATCH 19/28] askrene: simply fail if a flow amount exceeds 64 bits. Rather than handling failure, simply report and exit the plugin. Simplifies error handling. Signed-off-by: Rusty Russell --- plugins/askrene/flow.c | 132 +++++++++++++++++------------------------ plugins/askrene/flow.h | 17 ++++-- plugins/askrene/mcf.c | 8 +-- 3 files changed, 69 insertions(+), 88 deletions(-) diff --git a/plugins/askrene/flow.c b/plugins/askrene/flow.c index 343d15de9d3d..6e95569c6130 100644 --- a/plugins/askrene/flow.c +++ b/plugins/askrene/flow.c @@ -14,7 +14,9 @@ #define SUPERVERBOSE_ENABLED 1 #endif -struct amount_msat *tal_flow_amounts(const tal_t *ctx, const struct flow *flow) +struct amount_msat *tal_flow_amounts(const tal_t *ctx, + struct plugin *plugin, + const struct flow *flow) { const size_t pathlen = tal_count(flow->path); struct amount_msat *amounts = tal_arr(ctx, struct amount_msat, pathlen); @@ -24,14 +26,15 @@ struct amount_msat *tal_flow_amounts(const tal_t *ctx, const struct flow *flow) const struct half_chan *h = flow_edge(flow, i + 1); amounts[i] = amounts[i + 1]; if (!amount_msat_add_fee(&amounts[i], h->base_fee, - h->proportional_fee)) - goto function_fail; + h->proportional_fee)) { + plugin_err(plugin, "Could not add fee %u/%u to amount %s in %i/%zu", + h->base_fee, h->proportional_fee, + fmt_amount_msat(tmpctx, &amounts[i+1]), + i, pathlen); + } } return amounts; - -function_fail: - return tal_free(amounts); } const char *fmt_flows(const tal_t *ctx, const struct gossmap *gossmap, @@ -168,39 +171,21 @@ flow_maximum_deliverable(struct amount_msat *max_deliverable, // } /* How much do we deliver to destination using this set of routes */ -bool flowset_delivers(struct amount_msat *delivers, struct flow **flows) +struct amount_msat flowset_delivers(struct plugin *plugin, + struct flow **flows) { struct amount_msat final = AMOUNT_MSAT(0); for (size_t i = 0; i < tal_count(flows); i++) { - if (!amount_msat_add(&final, flows[i]->amount, final)) - return false; + if (!amount_msat_add(&final, flows[i]->amount, final)) { + plugin_err(plugin, "Could not add flowsat %s to %s (%zu/%zu)", + fmt_amount_msat(tmpctx, flows[i]->amount), + fmt_amount_msat(tmpctx, final), + i, tal_count(flows)); + } } - *delivers = final; - return true; + return final; } -/* Checks if the flows satisfy the liquidity bounds imposed by the known maximum - * liquidity and pending HTLCs. - * - * FIXME The function returns false even in the case of failure. The caller has - * no way of knowing the difference between a failure of evaluation and a - * negative answer. */ -// static bool check_liquidity_bounds(struct flow **flows, -// const struct gossmap *gossmap, -// struct chan_extra_map *chan_extra_map) -// { -// bool check = true; -// for (size_t i = 0; i < tal_count(flows); ++i) { -// struct amount_msat max_deliverable; -// if (!flow_maximum_deliverable(&max_deliverable, flows[i], -// gossmap, chan_extra_map)) -// return false; -// struct amount_msat delivers = flow_delivers(flows[i]); -// check &= amount_msat_less_eq(delivers, max_deliverable); -// } -// return check; -// } - /* Compute the prob. of success of a set of concurrent set of flows. * * IMPORTANT: this is not simply the multiplication of the prob. of success of @@ -226,7 +211,9 @@ struct chan_inflight_flow // TODO(eduardo): here chan_extra_map should be const // TODO(eduardo): here flows should be const -double flowset_probability(const tal_t *ctx, struct flow **flows, +double flowset_probability(const tal_t *ctx, + struct plugin *plugin, + struct flow **flows, const struct gossmap *const gossmap, struct chan_extra_map *chan_extra_map, char **fail) { @@ -250,14 +237,6 @@ double flowset_probability(const tal_t *ctx, struct flow **flows, const struct flow *f = flows[i]; const size_t pathlen = tal_count(f->path); struct amount_msat *amounts = tal_flow_amounts(this_ctx, f); - if (!amounts) - { - if (fail) - *fail = tal_fmt( - ctx, - "failed to compute amounts along the path"); - goto function_fail; - } for (size_t j = 0; j < pathlen; ++j) { const struct chan_extra_half *h = @@ -313,59 +292,50 @@ double flowset_probability(const tal_t *ctx, struct flow **flows, return -1; } -bool flow_spend(struct amount_msat *ret, struct flow *flow) +struct amount_msat flow_spend(struct plugin *plugin, const struct flow *flow) { - assert(ret); - assert(flow); const size_t pathlen = tal_count(flow->path); struct amount_msat spend = flow->amount; for (int i = (int)pathlen - 1; i >= 0; i--) { const struct half_chan *h = flow_edge(flow, i); if (!amount_msat_add_fee(&spend, h->base_fee, - h->proportional_fee)) - goto function_fail; + h->proportional_fee)) { + plugin_err(plugin, "Could not add fee %u/%u to amount %s in %i/%zu", + h->base_fee, h->proportional_fee, + fmt_amount_msat(tmpctx, &amounts[i]), + i, pathlen); + } } - *ret = spend; - return true; - -function_fail: - return false; + return spend; } -bool flow_fee(struct amount_msat *ret, struct flow *flow) +struct amount_msat flow_fee(struct plugin *plugin, const struct flow *flow) { - assert(ret); - assert(flow); + struct amount_msat spend = flow_spend(plugin, flow); struct amount_msat fee; - struct amount_msat spend; - if (!flow_spend(&spend, flow)) - goto function_fail; - if (!amount_msat_sub(&fee, spend, flow->amount)) - goto function_fail; - - *ret = fee; - return true; + if (!amount_msat_sub(&fee, spend, flow->amount)) { + plugin_err(plugin, "Could not subtract %s from %s for fee", + fmt_amount_msat(tmpctx, flow->amount), + fmt_amount_msat(tmpctx, spend)); + } -function_fail: - return false; + return fee; } -bool flowset_fee(struct amount_msat *ret, struct flow **flows) +struct amount_msat flowset_fee(struct plugin *plugin, struct flow **flows) { - assert(ret); - assert(flows); struct amount_msat fee = AMOUNT_MSAT(0); for (size_t i = 0; i < tal_count(flows); i++) { - struct amount_msat this_fee; - if (!flow_fee(&this_fee, flows[i])) - return false; - if (!amount_msat_add(&fee, this_fee, fee)) - return false; + struct amount_msat this_fee = flow_fee(plugin, flows[i]); + if (!amount_msat_add(&fee, this_fee, fee)) { + plugin_err(plugin, "Could not add %s to %s for flowset fee", + fmt_amount_msat(tmpctx, this_fee), + fmt_amount_msat(tmpctx, fee)); + } } - *ret = fee; - return true; + return fee; } /* Helper to access the half chan at flow index idx */ @@ -396,7 +366,9 @@ bool flow_assign_delivery(struct flow *flow, const struct gossmap *gossmap, * IMPORTANT: flow->success_prob is misleading, because that's the prob. of * success provided that there are no other flows in the current MPP flow set. * */ -double flow_probability(struct flow *flow, const struct gossmap *gossmap, +double flow_probability(const struct flow *flow, + struct plugin *plugin, + const struct gossmap *gossmap, struct chan_extra_map *chan_extra_map) { assert(flow); @@ -417,8 +389,12 @@ double flow_probability(struct flow *flow, const struct gossmap *gossmap, if (prob < 0) goto function_fail; if (!amount_msat_add_fee(&spend, h->base_fee, - h->proportional_fee)) - goto function_fail; + h->proportional_fee)) { + plugin_err(plugin, "Could not add fee %u/%u to amount %s in %i/%zu", + h->base_fee, h->proportional_fee, + fmt_amount_msat(tmpctx, spend), + i, pathlen); + } } return prob; diff --git a/plugins/askrene/flow.h b/plugins/askrene/flow.h index 7c90e327bc19..7d7b550fce66 100644 --- a/plugins/askrene/flow.h +++ b/plugins/askrene/flow.h @@ -44,21 +44,24 @@ double flowset_probability(const tal_t *ctx, struct flow **flows, struct chan_extra_map *chan_extra_map, char **fail); /* How much do we need to send to make this flow arrive. */ -bool flow_spend(struct amount_msat *ret, struct flow *flow); +struct amount_msat flow_spend(struct plugin *plugin, const struct flow *flow); /* How much do we pay in fees to make this flow arrive. */ -bool flow_fee(struct amount_msat *ret, struct flow *flow); +struct amount_msat flow_fee(struct plugin *plugin, const struct flow *flow); -bool flowset_fee(struct amount_msat *fee, struct flow **flows); +struct amount_msat flowset_fee(struct plugin *plugin, struct flow **flows); -bool flowset_delivers(struct amount_msat *delivers, struct flow **flows); +struct amount_msat flowset_delivers(struct plugin *plugin, + struct flow **flows); static inline struct amount_msat flow_delivers(const struct flow *flow) { return flow->amount; } -struct amount_msat *tal_flow_amounts(const tal_t *ctx, const struct flow *flow); +struct amount_msat *tal_flow_amounts(const tal_t *ctx, + struct plugin *plugin, + const struct flow *flow); /* FIXME: remove */ enum askrene_errorcode { @@ -85,7 +88,9 @@ bool flow_assign_delivery(struct flow *flow, const struct gossmap *gossmap, struct chan_extra_map *chan_extra_map, struct amount_msat requested_amount); -double flow_probability(struct flow *flow, const struct gossmap *gossmap, +double flow_probability(const struct flow *flow, + struct plugin *plugin, + const struct gossmap *gossmap, struct chan_extra_map *chan_extra_map); u64 flow_delay(const struct flow *flow); diff --git a/plugins/askrene/mcf.c b/plugins/askrene/mcf.c index 12af73fc517a..c495a7270fbe 100644 --- a/plugins/askrene/mcf.c +++ b/plugins/askrene/mcf.c @@ -1184,6 +1184,7 @@ static inline uint64_t pseudorand_interval(uint64_t a, uint64_t b) * gossmap that corresponds to this flow. */ static struct flow ** get_flow_paths(const tal_t *ctx, const struct gossmap *gossmap, + struct plugin *plugin, const bitmap *disabled, // chan_extra_map cannot be const because we use it to keep @@ -1354,10 +1355,9 @@ get_flow_paths(const tal_t *ctx, const struct gossmap *gossmap, // accuracy struct amount_msat delivered = amount_msat(delta*1000); if (!amount_msat_sub(&delivered, delivered, excess)) { - if (fail) - *fail = tal_fmt( - ctx, "unable to substract excess"); - goto function_fail; + plugin_err(plugin, "Unable to subtract excess %s from %s", + fmt_amount_msat(excess), + fmt_amount_msat(delivered)); } excess = amount_msat(0); fp->amount = delivered; From 86f5dc5a68b781870dddb3154a25f429eae27e2e Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Wed, 7 Aug 2024 11:19:54 +0930 Subject: [PATCH 20/28] askrene: make the flow.[ch] files compile. This adapts them to their new locations, and copies a few more routines. Signed-off-by: Rusty Russell --- plugins/askrene/flow.c | 326 +++++++++++++++++++++++------------------ plugins/askrene/flow.h | 38 ++--- 2 files changed, 203 insertions(+), 161 deletions(-) diff --git a/plugins/askrene/flow.c b/plugins/askrene/flow.c index 6e95569c6130..a22180540f08 100644 --- a/plugins/askrene/flow.c +++ b/plugins/askrene/flow.c @@ -5,7 +5,9 @@ #include #include #include +#include #include +#include #include #ifndef SUPERVERBOSE @@ -14,6 +16,101 @@ #define SUPERVERBOSE_ENABLED 1 #endif +/* Checks BOLT 7 HTLC fee condition: + * recv >= base_fee + (send*proportional_fee)/1000000 */ +static bool check_fee_inequality(struct amount_msat recv, struct amount_msat send, + u64 base_fee, u64 proportional_fee) +{ + // nothing to forward, any incoming amount is good + if (amount_msat_zero(send)) + return true; + // FIXME If this addition fails we return false. The caller will not be + // able to know that there was an addition overflow, he will just assume + // that the fee inequality was not satisfied. + if (!amount_msat_add_fee(&send, base_fee, proportional_fee)) + return false; + return amount_msat_greater_eq(recv, send); +} + +/* Let `recv` be the maximum amount this channel can receive, this function + * computes the maximum amount this channel can forward `send`. + * From BOLT7 specification wee need to satisfy the following inequality: + * + * recv-send >= base_fee + floor(send*proportional_fee/1000000) + * + * That is equivalent to have + * + * send <= Bound(recv,send) + * + * where + * + * Bound(recv, send) = ((recv - base_fee)*1000000 + (send*proportional_fee) + *% 1000000)/(proportional_fee+1000000) + * + * However the quantity we want to determine, `send`, appears on both sides of + * the equation. However the term `send*proportional_fee) % 1000000` only + * contributes by increasing the bound by at most one so that we can neglect + * the extra term and use instead + * + * Bound_simple(recv) = ((recv - + *base_fee)*1000000)/(proportional_fee+1000000) + * + * as the upper bound for `send`. Formally one can check that + * + * Bound_simple(recv) <= Bound(recv, send) < Bound_simple(recv) + 2 + * + * So that if one wishes to find the very highest value of `send` that + * satisfies + * + * send <= Bound(recv, send) + * + * it is enough to compute + * + * send = Bound_simple(recv) + * + * which already satisfies the fee equation and then try to go higher + * with send+1, send+2, etc. But we know that it is enough to try up to + * send+1 because Bound(recv, send) < Bound_simple(recv) + 2. + * */ +static struct amount_msat channel_maximum_forward(const struct gossmap_chan *chan, + const int dir, + struct amount_msat recv) +{ + const u64 b = chan->half[dir].base_fee, + p = chan->half[dir].proportional_fee; + + const u64 one_million = 1000000; + u64 x_msat = + recv.millisatoshis; /* Raw: need to invert the fee equation */ + + // special case, when recv - base_fee <= 0, we cannot forward anything + if (x_msat <= b) + return AMOUNT_MSAT(0); + + x_msat -= b; + + /* recv must be a real number of msat... */ + assert(!mul_overflows_u64(one_million, x_msat)); + + struct amount_msat best_send = + AMOUNT_MSAT_INIT((one_million * x_msat) / (one_million + p)); + + /* Try to increase the value we send (up tp the last millisat) until we + * fail to fulfill the fee inequality. It takes only one iteration + * though. */ + for (size_t i = 0; i < 10; ++i) { + struct amount_msat next_send; + if (!amount_msat_add(&next_send, best_send, amount_msat(1))) + abort(); + + if (check_fee_inequality(recv, next_send, b, p)) + best_send = next_send; + else + break; + } + return best_send; +} + struct amount_msat *tal_flow_amounts(const tal_t *ctx, struct plugin *plugin, const struct flow *flow) @@ -29,7 +126,7 @@ struct amount_msat *tal_flow_amounts(const tal_t *ctx, h->proportional_fee)) { plugin_err(plugin, "Could not add fee %u/%u to amount %s in %i/%zu", h->base_fee, h->proportional_fee, - fmt_amount_msat(tmpctx, &amounts[i+1]), + fmt_amount_msat(tmpctx, amounts[i+1]), i, pathlen); } } @@ -37,13 +134,11 @@ struct amount_msat *tal_flow_amounts(const tal_t *ctx, return amounts; } -const char *fmt_flows(const tal_t *ctx, const struct gossmap *gossmap, - struct chan_extra_map *chan_extra_map, +const char *fmt_flows(const tal_t *ctx, const struct route_query *rq, struct flow **flows) { tal_t *this_ctx = tal(ctx, tal_t); - double tot_prob = - flowset_probability(tmpctx, flows, gossmap, chan_extra_map, NULL); + double tot_prob = flowset_probability(flows, rq); assert(tot_prob >= 0); char *buff = tal_fmt(ctx, "%zu subflows, prob %2lf\n", tal_count(flows), tot_prob); @@ -52,14 +147,12 @@ const char *fmt_flows(const tal_t *ctx, const struct gossmap *gossmap, tal_append_fmt(&buff, " "); for (size_t j = 0; j < tal_count(flows[i]->path); j++) { struct short_channel_id scid = - gossmap_chan_scid(gossmap, flows[i]->path[j]); + gossmap_chan_scid(rq->gossmap, flows[i]->path[j]); tal_append_fmt(&buff, "%s%s", j ? "->" : "", fmt_short_channel_id(this_ctx, scid)); } delivered = flows[i]->amount; - if (!flow_fee(&fee, flows[i])) { - abort(); - } + fee = flow_fee(rq->plugin, flows[i]); tal_append_fmt(&buff, " prob %.2f, %s delivered with fee %s\n", flows[i]->success_prob, fmt_amount_msat(this_ctx, delivered), @@ -77,84 +170,60 @@ const char *fmt_flows(const tal_t *ctx, const struct gossmap *gossmap, * It fails if the maximum that we can * deliver at node i is smaller than the minimum required to forward the least * amount greater than zero to the next node. */ -enum askrene_errorcode +const struct gossmap_chan * flow_maximum_deliverable(struct amount_msat *max_deliverable, const struct flow *flow, - const struct gossmap *gossmap, - struct chan_extra_map *chan_extra_map, - const struct gossmap_chan **bad_channel) + const struct route_query *rq) { + struct amount_msat maxcap; + assert(tal_count(flow->path) > 0); assert(tal_count(flow->dirs) > 0); assert(tal_count(flow->path) == tal_count(flow->dirs)); - struct amount_msat x; - enum askrene_errorcode err; - - err = channel_liquidity(&x, gossmap, chan_extra_map, flow->path[0], - flow->dirs[0]); - if(err){ - if(bad_channel)*bad_channel = flow->path[0]; - return err; - } - x = amount_msat_min(x, channel_htlc_max(flow->path[0], flow->dirs[0])); - if(amount_msat_zero(x)) - { - if(bad_channel)*bad_channel = flow->path[0]; - return ASKRENE_BAD_CHANNEL; - } + get_constraints(rq, flow->path[0], flow->dirs[0], NULL, &maxcap); + maxcap = amount_msat_min(maxcap, gossmap_chan_htlc_max(flow->path[0], flow->dirs[0])); + + if (amount_msat_zero(maxcap)) + return flow->path[0]; for (size_t i = 1; i < tal_count(flow->path); ++i) { // ith node can forward up to 'liquidity_cap' because of the ith // channel liquidity bound struct amount_msat liquidity_cap; - err = channel_liquidity(&liquidity_cap, gossmap, chan_extra_map, - flow->path[i], flow->dirs[i]); - if(err) { - if(bad_channel)*bad_channel = flow->path[i]; - return err; - } + get_constraints(rq, flow->path[i], flow->dirs[i], NULL, &liquidity_cap); /* ith node can receive up to 'x', therefore he will not forward * more than 'forward_cap' that we compute below inverting the * fee equation. */ struct amount_msat forward_cap; - err = channel_maximum_forward(&forward_cap, flow->path[i], - flow->dirs[i], x); - if(err) - { - if(bad_channel)*bad_channel = flow->path[i]; - return err; - } - struct amount_msat x_new = - amount_msat_min(forward_cap, liquidity_cap); - x_new = amount_msat_min( - x_new, channel_htlc_max(flow->path[i], flow->dirs[i])); + forward_cap = channel_maximum_forward(flow->path[i], flow->dirs[i], + maxcap); + struct amount_msat new_max = amount_msat_min(forward_cap, liquidity_cap); + new_max = amount_msat_min(new_max, + gossmap_chan_htlc_max(flow->path[i], flow->dirs[i])); /* safety check: amounts decrease along the route */ - assert(amount_msat_less_eq(x_new, x)); + assert(amount_msat_less_eq(new_max, maxcap)); - if(amount_msat_zero(x_new)) - { - if(bad_channel)*bad_channel = flow->path[i]; - return ASKRENE_BAD_CHANNEL; - } + if (amount_msat_zero(new_max)) + return flow->path[i]; /* safety check: the max liquidity in the next hop + fees cannot be greater than the max liquidity in the current hop, IF the next hop is non-zero. */ - struct amount_msat x_check = x_new; + struct amount_msat check = new_max; assert( - amount_msat_add_fee(&x_check, flow_edge(flow, i)->base_fee, + amount_msat_add_fee(&check, flow_edge(flow, i)->base_fee, flow_edge(flow, i)->proportional_fee)); - assert(amount_msat_less_eq(x_check, x)); + assert(amount_msat_less_eq(check, maxcap)); - x = x_new; + maxcap = new_max; } - assert(!amount_msat_zero(x)); - *max_deliverable = x; - return ASKRENE_NOERROR; + assert(!amount_msat_zero(maxcap)); + *max_deliverable = maxcap; + return NULL; } /* Returns the smallest amount we can send so that the destination can get one @@ -186,6 +255,35 @@ struct amount_msat flowset_delivers(struct plugin *plugin, return final; } +static double edge_probability(struct amount_msat sent, + struct amount_msat mincap, + struct amount_msat maxcap, + struct amount_msat used) +{ + struct amount_msat numerator, denominator; + + if (!amount_msat_sub(&mincap, mincap, used)) + mincap = AMOUNT_MSAT(0); + if (!amount_msat_sub(&maxcap, maxcap, used)) + maxcap = AMOUNT_MSAT(0); + + if (amount_msat_less_eq(sent, mincap)) + return 1.0; + else if (amount_msat_greater(sent, maxcap)) + return 0.0; + + /* Linear probability: 1 - (spend - min) / (max - min) */ + + /* spend > mincap, from above. */ + if (!amount_msat_sub(&numerator, sent, mincap)) + abort(); + /* This can only fail is maxcap was < mincap, + * so we would be captured above */ + if (!amount_msat_sub(&denominator, maxcap, mincap)) + abort(); + return 1.0 - amount_msat_ratio(numerator, denominator); +} + /* Compute the prob. of success of a set of concurrent set of flows. * * IMPORTANT: this is not simply the multiplication of the prob. of success of @@ -209,87 +307,45 @@ struct chan_inflight_flow struct amount_msat half[2]; }; -// TODO(eduardo): here chan_extra_map should be const -// TODO(eduardo): here flows should be const -double flowset_probability(const tal_t *ctx, - struct plugin *plugin, - struct flow **flows, - const struct gossmap *const gossmap, - struct chan_extra_map *chan_extra_map, char **fail) +double flowset_probability(struct flow **flows, + const struct route_query *rq) { - assert(flows); - assert(gossmap); - assert(chan_extra_map); - tal_t *this_ctx = tal(ctx, tal_t); + tal_t *this_ctx = tal(tmpctx, tal_t); double prob = 1.0; // TODO(eduardo): should it be better to use a map instead of an array // here? - const size_t max_num_chans = gossmap_max_chan_idx(gossmap); + const size_t max_num_chans = gossmap_max_chan_idx(rq->gossmap); struct chan_inflight_flow *in_flight = - tal_arr(this_ctx, struct chan_inflight_flow, max_num_chans); - - for (size_t i = 0; i < max_num_chans; ++i) { - in_flight[i].half[0] = in_flight[i].half[1] = AMOUNT_MSAT(0); - } + tal_arrz(this_ctx, struct chan_inflight_flow, max_num_chans); for (size_t i = 0; i < tal_count(flows); ++i) { const struct flow *f = flows[i]; const size_t pathlen = tal_count(f->path); - struct amount_msat *amounts = tal_flow_amounts(this_ctx, f); + struct amount_msat *amounts = tal_flow_amounts(this_ctx, rq->plugin, f); for (size_t j = 0; j < pathlen; ++j) { - const struct chan_extra_half *h = - get_chan_extra_half_by_chan(gossmap, chan_extra_map, - f->path[j], f->dirs[j]); - if (!h) { - if (fail) - *fail = tal_fmt( - ctx, - "channel not found in chan_extra_map"); - goto function_fail; - } - const u32 c_idx = gossmap_chan_idx(gossmap, f->path[j]); + struct amount_msat mincap, maxcap; const int c_dir = f->dirs[j]; - + const u32 c_idx = gossmap_chan_idx(rq->gossmap, f->path[j]); const struct amount_msat deliver = amounts[j]; - struct amount_msat prev_flow; - if (!amount_msat_add(&prev_flow, h->htlc_total, - in_flight[c_idx].half[c_dir])) { - if (fail) - *fail = tal_fmt( - ctx, "in-flight amount_msat overflow"); - goto function_fail; - } + get_constraints(rq, f->path[j], c_dir, &mincap, &maxcap); - double edge_prob = - edge_probability(h->known_min, h->known_max, - prev_flow, deliver); - if (edge_prob < 0) { - if (fail) - *fail = tal_fmt(ctx, - "edge_probability failed"); - goto function_fail; - } - prob *= edge_prob; + prob *= edge_probability(deliver, mincap, maxcap, + in_flight[c_idx].half[c_dir]); if (!amount_msat_add(&in_flight[c_idx].half[c_dir], in_flight[c_idx].half[c_dir], deliver)) { - if (fail) - *fail = tal_fmt( - ctx, "in-flight amount_msat overflow"); - goto function_fail; + plugin_err(rq->plugin, "Could not add %s to inflight %s", + fmt_amount_msat(tmpctx, deliver), + fmt_amount_msat(tmpctx, in_flight[c_idx].half[c_dir])); } } } tal_free(this_ctx); return prob; - - function_fail: - tal_free(this_ctx); - return -1; } struct amount_msat flow_spend(struct plugin *plugin, const struct flow *flow) @@ -303,7 +359,7 @@ struct amount_msat flow_spend(struct plugin *plugin, const struct flow *flow) h->proportional_fee)) { plugin_err(plugin, "Could not add fee %u/%u to amount %s in %i/%zu", h->base_fee, h->proportional_fee, - fmt_amount_msat(tmpctx, &amounts[i]), + fmt_amount_msat(tmpctx, spend), i, pathlen); } } @@ -348,17 +404,20 @@ const struct half_chan *flow_edge(const struct flow *flow, size_t idx) /* Assign the delivered amount to the flow if it fits the path maximum capacity. */ -bool flow_assign_delivery(struct flow *flow, const struct gossmap *gossmap, - struct chan_extra_map *chan_extra_map, - struct amount_msat requested_amount) +const struct gossmap_chan * +flow_assign_delivery(struct flow *flow, + const struct route_query *rq, + struct amount_msat requested_amount) { - struct amount_msat max_deliverable = AMOUNT_MSAT(0); - if (flow_maximum_deliverable(&max_deliverable, flow, gossmap, - chan_extra_map, NULL)) - return false; + struct amount_msat max_deliverable; + const struct gossmap_chan *badchan; + + badchan = flow_maximum_deliverable(&max_deliverable, flow, rq); + if (badchan) + return badchan; assert(!amount_msat_zero(max_deliverable)); flow->amount = amount_msat_min(requested_amount, max_deliverable); - return true; + return NULL; } /* Helper function to find the success_prob for a single flow @@ -367,30 +426,22 @@ bool flow_assign_delivery(struct flow *flow, const struct gossmap *gossmap, * success provided that there are no other flows in the current MPP flow set. * */ double flow_probability(const struct flow *flow, - struct plugin *plugin, - const struct gossmap *gossmap, - struct chan_extra_map *chan_extra_map) + const struct route_query *rq) { - assert(flow); - assert(gossmap); - assert(chan_extra_map); const size_t pathlen = tal_count(flow->path); struct amount_msat spend = flow->amount; double prob = 1.0; for (int i = (int)pathlen - 1; i >= 0; i--) { const struct half_chan *h = flow_edge(flow, i); - const struct chan_extra_half *eh = get_chan_extra_half_by_chan( - gossmap, chan_extra_map, flow->path[i], flow->dirs[i]); + struct amount_msat mincap, maxcap; - prob *= edge_probability(eh->known_min, eh->known_max, - eh->htlc_total, spend); + get_constraints(rq, flow->path[i], flow->dirs[i], &mincap, &maxcap); + prob *= edge_probability(spend, mincap, maxcap, AMOUNT_MSAT(0)); - if (prob < 0) - goto function_fail; if (!amount_msat_add_fee(&spend, h->base_fee, h->proportional_fee)) { - plugin_err(plugin, "Could not add fee %u/%u to amount %s in %i/%zu", + plugin_err(rq->plugin, "Could not add fee %u/%u to amount %s in %i/%zu", h->base_fee, h->proportional_fee, fmt_amount_msat(tmpctx, spend), i, pathlen); @@ -398,9 +449,6 @@ double flow_probability(const struct flow *flow, } return prob; - -function_fail: - return -1.; } u64 flow_delay(const struct flow *flow) diff --git a/plugins/askrene/flow.h b/plugins/askrene/flow.h index 7d7b550fce66..68d04442f33a 100644 --- a/plugins/askrene/flow.h +++ b/plugins/askrene/flow.h @@ -2,10 +2,12 @@ #define LIGHTNING_PLUGINS_ASKRENE_FLOW_H #include "config.h" #include -#include #include #include +struct plugin; +struct route_query; + /* An actual partial flow. */ struct flow { const struct gossmap_chan **path; @@ -16,8 +18,8 @@ struct flow { struct amount_msat amount; }; -const char *fmt_flows(const tal_t *ctx, const struct gossmap *gossmap, - struct chan_extra_map *chan_extra_map, +const char *fmt_flows(const tal_t *ctx, + const struct route_query *rq, struct flow **flows); /* Helper to access the half chan at flow index idx */ @@ -39,9 +41,8 @@ double flow_edge_cost(const struct gossmap *gossmap, double delay_riskfactor); /* Compute the prob. of success of a set of concurrent set of flows. */ -double flowset_probability(const tal_t *ctx, struct flow **flows, - const struct gossmap *const gossmap, - struct chan_extra_map *chan_extra_map, char **fail); +double flowset_probability(struct flow **flows, + const struct route_query *rq); /* How much do we need to send to make this flow arrive. */ struct amount_msat flow_spend(struct plugin *plugin, const struct flow *flow); @@ -75,30 +76,23 @@ enum askrene_errorcode { ASKRENE_UNEXPECTED, }; -enum askrene_errorcode +/* Returns problematic channel, OR sets max_deliverable to non-zero amount */ +const struct gossmap_chan * flow_maximum_deliverable(struct amount_msat *max_deliverable, const struct flow *flow, - const struct gossmap *gossmap, - struct chan_extra_map *chan_extra_map, - const struct gossmap_chan **bad_channel); + const struct route_query *rq); /* Assign the delivered amount to the flow if it fits - the path maximum capacity. */ -bool flow_assign_delivery(struct flow *flow, const struct gossmap *gossmap, - struct chan_extra_map *chan_extra_map, - struct amount_msat requested_amount); + the path maximum capacity. Returns bad channel if max would be zero. */ +const struct gossmap_chan * +flow_assign_delivery(struct flow *flow, + const struct route_query *rq, + struct amount_msat requested_amount); double flow_probability(const struct flow *flow, - struct plugin *plugin, - const struct gossmap *gossmap, - struct chan_extra_map *chan_extra_map); + const struct route_query *rq); u64 flow_delay(const struct flow *flow); u64 flows_worst_delay(struct flow **flows); -struct flow ** -flows_ensure_liquidity_constraints(const tal_t *ctx, struct flow **flows TAKES, - const struct gossmap *gossmap, - struct chan_extra_map *chan_extra_map); - #endif /* LIGHTNING_PLUGINS_ASKRENE_FLOW_H */ From 10bfb884e04b8b10e4a370b743477e66ca076450 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Wed, 7 Aug 2024 11:19:55 +0930 Subject: [PATCH 21/28] askrene: include the mcf and flow routines. This make the code use askrene's "struct route_query". Signed-off-by: Rusty Russell --- plugins/askrene/Makefile | 4 +- plugins/askrene/mcf.c | 243 ++++++++++++--------------------------- plugins/askrene/mcf.h | 6 +- 3 files changed, 76 insertions(+), 177 deletions(-) diff --git a/plugins/askrene/Makefile b/plugins/askrene/Makefile index 228390068841..3deece30db6c 100644 --- a/plugins/askrene/Makefile +++ b/plugins/askrene/Makefile @@ -1,5 +1,5 @@ -PLUGIN_ASKRENE_SRC := plugins/askrene/askrene.c plugins/askrene/layer.c plugins/askrene/reserve.c -PLUGIN_ASKRENE_HEADER := plugins/askrene/askrene.h plugins/askrene/layer.h plugins/askrene/reserve.h +PLUGIN_ASKRENE_SRC := plugins/askrene/askrene.c plugins/askrene/layer.c plugins/askrene/reserve.c plugins/askrene/mcf.c plugins/askrene/dijkstra.c plugins/askrene/flow.c +PLUGIN_ASKRENE_HEADER := plugins/askrene/askrene.h plugins/askrene/layer.h plugins/askrene/reserve.h plugins/askrene/mcf.h plugins/askrene/dijkstra.h plugins/askrene/flow.h PLUGIN_ASKRENE_OBJS := $(PLUGIN_ASKRENE_SRC:.c=.o) $(PLUGIN_ASKRENE_OBJS): $(PLUGIN_ASKRENE_HEADER) diff --git a/plugins/askrene/mcf.c b/plugins/askrene/mcf.c index c495a7270fbe..53cffea0a51e 100644 --- a/plugins/askrene/mcf.c +++ b/plugins/askrene/mcf.c @@ -1,5 +1,6 @@ #include "config.h" #include +#include #include #include #include @@ -7,9 +8,11 @@ #include #include #include -#include +#include #include #include +#include +#include #include /* # Optimal payments @@ -311,17 +314,10 @@ static inline struct arc arc_from_parts(u32 chanidx, int chandir, u32 part, bool #define MIN(x, y) (((x) < (y)) ? (x) : (y)) struct pay_parameters { - /* The gossmap we are using */ - struct gossmap *gossmap; + const struct route_query *rq; const struct gossmap_node *source; const struct gossmap_node *target; - /* Extra information we intuited about the channels */ - struct chan_extra_map *chan_extra_map; - - /* Optional bitarray of disabled channels. */ - const bitmap *disabled; - // how much we pay struct amount_msat amount; @@ -329,8 +325,6 @@ struct pay_parameters { double cap_fraction[CHANNEL_PARTS], cost_fraction[CHANNEL_PARTS]; - struct amount_msat max_fee; - double min_probability; double delay_feefactor; double base_fee_penalty; u32 prob_cost_factor; @@ -442,76 +436,39 @@ static struct arc node_adjacency_next( return linear_network->node_adjacency_next_arc[arc.idx]; } -static bool channel_is_available(const struct gossmap_chan *c, int dir, - const struct gossmap *gossmap, - const bitmap *disabled) -{ - if (!gossmap_chan_set(c, dir)) - return false; - - const u32 chan_id = gossmap_chan_idx(gossmap, c); - return !bitmap_test_bit(disabled, chan_id); -} - // TODO(eduardo): unit test this /* Split a directed channel into parts with linear cost function. */ -static bool linearize_channel(const struct pay_parameters *params, +static void linearize_channel(const struct pay_parameters *params, const struct gossmap_chan *c, const int dir, s64 *capacity, s64 *cost) { - struct chan_extra_half *extra_half = get_chan_extra_half_by_chan( - params->gossmap, - params->chan_extra_map, - c, - dir); - - if (!extra_half) { - return false; - } + struct amount_msat mincap, maxcap; + + /* This takes into account any payments in progress. */ + get_constraints(params->rq, c, dir, &mincap, &maxcap); + /* Assume if min > max, max is wrong */ + if (amount_msat_greater(mincap, maxcap)) + maxcap = mincap; - assert( - amount_msat_less_eq(extra_half->htlc_total, extra_half->known_max)); - assert( - amount_msat_less_eq(extra_half->known_min, extra_half->known_max)); - - s64 h = extra_half->htlc_total.millisatoshis/1000; /* Raw: linearize_channel */ - s64 a = extra_half->known_min.millisatoshis/1000, /* Raw: linearize_channel */ - b = 1 + extra_half->known_max.millisatoshis/1000; /* Raw: linearize_channel */ - - /* If HTLCs add up to more than the known_max it means we have a - * completely wrong knowledge. */ - // assert(ha it doesn't mean automatically that our - * known_min should have been updated, because we reserve this HTLC - * after sendpay behind the scenes it might happen that sendpay failed - * because of insufficient funds we haven't noticed yet. */ - // assert(h<=a); - - /* We reduce this channel capacity because HTLC are reserving liquidity. */ - a -= h; - b -= h; - a = MAX(a,0); - b = MAX(a+1,b); + u64 a = mincap.millisatoshis/1000, /* Raw: linearize_channel */ + b = 1 + maxcap.millisatoshis/1000; /* Raw: linearize_channel */ /* An extra bound on capacity, here we use it to reduce the flow such * that it does not exceed htlcmax. */ - s64 cap_on_capacity = - channel_htlc_max(c, dir).millisatoshis/1000; /* Raw: linearize_channel */ + u64 cap_on_capacity = fp16_to_u64(c->half[dir].htlc_max) / 1000; capacity[0]=a; cost[0]=0; for(size_t i=1;icap_fraction[i]*(b-a), cap_on_capacity); + assert(cap_on_capacity >= capacity[i]); cap_on_capacity -= capacity[i]; - assert(cap_on_capacity>=0); cost[i] = params->cost_fraction[i] *params->amount.millisatoshis /* Raw: linearize_channel */ *params->prob_cost_factor*1.0/(b-a); } - return true; } static struct residual_network * @@ -623,14 +580,14 @@ static s64 linear_fee_cost( } static struct linear_network * -init_linear_network(const tal_t *ctx, const struct pay_parameters *params, - char **fail) +init_linear_network(const tal_t *ctx, const struct pay_parameters *params) { struct linear_network * linear_network = tal(ctx, struct linear_network); + const struct gossmap *gossmap = params->rq->gossmap; - const size_t max_num_chans = gossmap_max_chan_idx(params->gossmap); + const size_t max_num_chans = gossmap_max_chan_idx(gossmap); const size_t max_num_arcs = max_num_chans * ARCS_PER_CHANNEL; - const size_t max_num_nodes = gossmap_max_node_idx(params->gossmap); + const size_t max_num_nodes = gossmap_max_node_idx(gossmap); linear_network->max_num_arcs = max_num_arcs; linear_network->max_num_nodes = max_num_nodes; @@ -657,28 +614,27 @@ init_linear_network(const tal_t *ctx, const struct pay_parameters *params, linear_network->capacity = tal_arrz(linear_network,s64,max_num_arcs); - for(struct gossmap_node *node = gossmap_first_node(params->gossmap); + for(struct gossmap_node *node = gossmap_first_node(gossmap); node; - node=gossmap_next_node(params->gossmap,node)) + node=gossmap_next_node(gossmap,node)) { - const u32 node_id = gossmap_node_idx(params->gossmap,node); + const u32 node_id = gossmap_node_idx(gossmap,node); for(size_t j=0;jnum_chans;++j) { int half; - const struct gossmap_chan *c = gossmap_nth_chan(params->gossmap, + const struct gossmap_chan *c = gossmap_nth_chan(gossmap, node, j, &half); - if (!channel_is_available(c, half, params->gossmap, - params->disabled)) + if (!gossmap_chan_set(c, half)) continue; - const u32 chan_id = gossmap_chan_idx(params->gossmap, c); + const u32 chan_id = gossmap_chan_idx(gossmap, c); - const struct gossmap_node *next = gossmap_nth_node(params->gossmap, + const struct gossmap_node *next = gossmap_nth_node(gossmap, c,!half); - const u32 next_id = gossmap_node_idx(params->gossmap,next); + const u32 next_id = gossmap_node_idx(gossmap,next); if(node_id==next_id) continue; @@ -689,13 +645,7 @@ init_linear_network(const tal_t *ctx, const struct pay_parameters *params, // split this channel direction to obtain the arcs // that are outgoing to `node` - if (!linearize_channel(params, c, half, - capacity, prob_cost)) { - if(fail) - *fail = - tal_fmt(ctx, "linearize_channel failed"); - goto function_fail; - } + linearize_channel(params, c, half, capacity, prob_cost); const s64 fee_cost = linear_fee_cost(c,half, params->base_fee_penalty, @@ -731,9 +681,6 @@ init_linear_network(const tal_t *ctx, const struct pay_parameters *params, } return linear_network; - - function_fail: - return tal_free(linear_network); } /* Simple queue to traverse the network. */ @@ -749,12 +696,11 @@ struct queue_data * The path is encoded into prev, which contains the idx of the arcs that are * traversed. */ static bool -find_admissible_path(const tal_t *ctx, - const struct linear_network *linear_network, +find_admissible_path(const struct linear_network *linear_network, const struct residual_network *residual_network, const u32 source, const u32 target, struct arc *prev) { - tal_t *this_ctx = tal(ctx,tal_t); + tal_t *this_ctx = tal(tmpctx,tal_t); bool target_found = false; @@ -881,8 +827,7 @@ static void augment_flow( static bool find_feasible_flow(const tal_t *ctx, const struct linear_network *linear_network, struct residual_network *residual_network, - const u32 source, const u32 target, s64 amount, - char **fail) + const u32 source, const u32 target, s64 amount) { assert(amount>=0); tal_t *this_ctx = tal(ctx,tal_t); @@ -894,13 +839,9 @@ static bool find_feasible_flow(const tal_t *ctx, while(amount>0) { // find a path from source to target - if (!find_admissible_path(this_ctx, linear_network, + if (!find_admissible_path(linear_network, residual_network, source, target, - prev)) - - { - if(fail) - *fail = tal_fmt(ctx, "find_admissible_path failed"); + prev)) { goto function_fail; } @@ -928,13 +869,13 @@ static bool find_feasible_flow(const tal_t *ctx, // TODO(eduardo): unit test this /* Similar to `find_admissible_path` but use Dijkstra to optimize the distance * label. Stops when the target is hit. */ -static bool find_optimal_path(const tal_t *ctx, struct dijkstra *dijkstra, +static bool find_optimal_path(struct dijkstra *dijkstra, const struct linear_network *linear_network, const struct residual_network *residual_network, const u32 source, const u32 target, - struct arc *prev, char **fail) + struct arc *prev) { - tal_t *this_ctx = tal(ctx,tal_t); + tal_t *this_ctx = tal(tmpctx,tal_t); bool target_found = false; bitmap *visited = tal_arrz(this_ctx, bitmap, @@ -988,10 +929,6 @@ static bool find_optimal_path(const tal_t *ctx, struct dijkstra *dijkstra, } } - if (!target_found && fail) - *fail = tal_fmt(ctx, "no route to destination"); - - finish: tal_free(this_ctx); return target_found; } @@ -1027,15 +964,13 @@ static void zero_flow( * each step, we might use the previous flow result, which is not optimal in the * current iteration but I might be not too far from the truth. * It comes to mind to use cycle cancelling. */ -static bool optimize_mcf(const tal_t *ctx, struct dijkstra *dijkstra, +static bool optimize_mcf(struct dijkstra *dijkstra, const struct linear_network *linear_network, struct residual_network *residual_network, - const u32 source, const u32 target, const s64 amount, - char **fail) + const u32 source, const u32 target, const s64 amount) { assert(amount>=0); - tal_t *this_ctx = tal(ctx,tal_t); - char *errmsg; + tal_t *this_ctx = tal(tmpctx,tal_t); zero_flow(linear_network,residual_network); struct arc *prev = tal_arr(this_ctx,struct arc,linear_network->max_num_nodes); @@ -1046,13 +981,8 @@ static bool optimize_mcf(const tal_t *ctx, struct dijkstra *dijkstra, while(remaining_amount>0) { - if (!find_optimal_path(this_ctx, dijkstra, linear_network, - residual_network, source, target, prev, - &errmsg)) { - if (fail) - *fail = - tal_fmt(ctx, "find_optimal_path failed: %s", - errmsg); + if (!find_optimal_path(dijkstra, linear_network, + residual_network, source, target, prev)) { goto function_fail; } @@ -1103,7 +1033,6 @@ struct chan_flow * positive balance. */ static u32 find_positive_balance( const struct gossmap *gossmap, - const bitmap *disabled, const struct chan_flow *chan_flow, const u32 start_idx, const s64 *balance, @@ -1136,7 +1065,7 @@ static u32 find_positive_balance( = gossmap_nth_chan(gossmap, cur,i,&dir); - if (!channel_is_available(c, dir, gossmap, disabled)) + if (!gossmap_chan_set(c, dir)) continue; const u32 c_idx = gossmap_chan_idx(gossmap,c); @@ -1183,32 +1112,24 @@ static inline uint64_t pseudorand_interval(uint64_t a, uint64_t b) /* Given a flow in the residual network, build a set of payment flows in the * gossmap that corresponds to this flow. */ static struct flow ** -get_flow_paths(const tal_t *ctx, const struct gossmap *gossmap, - struct plugin *plugin, - const bitmap *disabled, - - // chan_extra_map cannot be const because we use it to keep - // track of htlcs and in_flight sats. - struct chan_extra_map *chan_extra_map, +get_flow_paths(const tal_t *ctx, + const struct route_query *rq, const struct linear_network *linear_network, const struct residual_network *residual_network, // how many msats in excess we paid for not having msat accuracy // in the MCF solver - struct amount_msat excess, - - // error message - char **fail) + struct amount_msat excess) { tal_t *this_ctx = tal(ctx,tal_t); struct flow **flows = tal_arr(ctx,struct flow*,0); assert(amount_msat_less(excess, AMOUNT_MSAT(1000))); - const size_t max_num_chans = gossmap_max_chan_idx(gossmap); + const size_t max_num_chans = gossmap_max_chan_idx(rq->gossmap); struct chan_flow *chan_flow = tal_arrz(this_ctx,struct chan_flow,max_num_chans); - const size_t max_num_nodes = gossmap_max_node_idx(gossmap); + const size_t max_num_nodes = gossmap_max_node_idx(rq->gossmap); s64 *balance = tal_arrz(this_ctx,s64,max_num_nodes); const struct gossmap_chan **prev_chan @@ -1257,7 +1178,7 @@ get_flow_paths(const tal_t *ctx, const struct gossmap *gossmap, { prev_chan[node_idx]=NULL; u32 final_idx = find_positive_balance( - gossmap, disabled, chan_flow, node_idx, balance, + rq->gossmap, chan_flow, node_idx, balance, prev_chan, prev_dir, prev_idx); /* For each route we will compute the highest htlc_min @@ -1281,7 +1202,7 @@ get_flow_paths(const tal_t *ctx, const struct gossmap *gossmap, const int dir = prev_dir[cur_idx]; const struct gossmap_chan *const c = prev_chan[cur_idx]; - const u32 c_idx = gossmap_chan_idx(gossmap,c); + const u32 c_idx = gossmap_chan_idx(rq->gossmap,c); delta=MIN(delta,chan_flow[c_idx].half[dir]); length++; @@ -1289,12 +1210,12 @@ get_flow_paths(const tal_t *ctx, const struct gossmap *gossmap, /* obtain the supremum htlc_min along the route */ sup_htlc_min = amount_msat_max( - sup_htlc_min, channel_htlc_min(c, dir)); + sup_htlc_min, gossmap_chan_htlc_min(c, dir)); /* obtain the infimum htlc_max along the route */ inf_htlc_max = amount_msat_min( - inf_htlc_max, channel_htlc_max(c, dir)); + inf_htlc_max, gossmap_chan_htlc_max(c, dir)); } s64 htlc_max=inf_htlc_max.millisatoshis/1000;/* Raw: need htlc_max in sats to do arithmetic operations.*/ @@ -1338,7 +1259,7 @@ get_flow_paths(const tal_t *ctx, const struct gossmap *gossmap, const int dir = prev_dir[cur_idx]; const struct gossmap_chan *const c = prev_chan[cur_idx]; - const u32 c_idx = gossmap_chan_idx(gossmap,c); + const u32 c_idx = gossmap_chan_idx(rq->gossmap,c); length--; fp->path[length]=c; @@ -1355,22 +1276,14 @@ get_flow_paths(const tal_t *ctx, const struct gossmap *gossmap, // accuracy struct amount_msat delivered = amount_msat(delta*1000); if (!amount_msat_sub(&delivered, delivered, excess)) { - plugin_err(plugin, "Unable to subtract excess %s from %s", - fmt_amount_msat(excess), - fmt_amount_msat(delivered)); + plugin_err(rq->plugin, "Unable to subtract excess %s from %s", + fmt_amount_msat(tmpctx, excess), + fmt_amount_msat(tmpctx, delivered)); } excess = amount_msat(0); fp->amount = delivered; - fp->success_prob = - flow_probability(fp, gossmap, chan_extra_map); - if (fp->success_prob < 0) { - if (fail) - *fail = - tal_fmt(ctx, "failed to compute " - "flow probability"); - goto function_fail; - } + fp->success_prob = flow_probability(fp, rq); // add fp to flows tal_arr_expand(&flows, fp); @@ -1383,14 +1296,8 @@ get_flow_paths(const tal_t *ctx, const struct gossmap *gossmap, flows[i] = tal_steal(flows,flows[i]); assert(flows[i]); } - if (fail) - *fail = NULL; tal_free(this_ctx); return flows; - - function_fail: - tal_free(this_ctx); - return tal_free(flows); } // TODO(eduardo): choose some default values for the minflow parameters @@ -1405,21 +1312,22 @@ get_flow_paths(const tal_t *ctx, const struct gossmap *gossmap, * Check that local channels have fee costs = 0 and bounds with certainty (min=max). */ // TODO(eduardo): we should LOG_DBG the process of finding the MCF while // adjusting the frugality factor. -struct flow **minflow(const tal_t *ctx, struct gossmap *gossmap, +struct flow **minflow(const tal_t *ctx, + const struct route_query *rq, const struct gossmap_node *source, + const struct gossmap_node *target, struct amount_msat amount, u32 mu, double delay_feefactor, double base_fee_penalty, u32 prob_cost_factor) { tal_t *this_ctx = tal(ctx,tal_t); - char *errmsg; struct flow **flow_paths; struct pay_parameters *params = tal(this_ctx,struct pay_parameters); struct dijkstra *dijkstra; - params->gossmap = gossmap; + params->rq = rq; params->source = source; params->target = target; params->amount = amount; @@ -1443,14 +1351,14 @@ struct flow **minflow(const tal_t *ctx, struct gossmap *gossmap, params->prob_cost_factor = prob_cost_factor; // build the uncertainty network with linearization and residual arcs - struct linear_network *linear_network= init_linear_network(this_ctx, params, &errmsg); + struct linear_network *linear_network= init_linear_network(this_ctx, params); struct residual_network *residual_network = alloc_residual_network(this_ctx, linear_network->max_num_nodes, linear_network->max_num_arcs); - dijkstra = dijkstra_new(this_ctx, gossmap_max_node_idx(params->gossmap)); + dijkstra = dijkstra_new(this_ctx, gossmap_max_node_idx(rq->gossmap)); - const u32 target_idx = gossmap_node_idx(params->gossmap,target); - const u32 source_idx = gossmap_node_idx(params->gossmap,source); + const u32 target_idx = gossmap_node_idx(rq->gossmap,target); + const u32 source_idx = gossmap_node_idx(rq->gossmap,source); init_residual_network(linear_network,residual_network); @@ -1475,32 +1383,23 @@ struct flow **minflow(const tal_t *ctx, struct gossmap *gossmap, = amount_msat(pay_amount_msats ? 1000 - pay_amount_msats : 0); if (!find_feasible_flow(this_ctx, linear_network, residual_network, - source_idx, target_idx, pay_amount_sats, - &errmsg)) { - // there is no flow that satisfy the constraints, we stop here - if(fail) - *fail = tal_fmt(ctx, "failed to find a feasible flow: %s", errmsg); + source_idx, target_idx, pay_amount_sats)) { goto function_fail; } combine_cost_function(linear_network, residual_network, mu); /* We solve a linear MCF problem. */ - if(!optimize_mcf(this_ctx, dijkstra,linear_network,residual_network, - source_idx,target_idx,pay_amount_sats, &errmsg)) + if(!optimize_mcf(dijkstra,linear_network,residual_network, + source_idx,target_idx,pay_amount_sats)) { - // optimize_mcf doesn't fail unless there is a bug. - if (fail) - *fail = - tal_fmt(ctx, "optimize_mcf failed: %s", errmsg); goto function_fail; } /* We dissect the solution of the MCF into payment routes. * Actual amounts considering fees are computed for every * channel in the routes. */ - flow_paths = get_flow_paths(this_ctx, params->gossmap, params->disabled, - params->chan_extra_map, linear_network, - residual_network, excess, &errmsg); + flow_paths = get_flow_paths(this_ctx, rq, + linear_network, residual_network, excess); tal_free(this_ctx); return flow_paths; diff --git a/plugins/askrene/mcf.h b/plugins/askrene/mcf.h index 08037aeab2b3..eae5bd506ab0 100644 --- a/plugins/askrene/mcf.h +++ b/plugins/askrene/mcf.h @@ -3,11 +3,10 @@ /* Eduardo Quintela's (lagrang3@protonmail.com) Min Cost Flow implementation * from renepay, as modified to fit askrene */ #include "config.h" -#include #include #include -struct chan_extra_map; +struct route_query; enum { RENEPAY_ERR_OK, @@ -52,7 +51,8 @@ enum { * * Return a series of subflows which deliver amount to target, or NULL. */ -struct flow **minflow(const tal_t *ctx, struct gossmap *gossmap, +struct flow **minflow(const tal_t *ctx, + const struct route_query *rq, const struct gossmap_node *source, const struct gossmap_node *target, struct amount_msat amount, From 79bbb04c4f4fd216f5a739d5640b767b04185182 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Wed, 7 Aug 2024 11:19:55 +0930 Subject: [PATCH 22/28] plugins/askrene: remove local contexts. In general, we should be using tmpctx unless there's a specific reason not to. It's clear, and simplifies the code somewhat. If tmpctx is not cleaned often enough, we can look at a per-MCF context, but this seems like premature optimization. Signed-off-by: Rusty Russell --- plugins/askrene/flow.c | 20 +++++------- plugins/askrene/flow.h | 4 --- plugins/askrene/mcf.c | 73 +++++++++++++----------------------------- 3 files changed, 31 insertions(+), 66 deletions(-) diff --git a/plugins/askrene/flow.c b/plugins/askrene/flow.c index a22180540f08..6d4a3b86d271 100644 --- a/plugins/askrene/flow.c +++ b/plugins/askrene/flow.c @@ -111,9 +111,9 @@ static struct amount_msat channel_maximum_forward(const struct gossmap_chan *cha return best_send; } -struct amount_msat *tal_flow_amounts(const tal_t *ctx, - struct plugin *plugin, - const struct flow *flow) +static struct amount_msat *flow_amounts(const tal_t *ctx, + struct plugin *plugin, + const struct flow *flow) { const size_t pathlen = tal_count(flow->path); struct amount_msat *amounts = tal_arr(ctx, struct amount_msat, pathlen); @@ -137,7 +137,6 @@ struct amount_msat *tal_flow_amounts(const tal_t *ctx, const char *fmt_flows(const tal_t *ctx, const struct route_query *rq, struct flow **flows) { - tal_t *this_ctx = tal(ctx, tal_t); double tot_prob = flowset_probability(flows, rq); assert(tot_prob >= 0); char *buff = tal_fmt(ctx, "%zu subflows, prob %2lf\n", tal_count(flows), @@ -149,17 +148,16 @@ const char *fmt_flows(const tal_t *ctx, const struct route_query *rq, struct short_channel_id scid = gossmap_chan_scid(rq->gossmap, flows[i]->path[j]); tal_append_fmt(&buff, "%s%s", j ? "->" : "", - fmt_short_channel_id(this_ctx, scid)); + fmt_short_channel_id(tmpctx, scid)); } delivered = flows[i]->amount; fee = flow_fee(rq->plugin, flows[i]); tal_append_fmt(&buff, " prob %.2f, %s delivered with fee %s\n", flows[i]->success_prob, - fmt_amount_msat(this_ctx, delivered), - fmt_amount_msat(this_ctx, fee)); + fmt_amount_msat(tmpctx, delivered), + fmt_amount_msat(tmpctx, fee)); } - tal_free(this_ctx); return buff; } @@ -310,19 +308,18 @@ struct chan_inflight_flow double flowset_probability(struct flow **flows, const struct route_query *rq) { - tal_t *this_ctx = tal(tmpctx, tal_t); double prob = 1.0; // TODO(eduardo): should it be better to use a map instead of an array // here? const size_t max_num_chans = gossmap_max_chan_idx(rq->gossmap); struct chan_inflight_flow *in_flight = - tal_arrz(this_ctx, struct chan_inflight_flow, max_num_chans); + tal_arrz(tmpctx, struct chan_inflight_flow, max_num_chans); for (size_t i = 0; i < tal_count(flows); ++i) { const struct flow *f = flows[i]; const size_t pathlen = tal_count(f->path); - struct amount_msat *amounts = tal_flow_amounts(this_ctx, rq->plugin, f); + struct amount_msat *amounts = flow_amounts(tmpctx, rq->plugin, f); for (size_t j = 0; j < pathlen; ++j) { struct amount_msat mincap, maxcap; @@ -344,7 +341,6 @@ double flowset_probability(struct flow **flows, } } } - tal_free(this_ctx); return prob; } diff --git a/plugins/askrene/flow.h b/plugins/askrene/flow.h index 68d04442f33a..f85592739654 100644 --- a/plugins/askrene/flow.h +++ b/plugins/askrene/flow.h @@ -60,10 +60,6 @@ static inline struct amount_msat flow_delivers(const struct flow *flow) return flow->amount; } -struct amount_msat *tal_flow_amounts(const tal_t *ctx, - struct plugin *plugin, - const struct flow *flow); - /* FIXME: remove */ enum askrene_errorcode { ASKRENE_NOERROR = 0, diff --git a/plugins/askrene/mcf.c b/plugins/askrene/mcf.c index 53cffea0a51e..b098fa52c24b 100644 --- a/plugins/askrene/mcf.c +++ b/plugins/askrene/mcf.c @@ -700,8 +700,6 @@ find_admissible_path(const struct linear_network *linear_network, const struct residual_network *residual_network, const u32 source, const u32 target, struct arc *prev) { - tal_t *this_ctx = tal(tmpctx,tal_t); - bool target_found = false; for(size_t i=0;iidx = source; lqueue_enqueue(&myqueue,qdata); @@ -747,12 +745,11 @@ find_admissible_path(const struct linear_network *linear_network, prev[next] = arc; - qdata = tal(this_ctx,struct queue_data); + qdata = tal(tmpctx, struct queue_data); qdata->idx = next; lqueue_enqueue(&myqueue,qdata); } } - tal_free(this_ctx); return target_found; } @@ -824,17 +821,15 @@ static void augment_flow( * * 13/04/2023 This implementation uses a simple augmenting path approach. * */ -static bool find_feasible_flow(const tal_t *ctx, - const struct linear_network *linear_network, +static bool find_feasible_flow(const struct linear_network *linear_network, struct residual_network *residual_network, const u32 source, const u32 target, s64 amount) { assert(amount>=0); - tal_t *this_ctx = tal(ctx,tal_t); /* path information * prev: is the id of the arc that lead to the node. */ - struct arc *prev = tal_arr(this_ctx,struct arc,linear_network->max_num_nodes); + struct arc *prev = tal_arr(tmpctx,struct arc,linear_network->max_num_nodes); while(amount>0) { @@ -842,7 +837,7 @@ static bool find_feasible_flow(const tal_t *ctx, if (!find_admissible_path(linear_network, residual_network, source, target, prev)) { - goto function_fail; + return false; } // traverse the path and see how much flow we can send @@ -858,12 +853,7 @@ static bool find_feasible_flow(const tal_t *ctx, amount -= delta; } - tal_free(this_ctx); return true; - - function_fail: - tal_free(this_ctx); - return false; } // TODO(eduardo): unit test this @@ -875,11 +865,10 @@ static bool find_optimal_path(struct dijkstra *dijkstra, const u32 source, const u32 target, struct arc *prev) { - tal_t *this_ctx = tal(tmpctx,tal_t); bool target_found = false; - bitmap *visited = tal_arrz(this_ctx, bitmap, - BITMAP_NWORDS(linear_network->max_num_nodes)); + bitmap *visited = tal_arrz(tmpctx, bitmap, + BITMAP_NWORDS(linear_network->max_num_nodes)); for(size_t i=0;i=0); - tal_t *this_ctx = tal(tmpctx,tal_t); zero_flow(linear_network,residual_network); - struct arc *prev = tal_arr(this_ctx,struct arc,linear_network->max_num_nodes); + struct arc *prev = tal_arr(tmpctx,struct arc,linear_network->max_num_nodes); const s64 *const distance = dijkstra_distance_data(dijkstra); @@ -983,7 +970,7 @@ static bool optimize_mcf(struct dijkstra *dijkstra, { if (!find_optimal_path(dijkstra, linear_network, residual_network, source, target, prev)) { - goto function_fail; + return false; } // traverse the path and see how much flow we can send @@ -1014,13 +1001,7 @@ static bool optimize_mcf(struct dijkstra *dijkstra, * */ } } - tal_free(this_ctx); return true; - - function_fail: - - tal_free(this_ctx); - return false; } // flow on directed channels @@ -1121,23 +1102,22 @@ get_flow_paths(const tal_t *ctx, // in the MCF solver struct amount_msat excess) { - tal_t *this_ctx = tal(ctx,tal_t); struct flow **flows = tal_arr(ctx,struct flow*,0); assert(amount_msat_less(excess, AMOUNT_MSAT(1000))); const size_t max_num_chans = gossmap_max_chan_idx(rq->gossmap); - struct chan_flow *chan_flow = tal_arrz(this_ctx,struct chan_flow,max_num_chans); + struct chan_flow *chan_flow = tal_arrz(tmpctx,struct chan_flow,max_num_chans); const size_t max_num_nodes = gossmap_max_node_idx(rq->gossmap); - s64 *balance = tal_arrz(this_ctx,s64,max_num_nodes); + s64 *balance = tal_arrz(tmpctx,s64,max_num_nodes); const struct gossmap_chan **prev_chan - = tal_arr(this_ctx,const struct gossmap_chan *,max_num_nodes); + = tal_arr(tmpctx,const struct gossmap_chan *,max_num_nodes); - int *prev_dir = tal_arr(this_ctx,int,max_num_nodes); - u32 *prev_idx = tal_arr(this_ctx,u32,max_num_nodes); + int *prev_dir = tal_arr(tmpctx,int,max_num_nodes); + u32 *prev_idx = tal_arr(tmpctx,u32,max_num_nodes); // Convert the arc based residual network flow into a flow in the // directed channel network. @@ -1243,7 +1223,7 @@ get_flow_paths(const tal_t *ctx, htlc_max); } - struct flow *fp = tal(this_ctx,struct flow); + struct flow *fp = tal(tmpctx,struct flow); fp->path = tal_arr(fp,const struct gossmap_chan *,length); fp->dirs = tal_arr(fp,int,length); @@ -1296,7 +1276,6 @@ get_flow_paths(const tal_t *ctx, flows[i] = tal_steal(flows,flows[i]); assert(flows[i]); } - tal_free(this_ctx); return flows; } @@ -1321,10 +1300,9 @@ struct flow **minflow(const tal_t *ctx, double delay_feefactor, double base_fee_penalty, u32 prob_cost_factor) { - tal_t *this_ctx = tal(ctx,tal_t); struct flow **flow_paths; - struct pay_parameters *params = tal(this_ctx,struct pay_parameters); + struct pay_parameters *params = tal(tmpctx,struct pay_parameters); struct dijkstra *dijkstra; params->rq = rq; @@ -1351,11 +1329,11 @@ struct flow **minflow(const tal_t *ctx, params->prob_cost_factor = prob_cost_factor; // build the uncertainty network with linearization and residual arcs - struct linear_network *linear_network= init_linear_network(this_ctx, params); + struct linear_network *linear_network= init_linear_network(tmpctx, params); struct residual_network *residual_network = - alloc_residual_network(this_ctx, linear_network->max_num_nodes, + alloc_residual_network(tmpctx, linear_network->max_num_nodes, linear_network->max_num_arcs); - dijkstra = dijkstra_new(this_ctx, gossmap_max_node_idx(rq->gossmap)); + dijkstra = dijkstra_new(tmpctx, gossmap_max_node_idx(rq->gossmap)); const u32 target_idx = gossmap_node_idx(rq->gossmap,target); const u32 source_idx = gossmap_node_idx(rq->gossmap,source); @@ -1382,9 +1360,9 @@ struct flow **minflow(const tal_t *ctx, const struct amount_msat excess = amount_msat(pay_amount_msats ? 1000 - pay_amount_msats : 0); - if (!find_feasible_flow(this_ctx, linear_network, residual_network, + if (!find_feasible_flow(linear_network, residual_network, source_idx, target_idx, pay_amount_sats)) { - goto function_fail; + return NULL; } combine_cost_function(linear_network, residual_network, mu); @@ -1392,19 +1370,14 @@ struct flow **minflow(const tal_t *ctx, if(!optimize_mcf(dijkstra,linear_network,residual_network, source_idx,target_idx,pay_amount_sats)) { - goto function_fail; + return NULL; } /* We dissect the solution of the MCF into payment routes. * Actual amounts considering fees are computed for every * channel in the routes. */ - flow_paths = get_flow_paths(this_ctx, rq, + flow_paths = get_flow_paths(tmpctx, rq, linear_network, residual_network, excess); - tal_free(this_ctx); return flow_paths; - - function_fail: - tal_free(this_ctx); - return NULL; } From ee4dba7ea3c162016edbfc5f71649dee2a79f6ad Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Wed, 7 Aug 2024 11:19:55 +0930 Subject: [PATCH 23/28] plugins/askrene: attach getroutes call to MCF code. Now getroutes actually does something! Signed-off-by: Rusty Russell --- plugins/askrene/askrene.c | 161 +++++++++++++++++++++++++++++++++----- tests/test_askrene.py | 150 ++++++++++++++++++++++++++++++++++- 2 files changed, 291 insertions(+), 20 deletions(-) diff --git a/plugins/askrene/askrene.c b/plugins/askrene/askrene.c index 99a743c913fe..abc17dcebb4b 100644 --- a/plugins/askrene/askrene.c +++ b/plugins/askrene/askrene.c @@ -15,7 +15,9 @@ #include #include #include +#include #include +#include #include #include @@ -168,12 +170,22 @@ static const char *get_routes(struct command *cmd, const struct node_id *source, const struct node_id *dest, struct amount_msat amount, + struct amount_msat maxfee, + u32 finalcltv, const char **layers, - struct route ***routes) + struct route ***routes, + struct amount_msat **amounts, + double *probability) { struct askrene *askrene = get_askrene(cmd->plugin); struct route_query *rq = tal(cmd, struct route_query); struct gossmap_localmods *localmods; + struct flow **flows; + const struct gossmap_node *srcnode, *dstnode; + double delay_feefactor; + double base_fee_penalty; + u32 prob_cost_factor, mu; + const char *ret; if (gossmap_refresh(askrene->gossmap, NULL)) { /* FIXME: gossmap_refresh callbacks to we can update in place */ @@ -208,20 +220,122 @@ static const char *get_routes(struct command *cmd, reserves_clear_capacities(askrene->reserved, askrene->gossmap, rq->capacities); gossmap_apply_localmods(askrene->gossmap, localmods); - (void)rq; - - /* FIXME: Do route here! This is a dummy, single "direct" route. */ - *routes = tal_arr(cmd, struct route *, 1); - (*routes)[0]->success_prob = 1; - (*routes)[0]->hops = tal_arr((*routes)[0], struct route_hop, 1); - (*routes)[0]->hops[0].scid.u64 = 0x0000010000020003ULL; - (*routes)[0]->hops[0].direction = 0; - (*routes)[0]->hops[0].node_id = *dest; - (*routes)[0]->hops[0].amount = amount; - (*routes)[0]->hops[0].delay = 6; + srcnode = gossmap_find_node(askrene->gossmap, source); + if (!srcnode) { + ret = tal_fmt(cmd, "Unknown source node %s", fmt_node_id(tmpctx, source)); + goto out; + } + + dstnode = gossmap_find_node(askrene->gossmap, dest); + if (!dstnode) { + ret = tal_fmt(cmd, "Unknown destination node %s", fmt_node_id(tmpctx, dest)); + goto out; + } + + delay_feefactor = 1.0/1000000; + base_fee_penalty = 10.0; + + /* From mcf.c: The input parameter `prob_cost_factor` in the function + * `minflow` is defined as the PPM from the delivery amount `T` we are + * *willing to pay* to increase the prob. of success by 0.1% */ + + /* This value is somewhat implied by our fee budget: say we would pay + * the entire budget for 100% probability, that means prob_cost_factor + * is (fee / amount) / 1000, or in PPM: (fee / amount) * 1000 */ + if (amount_msat_zero(amount)) + prob_cost_factor = 0; + else + prob_cost_factor = amount_msat_ratio(maxfee, amount) * 1000; + + /* First up, don't care about fees. */ + mu = 0; + flows = minflow(rq, rq, srcnode, dstnode, amount, + mu, delay_feefactor, base_fee_penalty, prob_cost_factor); + if (!flows) { + /* FIXME: disjktra here to see if there is any route, and + * diagnose problem (offline peers? Not enough capacity at + * our end? Not enough at theirs?) */ + ret = tal_fmt(cmd, "Could not find route"); + goto out; + } + + /* Too much delay? */ + /* BOLT #4: + * ## `max_htlc_cltv` Selection + * + * This ... value is defined as 2016 blocks, based on historical value + * deployed by Lightning implementations. + */ + /* FIXME: Typo in spec for CLTV in descripton! But it breaks our spelling check, so we omit it above */ + while (finalcltv + flows_worst_delay(flows) > 2016) { + delay_feefactor *= 2; + flows = minflow(rq, rq, srcnode, dstnode, amount, + mu, delay_feefactor, base_fee_penalty, prob_cost_factor); + if (!flows || delay_feefactor > 10) { + ret = tal_fmt(cmd, "Could not find route without excessive delays"); + goto out; + } + } + + /* Too expensive? */ + while (amount_msat_greater(flowset_fee(cmd->plugin, flows), maxfee)) { + mu += 10; + flows = minflow(rq, rq, srcnode, dstnode, amount, + mu, delay_feefactor, base_fee_penalty, prob_cost_factor); + if (!flows || mu == 100) { + ret = tal_fmt(cmd, "Could not find route without excessive cost"); + goto out; + } + } + + if (finalcltv + flows_worst_delay(flows) > 2016) { + ret = tal_fmt(cmd, "Could not find route without excessive cost or delays"); + goto out; + } + + /* Convert back into routes, with delay and other information fixed */ + *routes = tal_arr(cmd, struct route *, tal_count(flows)); + *amounts = tal_arr(cmd, struct amount_msat, tal_count(flows)); + for (size_t i = 0; i < tal_count(flows); i++) { + struct route *r; + struct amount_msat msat; + u32 delay; + + (*routes)[i] = r = tal(*routes, struct route); + /* FIXME: flow_probability doesn't take into account other flows! */ + r->success_prob = flows[i]->success_prob; + r->hops = tal_arr(r, struct route_hop, tal_count(flows[i]->path)); + + /* Fill in backwards to calc amount and delay */ + msat = flows[i]->amount; + delay = finalcltv; + + for (int j = tal_count(flows[i]->path) - 1; j >= 0; j--) { + struct route_hop *rh = &r->hops[j]; + struct gossmap_node *far_end; + const struct half_chan *h = flow_edge(flows[i], j); + + if (!amount_msat_add_fee(&msat, h->base_fee, h->proportional_fee)) + plugin_err(cmd->plugin, "Adding fee to amount"); + delay += h->delay; + + rh->scid = gossmap_chan_scid(rq->gossmap, flows[i]->path[j]); + rh->direction = flows[i]->dirs[j]; + far_end = gossmap_nth_node(rq->gossmap, flows[i]->path[j], !flows[i]->dirs[j]); + gossmap_node_get_id(rq->gossmap, far_end, &rh->node_id); + rh->amount = msat; + rh->delay = delay; + } + (*amounts)[i] = flow_delivers(flows[i]); + } + + *probability = flowset_probability(flows, rq); + ret = NULL; + +out: gossmap_remove_localmods(askrene->gossmap, localmods); - return NULL; + return ret; } void get_constraints(const struct route_query *rq, @@ -294,41 +408,50 @@ static struct command_result *json_getroutes(struct command *cmd, { struct node_id *dest, *source; const char **layers; - struct amount_msat *amount; + struct amount_msat *amount, *maxfee, *amounts; struct route **routes; struct json_stream *response; + u32 *finalcltv; const char *err; + double probability; if (!param(cmd, buffer, params, p_req("source", param_node_id, &source), p_req("destination", param_node_id, &dest), p_req("amount_msat", param_msat, &amount), p_req("layers", param_string_array, &layers), + p_req("maxfee_msat", param_msat, &maxfee), + p_req("finalcltv", param_u32, &finalcltv), NULL)) return command_param_failed(); - err = get_routes(cmd, source, dest, *amount, layers, &routes); + err = get_routes(cmd, source, dest, *amount, *maxfee, *finalcltv, layers, + &routes, &amounts, &probability); if (err) return command_fail(cmd, PAY_ROUTE_NOT_FOUND, "%s", err); response = jsonrpc_stream_success(cmd); - json_object_start(response, "routes"); + json_add_u64(response, "probability_ppm", (u64)(probability * 1000000)); json_array_start(response, "routes"); for (size_t i = 0; i < tal_count(routes); i++) { + json_object_start(response, NULL); json_add_u64(response, "probability_ppm", (u64)(routes[i]->success_prob * 1000000)); + json_add_amount_msat(response, "amount_msat", amounts[i]); json_array_start(response, "path"); for (size_t j = 0; j < tal_count(routes[i]->hops); j++) { const struct route_hop *r = &routes[i]->hops[j]; + json_object_start(response, NULL); json_add_short_channel_id(response, "short_channel_id", r->scid); json_add_u32(response, "direction", r->direction); - json_add_node_id(response, "node_id", &r->node_id); - json_add_amount_msat(response, "amount", r->amount); + json_add_node_id(response, "next_node_id", &r->node_id); + json_add_amount_msat(response, "amount_msat", r->amount); json_add_u32(response, "delay", r->delay); + json_object_end(response); } json_array_end(response); + json_object_end(response); } json_array_end(response); - json_object_end(response); return command_finished(cmd, response); } diff --git a/tests/test_askrene.py b/tests/test_askrene.py index 94683110f302..06df8278574f 100644 --- a/tests/test_askrene.py +++ b/tests/test_askrene.py @@ -1,8 +1,13 @@ from fixtures import * # noqa: F401,F403 +from pyln.client import RpcError from utils import ( - only_one, first_scid + only_one, first_scid, GenChannel, generate_gossip_store, + TEST_NETWORK ) +import os +import pytest import time +import shutil def test_layers(node_factory): @@ -102,3 +107,146 @@ def test_layers(node_factory): del expect['constraints'][0] listlayers = l2.rpc.askrene_listlayers('test_layers') assert listlayers == {'layers': [expect]} + + +def check_getroute_paths(node, + source, + destination, + amount_msat, + paths, + layers=[], + maxfee_msat=1000, + finalcltv=99): + """Check that routes are as expected in result""" + getroutes = node.rpc.getroutes(source=source, + destination=destination, + amount_msat=amount_msat, + layers=layers, + maxfee_msat=maxfee_msat, + finalcltv=finalcltv) + + assert getroutes['probability_ppm'] <= 1000000 + # Total delivered should be amount we told it to send. + assert amount_msat == sum([r['amount_msat'] for r in getroutes['routes']]) + + def dict_subset_eq(a, b): + """Is every key in B is the same in A?""" + return all(a.get(key) == b[key] for key in b) + + for expected_path in paths: + found = False + for i in range(len(getroutes['routes'])): + route = getroutes['routes'][i] + if len(route['path']) != len(expected_path): + continue + if all(dict_subset_eq(route['path'][i], expected_path[i]) for i in range(len(expected_path))): + del getroutes['routes'][i] + found = True + break + if not found: + raise ValueError("Could not find expected_path {} in paths {}".format(expected_path, getroutes['routes'])) + + if getroutes['routes'] != []: + raise ValueError("Did not expect paths {}".format(getroutes['routes'])) + + +def test_getroutes(node_factory): + """Test getroutes call""" + l1 = node_factory.get_node(start=False) + gsfile, nodemap = generate_gossip_store([GenChannel(0, 1, forward=GenChannel.Half(propfee=10000)), + GenChannel(0, 2, capacity_sats=9000), + GenChannel(1, 3, forward=GenChannel.Half(propfee=20000)), + GenChannel(0, 2, capacity_sats=10000), + GenChannel(2, 4, forward=GenChannel.Half(delay=2000))]) + + # Set up l1 with this as the gossip_store + shutil.copy(gsfile.name, os.path.join(l1.daemon.lightning_dir, TEST_NETWORK, 'gossip_store')) + l1.start() + + # Start easy + assert l1.rpc.getroutes(source=nodemap[0], + destination=nodemap[1], + amount_msat=1000, + layers=[], + maxfee_msat=1000, + finalcltv=99) == {'probability_ppm': 999999, + 'routes': [{'probability_ppm': 999999, + 'amount_msat': 1000, + 'path': [{'short_channel_id': '0x1x0', + 'direction': 1, + 'next_node_id': nodemap[1], + 'amount_msat': 1010, + 'delay': 99 + 6}]}]} + # Two hop, still easy. + assert l1.rpc.getroutes(source=nodemap[0], + destination=nodemap[3], + amount_msat=100000, + layers=[], + maxfee_msat=5000, + finalcltv=99) == {'probability_ppm': 999798, + 'routes': [{'probability_ppm': 999798, + 'amount_msat': 100000, + 'path': [{'short_channel_id': '0x1x0', + 'direction': 1, + 'next_node_id': nodemap[1], + 'amount_msat': 103020, + 'delay': 99 + 6 + 6}, + {'short_channel_id': '1x3x2', + 'direction': 1, + 'next_node_id': nodemap[3], + 'amount_msat': 102000, + 'delay': 99 + 6} + ]}]} + + # Too expensive + with pytest.raises(RpcError, match="Could not find route without excessive cost"): + l1.rpc.getroutes(source=nodemap[0], + destination=nodemap[3], + amount_msat=100000, + layers=[], + maxfee_msat=100, + finalcltv=99) + + # Too much delay (if final delay too great!) + l1.rpc.getroutes(source=nodemap[0], + destination=nodemap[4], + amount_msat=100000, + layers=[], + maxfee_msat=100, + finalcltv=6) + with pytest.raises(RpcError, match="Could not find route without excessive delays"): + l1.rpc.getroutes(source=nodemap[0], + destination=nodemap[4], + amount_msat=100000, + layers=[], + maxfee_msat=100, + finalcltv=99) + + # Two choices, but for <= 1000 sats we choose the larger. + assert l1.rpc.getroutes(source=nodemap[0], + destination=nodemap[2], + amount_msat=1000000, + layers=[], + maxfee_msat=5000, + finalcltv=99) == {'probability_ppm': 900000, + 'routes': [{'probability_ppm': 900000, + 'amount_msat': 1000000, + 'path': [{'short_channel_id': '0x2x3', + 'direction': 1, + 'next_node_id': nodemap[2], + 'amount_msat': 1000001, + 'delay': 99 + 6}]}]} + + # For 10000 sats, we will split. + check_getroute_paths(l1, + nodemap[0], + nodemap[2], + 10000000, + [[{'short_channel_id': '0x2x1', + 'next_node_id': nodemap[2], + 'amount_msat': 500000, + 'delay': 99 + 6}], + [{'short_channel_id': '0x2x3', + 'next_node_id': nodemap[2], + 'amount_msat': 9500009, + 'delay': 99 + 6}]]) From 6702563702a935f1a5be8e7004e17ed3f4fca810 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Wed, 7 Aug 2024 11:19:55 +0930 Subject: [PATCH 24/28] pytest: simple getroutes tests. Signed-off-by: Rusty Russell --- tests/test_askrene.py | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/tests/test_askrene.py b/tests/test_askrene.py index 06df8278574f..b143f18e735e 100644 --- a/tests/test_askrene.py +++ b/tests/test_askrene.py @@ -250,3 +250,42 @@ def test_getroutes(node_factory): 'next_node_id': nodemap[2], 'amount_msat': 9500009, 'delay': 99 + 6}]]) + + +def test_getroutes_fee_fallback(node_factory): + """Test getroutes call takes into account fees, if excessive""" + + l1 = node_factory.get_node(start=False) + # 0 -> 1 -> 3: high capacity, high fee (1%) + # 0 -> 2 -> 3: low capacity, low fee. + gsfile, nodemap = generate_gossip_store([GenChannel(0, 1, + capacity_sats=20000, + forward=GenChannel.Half(propfee=10000)), + GenChannel(0, 2, + capacity_sats=10000), + GenChannel(1, 3, + capacity_sats=20000, + forward=GenChannel.Half(propfee=10000)), + GenChannel(2, 3, + capacity_sats=10000)]) + # Set up l1 with this as the gossip_store + shutil.copy(gsfile.name, os.path.join(l1.daemon.lightning_dir, TEST_NETWORK, 'gossip_store')) + l1.start() + + # Don't hit maxfee? Go easy path. + check_getroute_paths(l1, + nodemap[0], + nodemap[3], + 10000, + maxfee_msat=201, + paths=[[{'short_channel_id': '0x1x0'}, + {'short_channel_id': '1x3x2'}]]) + + # maxfee exceeded? lower prob path. + check_getroute_paths(l1, + nodemap[0], + nodemap[3], + 10000, + maxfee_msat=200, + paths=[[{'short_channel_id': '0x2x1'}, + {'short_channel_id': '2x3x3'}]]) From b3b885b856ee846a0a7faeee6d42bfaba335463c Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Wed, 7 Aug 2024 11:19:55 +0930 Subject: [PATCH 25/28] plugin/askrene: add "auto.sourcefree" layer. This marks all channels around the source node as free (no delay, no fee). This is normally what we want, if we are calculating a path for ourselves. Signed-off-by: Rusty Russell --- plugins/askrene/askrene.c | 69 ++++++++++++++++++++++++++++++++++-- tests/test_askrene.py | 73 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 139 insertions(+), 3 deletions(-) diff --git a/plugins/askrene/askrene.c b/plugins/askrene/askrene.c index abc17dcebb4b..cff85c7adfdc 100644 --- a/plugins/askrene/askrene.c +++ b/plugins/askrene/askrene.c @@ -26,6 +26,15 @@ static struct askrene *get_askrene(struct plugin *plugin) return plugin_get_data(plugin, struct askrene); } +static bool have_layer(const char **layers, const char *name) +{ + for (size_t i = 0; i < tal_count(layers); i++) { + if (streq(layers[i], name)) + return true; + } + return false; +} + /* JSON helpers */ static struct command_result *param_string_array(struct command *cmd, const char *name, @@ -165,6 +174,43 @@ static fp16_t *get_capacities(const tal_t *ctx, return caps; } +/* If we're the payer, we don't add delay or fee to our own outgoing + * channels. This wouldn't be right if we looped back through ourselves, + * but we won't. */ +/* FIXME: We could cache this until gossmap changes... */ +static void add_free_source(struct command *cmd, + struct gossmap *gossmap, + struct gossmap_localmods *localmods, + const struct node_id *source) +{ + const struct gossmap_node *srcnode; + + /* If we're not in map, we complain later */ + srcnode = gossmap_find_node(gossmap, source); + if (!srcnode) + return; + + for (size_t i = 0; i < srcnode->num_chans; i++) { + struct gossmap_chan *c; + int dir; + struct short_channel_id scid; + + c = gossmap_nth_chan(gossmap, srcnode, i, &dir); + scid = gossmap_chan_scid(gossmap, c); + if (!gossmap_local_updatechan(localmods, + scid, + /* Keep min and max */ + gossmap_chan_htlc_min(c, dir), + gossmap_chan_htlc_max(c, dir), + 0, 0, 0, + /* Keep enabled flag */ + c->half[dir].enabled, + dir)) + plugin_err(cmd->plugin, "Could not zero fee on local %s", + fmt_short_channel_id(tmpctx, scid)); + } +} + /* Returns an error message, or sets *routes */ static const char *get_routes(struct command *cmd, const struct node_id *source, @@ -215,6 +261,9 @@ static const char *get_routes(struct command *cmd, layer_clear_overridden_capacities(l, askrene->gossmap, rq->capacities); } + if (have_layer(layers, "auto.sourcefree")) + add_free_source(cmd, askrene->gossmap, localmods, source); + /* Clear scids with reservations, too, so we don't have to look up * all the time! */ reserves_clear_capacities(askrene->reserved, askrene->gossmap, rq->capacities); @@ -516,6 +565,20 @@ static struct command_result *json_askrene_unreserve(struct command *cmd, return command_finished(cmd, response); } +static struct command_result *param_layername(struct command *cmd, + const char *name, + const char *buffer, + const jsmntok_t *tok, + const char **str) +{ + *str = tal_strndup(cmd, buffer + tok->start, + tok->end - tok->start); + if (strstarts(*str, "auto.")) + return command_fail_badparam(cmd, name, buffer, tok, + "New layers cannot start with auto."); + return NULL; +} + static struct command_result *json_askrene_create_channel(struct command *cmd, const char *buffer, const jsmntok_t *params) @@ -533,7 +596,7 @@ static struct command_result *json_askrene_create_channel(struct command *cmd, struct askrene *askrene = get_askrene(cmd->plugin); if (!param_check(cmd, buffer, params, - p_req("layer", param_string, &layername), + p_req("layer", param_layername, &layername), p_req("source", param_node_id, &src), p_req("destination", param_node_id, &dst), p_req("short_channel_id", param_short_channel_id, &scid), @@ -586,7 +649,7 @@ static struct command_result *json_askrene_inform_channel(struct command *cmd, struct askrene *askrene = get_askrene(cmd->plugin); if (!param_check(cmd, buffer, params, - p_req("layer", param_string, &layername), + p_req("layer", param_layername, &layername), p_req("short_channel_id", param_short_channel_id, &scid), p_req("direction", param_zero_or_one, &direction), p_opt("minimum_msat", param_msat, &min), @@ -633,7 +696,7 @@ static struct command_result *json_askrene_disable_node(struct command *cmd, struct askrene *askrene = get_askrene(cmd->plugin); if (!param(cmd, buffer, params, - p_req("layer", param_string, &layername), + p_req("layer", param_layername, &layername), p_req("node", param_node_id, &node), NULL)) return command_param_failed(); diff --git a/tests/test_askrene.py b/tests/test_askrene.py index b143f18e735e..aa00c41d13ba 100644 --- a/tests/test_askrene.py +++ b/tests/test_askrene.py @@ -289,3 +289,76 @@ def test_getroutes_fee_fallback(node_factory): maxfee_msat=200, paths=[[{'short_channel_id': '0x2x1'}, {'short_channel_id': '2x3x3'}]]) + + +def test_getroutes_auto_sourcefree(node_factory): + """Test getroutes call with auto.sourcefree layer""" + l1 = node_factory.get_node(start=False) + gsfile, nodemap = generate_gossip_store([GenChannel(0, 1, forward=GenChannel.Half(propfee=10000)), + GenChannel(0, 2, capacity_sats=9000), + GenChannel(1, 3, forward=GenChannel.Half(propfee=20000)), + GenChannel(0, 2, capacity_sats=10000), + GenChannel(2, 4, forward=GenChannel.Half(delay=2000))]) + + # Set up l1 with this as the gossip_store + shutil.copy(gsfile.name, os.path.join(l1.daemon.lightning_dir, TEST_NETWORK, 'gossip_store')) + l1.start() + + # Start easy + assert l1.rpc.getroutes(source=nodemap[0], + destination=nodemap[1], + amount_msat=1000, + layers=['auto.sourcefree'], + maxfee_msat=1000, + finalcltv=99) == {'probability_ppm': 999999, + 'routes': [{'probability_ppm': 999999, + 'amount_msat': 1000, + 'path': [{'short_channel_id': '0x1x0', + 'direction': 1, + 'next_node_id': nodemap[1], + 'amount_msat': 1000, + 'delay': 99}]}]} + # Two hop, still easy. + assert l1.rpc.getroutes(source=nodemap[0], + destination=nodemap[3], + amount_msat=100000, + layers=['auto.sourcefree'], + maxfee_msat=5000, + finalcltv=99) == {'probability_ppm': 999798, + 'routes': [{'probability_ppm': 999798, + 'amount_msat': 100000, + 'path': [{'short_channel_id': '0x1x0', + 'direction': 1, + 'next_node_id': nodemap[1], + 'amount_msat': 102000, + 'delay': 99 + 6}, + {'short_channel_id': '1x3x2', + 'direction': 1, + 'next_node_id': nodemap[3], + 'amount_msat': 102000, + 'delay': 99 + 6} + ]}]} + + # Too expensive + with pytest.raises(RpcError, match="Could not find route without excessive cost"): + l1.rpc.getroutes(source=nodemap[0], + destination=nodemap[3], + amount_msat=100000, + layers=[], + maxfee_msat=100, + finalcltv=99) + + # Too much delay (if final delay too great!) + l1.rpc.getroutes(source=nodemap[0], + destination=nodemap[4], + amount_msat=100000, + layers=[], + maxfee_msat=100, + finalcltv=6) + with pytest.raises(RpcError, match="Could not find route without excessive delays"): + l1.rpc.getroutes(source=nodemap[0], + destination=nodemap[4], + amount_msat=100000, + layers=[], + maxfee_msat=100, + finalcltv=99) From 61c314a9daf1c27d7e16805bb65b9d933ec95282 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Wed, 7 Aug 2024 11:19:55 +0930 Subject: [PATCH 26/28] devtools/gossmap-compress: allow setting the nodeid explicitly for generated nodes. This lets us make gossip which contains "real" nodes. Signed-off-by: Rusty Russell --- devtools/gossmap-compress.c | 51 ++++++++++++++++++++++++++++--------- plugins/askrene/askrene.c | 6 ++--- tests/utils.py | 3 ++- 3 files changed, 44 insertions(+), 16 deletions(-) diff --git a/devtools/gossmap-compress.c b/devtools/gossmap-compress.c index 86ec8fb6c89e..29d6e1398d90 100644 --- a/devtools/gossmap-compress.c +++ b/devtools/gossmap-compress.c @@ -277,10 +277,16 @@ static u64 get_delay(struct gossmap *gossmap, return chan->half[dir].delay; } -static void pubkey_for_node(size_t nodeidx, struct pubkey *key) +static void pubkey_for_node(size_t nodeidx, struct pubkey *key, + const struct pubkey *node_ids) { struct secret seckey; + if (nodeidx < tal_count(node_ids)) { + *key = node_ids[nodeidx]; + return; + } + memset(&seckey, 1, sizeof(seckey)); memcpy(&seckey, &nodeidx, sizeof(nodeidx)); if (!pubkey_from_secret(&seckey, key)) @@ -324,7 +330,8 @@ static void write_announce(int outfd, size_t node1, size_t node2, u64 capacity, - size_t i) + size_t i, + const struct pubkey *node_ids) { struct { secp256k1_ecdsa_signature sig; @@ -336,8 +343,8 @@ static void write_announce(int outfd, struct node_id nodeid1, nodeid2; memset(&vals, 0, sizeof(vals)); - pubkey_for_node(node1, &id1); - pubkey_for_node(node2, &id2); + pubkey_for_node(node1, &id1, node_ids); + pubkey_for_node(node2, &id2, node_ids); /* Nodes in pubkey order */ if (pubkey_cmp(&id1, &id2) < 0) { @@ -385,7 +392,8 @@ static void write_update(int outfd, u64 htlc_min, u64 htlc_max, u64 basefee, u32 propfee, - u16 delay) + u16 delay, + const struct pubkey *node_ids) { struct vals { secp256k1_ecdsa_signature sig; @@ -404,8 +412,8 @@ static void write_update(int outfd, abort(); /* If node ids are backward, dir is reversed */ - pubkey_for_node(node1, &id1); - pubkey_for_node(node2, &id2); + pubkey_for_node(node1, &id1, node_ids); + pubkey_for_node(node2, &id2, node_ids); if (pubkey_cmp(&id1, &id2) > 0) dir = !dir; @@ -467,14 +475,32 @@ static char *opt_add_one(unsigned int *val) return NULL; } +static char *opt_nodes(const char *optarg, struct pubkey **node_ids) +{ + char **ids = tal_strsplit(tmpctx, optarg, ",", STR_EMPTY_OK); + + for (size_t i = 0; ids[i]; i++) { + struct pubkey n; + if (!pubkey_from_hexstr(ids[i], strlen(ids[i]), &n)) + return tal_fmt(tmpctx, "Invalid node id '%s'", ids[i]); + tal_arr_expand(node_ids, n); + } + return NULL; +} + int main(int argc, char *argv[]) { int infd, outfd; + struct pubkey *node_ids; + common_setup(argv[0]); setup_locale(); + node_ids = tal_arr(tmpctx, struct pubkey, 0); opt_register_noarg("--verbose|-v", opt_add_one, &verbose, "Print details (each additional gives more!)."); + opt_register_arg("--nodes", opt_nodes, NULL, &node_ids, + "Comma separated node ids to give first nodes."); opt_register_noarg("--help|-h", opt_usage_and_exit, "[decompress|compress] infile outfile" "Compress or decompress a gossmap file", @@ -643,15 +669,15 @@ int main(int argc, char *argv[]) /* Useful so they can map their ids back to node ids. */ for (size_t i = 0; i < node_limit; i++) { struct pubkey node_id; - pubkey_for_node(i, &node_id); + pubkey_for_node(i, &node_id, node_ids); printf("%s\n", fmt_pubkey(tmpctx, &node_id)); } if (verbose >= 2) { for (size_t i = 0; i < channel_count; i++) { struct pubkey id1, id2; - pubkey_for_node(chans[i].node1, &id1); - pubkey_for_node(chans[i].node2, &id2); + pubkey_for_node(chans[i].node1, &id1, node_ids); + pubkey_for_node(chans[i].node2, &id2, node_ids); printf("Channel %zu: %s -> %s\n", i, fmt_pubkey(tmpctx, &id1), @@ -715,7 +741,7 @@ int main(int argc, char *argv[]) chans[i].node1, chans[i].node2, chans[i].capacity, - i); + i, node_ids); for (size_t dir = 0; dir < 2; dir++) { write_update(outfd, chans[i].node1, chans[i].node2, i, dir, @@ -724,7 +750,8 @@ int main(int argc, char *argv[]) chans[i].half[dir].htlc_max, chans[i].half[dir].basefee, chans[i].half[dir].propfee, - chans[i].half[dir].delay); + chans[i].half[dir].delay, + node_ids); } } gzclose(inf); diff --git a/plugins/askrene/askrene.c b/plugins/askrene/askrene.c index cff85c7adfdc..bb01906ae594 100644 --- a/plugins/askrene/askrene.c +++ b/plugins/askrene/askrene.c @@ -178,7 +178,7 @@ static fp16_t *get_capacities(const tal_t *ctx, * channels. This wouldn't be right if we looped back through ourselves, * but we won't. */ /* FIXME: We could cache this until gossmap changes... */ -static void add_free_source(struct command *cmd, +static void add_free_source(struct plugin *plugin, struct gossmap *gossmap, struct gossmap_localmods *localmods, const struct node_id *source) @@ -206,7 +206,7 @@ static void add_free_source(struct command *cmd, /* Keep enabled flag */ c->half[dir].enabled, dir)) - plugin_err(cmd->plugin, "Could not zero fee on local %s", + plugin_err(plugin, "Could not zero fee on local %s", fmt_short_channel_id(tmpctx, scid)); } } @@ -262,7 +262,7 @@ static const char *get_routes(struct command *cmd, } if (have_layer(layers, "auto.sourcefree")) - add_free_source(cmd, askrene->gossmap, localmods, source); + add_free_source(cmd->plugin, askrene->gossmap, localmods, source); /* Clear scids with reservations, too, so we don't have to look up * all the time! */ diff --git a/tests/utils.py b/tests/utils.py index 3bb8cc6c2976..62da00290011 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -460,7 +460,7 @@ def __init__(self, node1, node2, capacity_sats=1000000, forward=None, reverse=No self.half = [forward, reverse] -def generate_gossip_store(channels): +def generate_gossip_store(channels, nodeids=[]): """Returns a gossip store file with the given channels in it, and a map of node labels -> ids """ nodes = [] @@ -550,6 +550,7 @@ def write_dumb_template(outf, channels, propname, illegalvals=[]): outfile = tempfile.NamedTemporaryFile(prefix='gossip-store-') nodeids = subprocess.check_output(['devtools/gossmap-compress', + '--nodes={}'.format(','.join(nodeids)), 'decompress', cfile.name, outfile.name]).decode('utf-8').splitlines() From 4f6c88e945328e78b99d4aae65087aa0f143d8d8 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Wed, 7 Aug 2024 11:19:55 +0930 Subject: [PATCH 27/28] askrene: split json_getroutes into two parts. This will allow us to call an RPC function in the middle. Signed-off-by: Rusty Russell --- plugins/askrene/askrene.c | 90 +++++++++++++++++++++++---------------- 1 file changed, 53 insertions(+), 37 deletions(-) diff --git a/plugins/askrene/askrene.c b/plugins/askrene/askrene.c index bb01906ae594..d602078afa60 100644 --- a/plugins/askrene/askrene.c +++ b/plugins/askrene/askrene.c @@ -212,20 +212,21 @@ static void add_free_source(struct plugin *plugin, } /* Returns an error message, or sets *routes */ -static const char *get_routes(struct command *cmd, +static const char *get_routes(const tal_t *ctx, + struct plugin *plugin, const struct node_id *source, const struct node_id *dest, struct amount_msat amount, struct amount_msat maxfee, u32 finalcltv, const char **layers, + struct gossmap_localmods *localmods, struct route ***routes, struct amount_msat **amounts, double *probability) { - struct askrene *askrene = get_askrene(cmd->plugin); - struct route_query *rq = tal(cmd, struct route_query); - struct gossmap_localmods *localmods; + struct askrene *askrene = get_askrene(plugin); + struct route_query *rq = tal(ctx, struct route_query); struct flow **flows; const struct gossmap_node *srcnode, *dstnode; double delay_feefactor; @@ -239,12 +240,11 @@ static const char *get_routes(struct command *cmd, askrene->capacities = get_capacities(askrene, askrene->plugin, askrene->gossmap); } - rq->plugin = cmd->plugin; + rq->plugin = plugin; rq->gossmap = askrene->gossmap; rq->reserved = askrene->reserved; rq->layers = tal_arr(rq, const struct layer *, 0); rq->capacities = tal_dup_talarr(rq, fp16_t, askrene->capacities); - localmods = gossmap_localmods_new(rq); /* Layers don't have to exist: they might be empty! */ for (size_t i = 0; i < tal_count(layers); i++) { @@ -262,7 +262,7 @@ static const char *get_routes(struct command *cmd, } if (have_layer(layers, "auto.sourcefree")) - add_free_source(cmd->plugin, askrene->gossmap, localmods, source); + add_free_source(plugin, askrene->gossmap, localmods, source); /* Clear scids with reservations, too, so we don't have to look up * all the time! */ @@ -272,13 +272,13 @@ static const char *get_routes(struct command *cmd, srcnode = gossmap_find_node(askrene->gossmap, source); if (!srcnode) { - ret = tal_fmt(cmd, "Unknown source node %s", fmt_node_id(tmpctx, source)); + ret = tal_fmt(ctx, "Unknown source node %s", fmt_node_id(tmpctx, source)); goto out; } dstnode = gossmap_find_node(askrene->gossmap, dest); if (!dstnode) { - ret = tal_fmt(cmd, "Unknown destination node %s", fmt_node_id(tmpctx, dest)); + ret = tal_fmt(ctx, "Unknown destination node %s", fmt_node_id(tmpctx, dest)); goto out; } @@ -305,7 +305,7 @@ static const char *get_routes(struct command *cmd, /* FIXME: disjktra here to see if there is any route, and * diagnose problem (offline peers? Not enough capacity at * our end? Not enough at theirs?) */ - ret = tal_fmt(cmd, "Could not find route"); + ret = tal_fmt(ctx, "Could not find route"); goto out; } @@ -322,30 +322,30 @@ static const char *get_routes(struct command *cmd, flows = minflow(rq, rq, srcnode, dstnode, amount, mu, delay_feefactor, base_fee_penalty, prob_cost_factor); if (!flows || delay_feefactor > 10) { - ret = tal_fmt(cmd, "Could not find route without excessive delays"); + ret = tal_fmt(ctx, "Could not find route without excessive delays"); goto out; } } /* Too expensive? */ - while (amount_msat_greater(flowset_fee(cmd->plugin, flows), maxfee)) { + while (amount_msat_greater(flowset_fee(plugin, flows), maxfee)) { mu += 10; flows = minflow(rq, rq, srcnode, dstnode, amount, mu, delay_feefactor, base_fee_penalty, prob_cost_factor); if (!flows || mu == 100) { - ret = tal_fmt(cmd, "Could not find route without excessive cost"); + ret = tal_fmt(ctx, "Could not find route without excessive cost"); goto out; } } if (finalcltv + flows_worst_delay(flows) > 2016) { - ret = tal_fmt(cmd, "Could not find route without excessive cost or delays"); + ret = tal_fmt(ctx, "Could not find route without excessive cost or delays"); goto out; } /* Convert back into routes, with delay and other information fixed */ - *routes = tal_arr(cmd, struct route *, tal_count(flows)); - *amounts = tal_arr(cmd, struct amount_msat, tal_count(flows)); + *routes = tal_arr(ctx, struct route *, tal_count(flows)); + *amounts = tal_arr(ctx, struct amount_msat, tal_count(flows)); for (size_t i = 0; i < tal_count(flows); i++) { struct route *r; struct amount_msat msat; @@ -366,7 +366,7 @@ static const char *get_routes(struct command *cmd, const struct half_chan *h = flow_edge(flows[i], j); if (!amount_msat_add_fee(&msat, h->base_fee, h->proportional_fee)) - plugin_err(cmd->plugin, "Adding fee to amount"); + plugin_err(plugin, "Adding fee to amount"); delay += h->delay; rh->scid = gossmap_chan_scid(rq->gossmap, flows[i]->path[j]); @@ -451,30 +451,27 @@ void get_constraints(const struct route_query *rq, } } -static struct command_result *json_getroutes(struct command *cmd, - const char *buffer, - const jsmntok_t *params) -{ - struct node_id *dest, *source; - const char **layers; - struct amount_msat *amount, *maxfee, *amounts; - struct route **routes; - struct json_stream *response; +struct getroutes_info { + struct node_id *source, *dest; + struct amount_msat *amount, *maxfee; u32 *finalcltv; + const char **layers; +}; + +static struct command_result *do_getroutes(struct command *cmd, + struct gossmap_localmods *localmods, + const struct getroutes_info *info) +{ const char *err; double probability; + struct amount_msat *amounts; + struct route **routes; + struct json_stream *response; - if (!param(cmd, buffer, params, - p_req("source", param_node_id, &source), - p_req("destination", param_node_id, &dest), - p_req("amount_msat", param_msat, &amount), - p_req("layers", param_string_array, &layers), - p_req("maxfee_msat", param_msat, &maxfee), - p_req("finalcltv", param_u32, &finalcltv), - NULL)) - return command_param_failed(); - - err = get_routes(cmd, source, dest, *amount, *maxfee, *finalcltv, layers, + err = get_routes(cmd, cmd->plugin, + info->source, info->dest, + *info->amount, *info->maxfee, *info->finalcltv, + info->layers, localmods, &routes, &amounts, &probability); if (err) return command_fail(cmd, PAY_ROUTE_NOT_FOUND, "%s", err); @@ -504,6 +501,25 @@ static struct command_result *json_getroutes(struct command *cmd, return command_finished(cmd, response); } +static struct command_result *json_getroutes(struct command *cmd, + const char *buffer, + const jsmntok_t *params) +{ + struct getroutes_info *info = tal(cmd, struct getroutes_info); + + if (!param(cmd, buffer, params, + p_req("source", param_node_id, &info->source), + p_req("destination", param_node_id, &info->dest), + p_req("amount_msat", param_msat, &info->amount), + p_req("layers", param_string_array, &info->layers), + p_req("maxfee_msat", param_msat, &info->maxfee), + p_req("finalcltv", param_u32, &info->finalcltv), + NULL)) + return command_param_failed(); + + return do_getroutes(cmd, gossmap_localmods_new(cmd), info); +} + static struct command_result *json_askrene_reserve(struct command *cmd, const char *buffer, const jsmntok_t *params) From 3d3a5f991468945857c5b2a776306321ed5f6548 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Wed, 7 Aug 2024 11:19:55 +0930 Subject: [PATCH 28/28] askrene: add "auto.localchans" layer. This populates information on both topology (i.e. unannounced channels) and capacity for the local node using `listpeerchannels`. Signed-off-by: Rusty Russell --- plugins/askrene/askrene.c | 92 ++++++++++++++++++++++++++++++++++++--- plugins/askrene/askrene.h | 3 ++ plugins/askrene/layer.c | 27 +++++++++--- plugins/askrene/layer.h | 8 +++- tests/test_askrene.py | 48 ++++++++++++++++++++ 5 files changed, 162 insertions(+), 16 deletions(-) diff --git a/plugins/askrene/askrene.c b/plugins/askrene/askrene.c index d602078afa60..075457c946a5 100644 --- a/plugins/askrene/askrene.c +++ b/plugins/askrene/askrene.c @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -185,7 +186,8 @@ static void add_free_source(struct plugin *plugin, { const struct gossmap_node *srcnode; - /* If we're not in map, we complain later */ + /* If we're not in map, we complain later (unless we're purely + * using local channels) */ srcnode = gossmap_find_node(gossmap, source); if (!srcnode) return; @@ -221,6 +223,7 @@ static const char *get_routes(const tal_t *ctx, u32 finalcltv, const char **layers, struct gossmap_localmods *localmods, + const struct layer *local_layer, struct route ***routes, struct amount_msat **amounts, double *probability) @@ -233,6 +236,7 @@ static const char *get_routes(const tal_t *ctx, double base_fee_penalty; u32 prob_cost_factor, mu; const char *ret; + bool zero_cost; if (gossmap_refresh(askrene->gossmap, NULL)) { /* FIXME: gossmap_refresh callbacks to we can update in place */ @@ -246,21 +250,33 @@ static const char *get_routes(const tal_t *ctx, rq->layers = tal_arr(rq, const struct layer *, 0); rq->capacities = tal_dup_talarr(rq, fp16_t, askrene->capacities); + /* If we're told to zerocost local channels, then make sure that's done + * in local mods as well. */ + zero_cost = have_layer(layers, "auto.sourcefree") + && node_id_eq(source, &askrene->my_id); + /* Layers don't have to exist: they might be empty! */ for (size_t i = 0; i < tal_count(layers); i++) { - struct layer *l = find_layer(askrene, layers[i]); - if (!l) - continue; + const struct layer *l = find_layer(askrene, layers[i]); + if (!l) { + if (local_layer && streq(layers[i], "auto.localchans")) { + plugin_log(plugin, LOG_DBG, "Adding auto.localchans"); + l = local_layer; + } else + continue; + } tal_arr_expand(&rq->layers, l); /* FIXME: Implement localmods_merge, and cache this in layer? */ - layer_add_localmods(l, rq->gossmap, localmods); + layer_add_localmods(l, rq->gossmap, zero_cost, localmods); /* Clear any entries in capacities array if we * override them (incl local channels) */ layer_clear_overridden_capacities(l, askrene->gossmap, rq->capacities); } + /* This does not see local mods! If you add local channel in a layer, it won't + * have costs zeroed out here. */ if (have_layer(layers, "auto.sourcefree")) add_free_source(plugin, askrene->gossmap, localmods, source); @@ -460,6 +476,7 @@ struct getroutes_info { static struct command_result *do_getroutes(struct command *cmd, struct gossmap_localmods *localmods, + const struct layer *local_layer, const struct getroutes_info *info) { const char *err; @@ -471,7 +488,7 @@ static struct command_result *do_getroutes(struct command *cmd, err = get_routes(cmd, cmd->plugin, info->source, info->dest, *info->amount, *info->maxfee, *info->finalcltv, - info->layers, localmods, + info->layers, localmods, local_layer, &routes, &amounts, &probability); if (err) return command_fail(cmd, PAY_ROUTE_NOT_FOUND, "%s", err); @@ -501,6 +518,55 @@ static struct command_result *do_getroutes(struct command *cmd, return command_finished(cmd, response); } +static void add_localchan(struct gossmap_localmods *mods, + const struct node_id *self, + const struct node_id *peer, + const struct short_channel_id_dir *scidd, + struct amount_msat htlcmin, + struct amount_msat htlcmax, + struct amount_msat spendable, + struct amount_msat fee_base, + u32 fee_proportional, + u32 cltv_delta, + bool enabled, + const char *buf UNUSED, + const jsmntok_t *chantok UNUSED, + struct layer *local_layer) +{ + gossmod_add_localchan(mods, self, peer, scidd, htlcmin, htlcmax, + spendable, fee_base, fee_proportional, cltv_delta, enabled, + buf, chantok, local_layer); + + /* Known capacity on local channels (ts = max) */ + layer_update_constraint(local_layer, scidd, CONSTRAINT_MIN, UINT64_MAX, spendable); + layer_update_constraint(local_layer, scidd, CONSTRAINT_MAX, UINT64_MAX, spendable); +} + +static struct command_result * +listpeerchannels_done(struct command *cmd, + const char *buffer, + const jsmntok_t *toks, + struct getroutes_info *info) +{ + struct layer *local_layer = new_temp_layer(info, "auto.localchans"); + struct gossmap_localmods *localmods; + bool zero_cost; + + /* If we're told to zerocost local channels, then make sure that's done + * in local mods as well. */ + zero_cost = have_layer(info->layers, "auto.sourcefree") + && node_id_eq(info->source, &get_askrene(cmd->plugin)->my_id); + + localmods = gossmods_from_listpeerchannels(cmd, + &get_askrene(cmd->plugin)->my_id, + buffer, toks, + zero_cost, + add_localchan, + local_layer); + + return do_getroutes(cmd, localmods, local_layer, info); +} + static struct command_result *json_getroutes(struct command *cmd, const char *buffer, const jsmntok_t *params) @@ -517,7 +583,17 @@ static struct command_result *json_getroutes(struct command *cmd, NULL)) return command_param_failed(); - return do_getroutes(cmd, gossmap_localmods_new(cmd), info); + if (have_layer(info->layers, "auto.localchans")) { + struct out_req *req; + + req = jsonrpc_request_start(cmd->plugin, cmd, + "listpeerchannels", + listpeerchannels_done, + forward_error, info); + return send_outreq(cmd->plugin, req); + } + + return do_getroutes(cmd, gossmap_localmods_new(cmd), NULL, info); } static struct command_result *json_askrene_reserve(struct command *cmd, @@ -823,6 +899,8 @@ static const char *init(struct plugin *plugin, plugin_err(plugin, "Could not load gossmap %s: %s", GOSSIP_STORE_FILENAME, strerror(errno)); askrene->capacities = get_capacities(askrene, askrene->plugin, askrene->gossmap); + rpc_scan(plugin, "getinfo", take(json_out_obj(NULL, NULL, NULL)), + "{id:%}", JSON_SCAN(json_to_node_id, &askrene->my_id)); plugin_set_data(plugin, askrene); plugin_set_memleak_handler(plugin, askrene_markmem); diff --git a/plugins/askrene/askrene.h b/plugins/askrene/askrene.h index 693e70ca7cc0..76abd2eae081 100644 --- a/plugins/askrene/askrene.h +++ b/plugins/askrene/askrene.h @@ -5,6 +5,7 @@ #include #include #include +#include struct gossmap_chan; @@ -26,6 +27,8 @@ struct askrene { struct reserve_hash *reserved; /* Compact cache of gossmap capacities */ fp16_t *capacities; + /* My own id */ + struct node_id my_id; }; /* Information for a single route query. */ diff --git a/plugins/askrene/layer.c b/plugins/askrene/layer.c index ea1296bac5c1..3991716dc988 100644 --- a/plugins/askrene/layer.c +++ b/plugins/askrene/layer.c @@ -85,9 +85,9 @@ struct layer { struct node_id *disabled_nodes; }; -struct layer *new_layer(struct askrene *askrene, const char *name) +struct layer *new_temp_layer(const tal_t *ctx, const char *name) { - struct layer *l = tal(askrene, struct layer); + struct layer *l = tal(ctx, struct layer); l->name = tal_strdup(l, name); l->local_channels = tal(l, struct local_channel_hash); @@ -96,6 +96,12 @@ struct layer *new_layer(struct askrene *askrene, const char *name) constraint_hash_init(l->constraints); l->disabled_nodes = tal_arr(l, struct node_id, 0); + return l; +} + +struct layer *new_layer(struct askrene *askrene, const char *name) +{ + struct layer *l = new_temp_layer(askrene, name); list_add(&askrene->layers, &l->list); return l; } @@ -296,11 +302,12 @@ void layer_add_disabled_node(struct layer *layer, const struct node_id *node) tal_arr_expand(&layer->disabled_nodes, *node); } -void layer_add_localmods(struct layer *layer, +void layer_add_localmods(const struct layer *layer, const struct gossmap *gossmap, + bool zero_cost, struct gossmap_localmods *localmods) { - struct local_channel *lc; + const struct local_channel *lc; struct local_channel_hash_iter lcit; /* First, disable all channels into blocked nodes (local updates @@ -337,14 +344,20 @@ void layer_add_localmods(struct layer *layer, gossmap_local_addchan(localmods, &lc->n1, &lc->n2, lc->scid, NULL); for (size_t i = 0; i < ARRAY_SIZE(lc->half); i++) { + u64 base, propfee, delay; if (!lc->half[i].enabled) continue; + if (zero_cost) { + base = propfee = delay = 0; + } else { + base = lc->half[i].base_fee.millisatoshis; /* Raw: gossmap */ + propfee = lc->half[i].proportional_fee; + delay = lc->half[i].delay; + } gossmap_local_updatechan(localmods, lc->scid, lc->half[i].htlc_min, lc->half[i].htlc_max, - lc->half[i].base_fee.millisatoshis /* Raw: gossmap */, - lc->half[i].proportional_fee, - lc->half[i].delay, + base, propfee, delay, true, i); } diff --git a/plugins/askrene/layer.h b/plugins/askrene/layer.h index 5fa45ffee7bd..7acea220c072 100644 --- a/plugins/askrene/layer.h +++ b/plugins/askrene/layer.h @@ -41,6 +41,9 @@ struct layer *find_layer(struct askrene *askrene, const char *name); /* Create new layer by name. */ struct layer *new_layer(struct askrene *askrene, const char *name); +/* New temporary layer (not in askrene's hash table) */ +struct layer *new_temp_layer(const tal_t *ctx, const char *name); + /* Get the name of the layer */ const char *layer_name(const struct layer *layer); @@ -87,9 +90,10 @@ const struct constraint *layer_update_constraint(struct layer *layer, u64 timestamp, struct amount_msat limit); -/* Add local channels from this layer */ -void layer_add_localmods(struct layer *layer, +/* Add local channels from this layer. zero_cost means set fees and delay to 0. */ +void layer_add_localmods(const struct layer *layer, const struct gossmap *gossmap, + bool zero_cost, struct gossmap_localmods *localmods); /* Remove constraints older then cutoff: returns num removed. */ diff --git a/tests/test_askrene.py b/tests/test_askrene.py index aa00c41d13ba..a0e4f33038b0 100644 --- a/tests/test_askrene.py +++ b/tests/test_askrene.py @@ -362,3 +362,51 @@ def test_getroutes_auto_sourcefree(node_factory): layers=[], maxfee_msat=100, finalcltv=99) + + +def test_getroutes_auto_localchans(node_factory): + """Test getroutes call with auto.localchans layer""" + # We get bad signature warnings, since our gossip is made up! + l1, l2 = node_factory.get_nodes(2, opts={'allow_warning': True}) + gsfile, nodemap = generate_gossip_store([GenChannel(0, 1, forward=GenChannel.Half(propfee=10000)), + GenChannel(1, 2, forward=GenChannel.Half(propfee=10000))], + nodeids=[l2.info['id']]) + + # Set up l1 with this as the gossip_store + l1.stop() + shutil.copy(gsfile.name, os.path.join(l1.daemon.lightning_dir, TEST_NETWORK, 'gossip_store')) + l1.start() + + # Now l1 beleives l2 has an entire network behind it. + scid12, _ = l1.fundchannel(l2, 10**6, announce_channel=False) + + # Cannot find a route unless we use local hints. + with pytest.raises(RpcError, match="Unknown source node {}".format(l1.info['id'])): + l1.rpc.getroutes(source=l1.info['id'], + destination=nodemap[2], + amount_msat=100000, + layers=[], + maxfee_msat=100000, + finalcltv=99) + + # This should work + check_getroute_paths(l1, + l1.info['id'], + nodemap[2], + 100000, + maxfee_msat=100000, + layers=['auto.localchans'], + paths=[[{'short_channel_id': scid12, 'amount_msat': 102012, 'delay': 99 + 6 + 6 + 6}, + {'short_channel_id': '0x1x0', 'amount_msat': 102010, 'delay': 99 + 6 + 6}, + {'short_channel_id': '1x2x1', 'amount_msat': 101000, 'delay': 99 + 6}]]) + + # This should get self-discount correct + check_getroute_paths(l1, + l1.info['id'], + nodemap[2], + 100000, + maxfee_msat=100000, + layers=['auto.localchans', 'auto.sourcefree'], + paths=[[{'short_channel_id': scid12, 'amount_msat': 102010, 'delay': 99 + 6 + 6}, + {'short_channel_id': '0x1x0', 'amount_msat': 102010, 'delay': 99 + 6 + 6}, + {'short_channel_id': '1x2x1', 'amount_msat': 101000, 'delay': 99 + 6}]])