diff --git a/examples/PickhardtPaymentsExample.ipynb b/examples/PickhardtPaymentsExample.ipynb index 6ec0161..0585ae0 100644 --- a/examples/PickhardtPaymentsExample.ipynb +++ b/examples/PickhardtPaymentsExample.ipynb @@ -8,7 +8,7 @@ "\n", "Example code demonstrating how to use the `pickhardtpayments` package in python. You need to install the library via `pip install pickhardtpayments` or you can download the full source code at https://ln.rene-pickhardt.de or a copy from github at: https://www.github.com/renepickhardt/pickhardtpayments\n", "\n", - "Of course you can use the classes int the library to create your own async payment loop or you could exchange the Oracle to talk to the actual Lightning network by wrapping against your favourite node implementation and exposing the the `send_onion` call. \n", + "Of course you can use the classes in the library to create your own async payment loop or you could exchange the Oracle to talk to the actual Lightning network by wrapping against your favourite node implementation and exposing the the `send_onion` call. \n", "\n", "This example assumes a randomly generated Oracle to conduct payments in a simulated way. For this you will need an actual channelgraph which you can get for example with `lightning-cli listchannels > listchannels20220412.json`\n", "\n", @@ -39,14 +39,14 @@ "#Carsten Otto's public node key\n", "C_OTTO = \"027ce055380348d7812d2ae7745701c9f93e70c1adeb2657f053f91df4f2843c71\"\n", "\n", - "#we first need to import the chanenl graph from c-lightning jsondump\n", + "#we first need to import the chanenl graph from core lightning jsondump\n", "#you can get your own data set via:\n", "# $: lightning-cli listchannels > listchannels20220412.json\n", "# alternatively you can go to https://ln.rene-pickhardt.de to find a data dump\n", "channel_graph = ChannelGraph(\"listchannels20220412.json\")\n", "\n", - "#we now create ourself an Oracle. This is Simulated Network that assumes unformly distribution \n", - "#of the liquidity for the channels on the `channel_graph`. Of course one could create ones own \n", + "#we now create an Oracle. This is a Simulated Network that assumes uniform distribution \n", + "#of the liquidity for the channels on the `channel_graph`. Of course one could create one's own \n", "#oracle (for example a wrapper to an existing lightning network node / implementation)\n", "oracle_lightning_network = OracleLightningNetwork(channel_graph)" ] @@ -66,7 +66,7 @@ ], "source": [ "# Since we randomly generated our Oracle but also since we control it we can compute\n", - "# The maximum possible amout that can be payed between two nodes\n", + "# The maximum possible amount that can be payed between two nodes\n", "maximum_payable_amount =oracle_lightning_network.theoretical_maximum_payable_amount(RENE,C_OTTO,1000)\n", "print(maximum_payable_amount, \"sats would be possible on this oracle to deliver if including 1 sat basefee channels\")\n" ] @@ -86,7 +86,7 @@ ], "source": [ "#Of course we want to restrict ourselves to the zeroBaseFee part of the network.\n", - "#Therefor we compute the theoretical maximum payable amount for that subgraph\n", + "#Therefore we compute the theoretical maximum payable amount for that subgraph\n", "maximum_payable_amount =oracle_lightning_network.theoretical_maximum_payable_amount(RENE,C_OTTO,0)\n", "print(maximum_payable_amount, \"sats possible on this oracle on the zeroBaseFeeGraph\")" ] @@ -307,16 +307,16 @@ } ], "source": [ - "# We chose an amount that is 50% of half the theoretic maximum to demonstrate the the\n", + "# We choose an amount that is half the theoretical maximum to demonstrate the\n", "# minimum cost flow solver with Bayesian updates on the Uncertainty Network finds the\n", "# liquidity rather quickly\n", "tested_amount = int(maximum_payable_amount/2)\n", "\n", - "# From the channel graph we can derrive our initial Uncertainty Network which is the main data structure\n", + "# From the channel graph we can derive our initial Uncertainty Network which is the main data structure\n", "# that we maintain in order to deliver sats from one node to another\n", "uncertainty_network = UncertaintyNetwork(channel_graph)\n", "\n", - "#we create ourselves a payment session which in this case operates by sending out the onions\n", + "#we create a payment session which in this case operates by sending out the onions\n", "#sequentially \n", "payment_session = SyncSimulatedPaymentSession(oracle_lightning_network, \n", " uncertainty_network,\n", @@ -335,7 +335,7 @@ "source": [ "## Optimizing for Fees\n", "\n", - "controlling mu we can decide how much we wish to focuse on lower fees. However we will see that it will be much harder to deliver the same amount in the sense that we need to send out more onions and also have more failed attampts. Consiquantly we expect to need more time." + "controlling mu we can decide how much we wish to focus on lower fees. However ,we will see that it will be much harder to deliver the same amount in the sense that we need to send out more onions and also have more failed attampts. Consequently we expect to need more time." ] }, { diff --git a/examples/basicexample.py b/examples/basicexample.py index 8823ad8..dcf6b7b 100644 --- a/examples/basicexample.py +++ b/examples/basicexample.py @@ -4,7 +4,7 @@ from pickhardtpayments.SyncSimulatedPaymentSession import SyncSimulatedPaymentSession -#we first need to import the chanenl graph from c-lightning jsondump +#we first need to import the channel graph from core lightning jsondump #you can get your own data set via: # $: lightning-cli listchannels > listchannels20220412.json # alternatively you can go to https://ln.rene-pickhardt.de to find a data dump @@ -12,7 +12,7 @@ uncertainty_network = UncertaintyNetwork(channel_graph) oracle_lightning_network = OracleLightningNetwork(channel_graph) -#we create ourselves a payment session which in this case operates by sending out the onions +#we create a payment session which in this case operates by sending out the onions #sequentially payment_session = SyncSimulatedPaymentSession(oracle_lightning_network, uncertainty_network, diff --git a/pickhardtpayments/Channel.py b/pickhardtpayments/Channel.py index 055face..eb67fcd 100644 --- a/pickhardtpayments/Channel.py +++ b/pickhardtpayments/Channel.py @@ -1,7 +1,7 @@ -class ChannelFields: +class ChannelFields(): """ These are the values describing public data about channels that is either available - via gossip or via the Bitcoin Blockchain. Their format is taken from the c-lighting + via gossip or via the Bitcoin Blockchain. Their format is taken from the core lighting API. If you use a different implementation I suggest to write a wrapper around the `ChannelFields` and `Channel` class """ @@ -21,12 +21,12 @@ class ChannelFields: SHORT_CHANNEL_ID = 'short_channel_id' -class Channel: +class Channel(): """ Stores the public available information of a channel. The `Channel` Class is intended to be read only and internally stores - the data from c-lightning's `lightning-cli listchannels` command as a json. + the data from core lightning's `lightning-cli listchannels` command as a json. If you retrieve data from a different implementation I suggest to overload the constructor and transform the information into the given json format """ diff --git a/pickhardtpayments/ChannelGraph.py b/pickhardtpayments/ChannelGraph.py index 4751c5b..578b60b 100644 --- a/pickhardtpayments/ChannelGraph.py +++ b/pickhardtpayments/ChannelGraph.py @@ -22,7 +22,7 @@ def _get_channel_json(self, filename: str): def __init__(self, lightning_cli_listchannels_json_file: str): """ - Importing the channel_graph from c-lightning listchannels command the file can be received by + Importing the channel_graph from core lightning listchannels command the file can be received by #$ lightning-cli listchannels > listchannels.json """ diff --git a/pickhardtpayments/OracleChannel.py b/pickhardtpayments/OracleChannel.py index fce9178..bb1d7a9 100644 --- a/pickhardtpayments/OracleChannel.py +++ b/pickhardtpayments/OracleChannel.py @@ -5,7 +5,7 @@ class OracleChannel(Channel): """ - An OracleChannel us used in experiments and Simulations to form the (Oracle)LightningNetwork. + An OracleChannel is used in experiments and Simulations to form the (Oracle)LightningNetwork. It contains a ground truth about the Liquidity of a channel """ @@ -24,8 +24,8 @@ def actual_liquidity(self): """ Tells us the actual liquidity according to the oracle. - This is usful for experiments but must of course not be used in routing and is also - not a vailable if mainnet remote channels are being used. + This is useful for experiments but must of course not be used in routing and is also + not available if mainnet remote channels are being used. """ return self._actual_liquidity diff --git a/pickhardtpayments/SyncSimulatedPaymentSession.py b/pickhardtpayments/SyncSimulatedPaymentSession.py index 6359a27..917e903 100644 --- a/pickhardtpayments/SyncSimulatedPaymentSession.py +++ b/pickhardtpayments/SyncSimulatedPaymentSession.py @@ -1,52 +1,26 @@ -""" -SyncSimulatedPaymentSession.py -==================================== -The core module of the pickhardt payment project. -An example payment is executed and statistics are run. -""" - -import logging -import sys -from typing import List - -from .Attempt import Attempt, AttemptStatus -from .Payment import Payment from .UncertaintyNetwork import UncertaintyNetwork from .OracleLightningNetwork import OracleLightningNetwork - from ortools.graph import pywrapgraph + +from typing import List import time import networkx as nx -DEFAULT_BASE_THRESHOLD = 0 - -def set_logger(): - # Set Logger - logger = logging.getLogger() - logger.setLevel(logging.DEBUG) - formatter = logging.Formatter('%(asctime)s.%(msecs)03d | %(levelname)s | %(message)s', datefmt='%H:%M:%S') - stdout_handler = logging.StreamHandler(sys.stdout) - stdout_handler.setLevel(logging.DEBUG) - stdout_handler.setFormatter(formatter) - file_handler = logging.FileHandler('pickhardt_pay.log') - file_handler.setLevel(logging.DEBUG) - file_handler.setFormatter(formatter) - logger.addHandler(file_handler) - logger.addHandler(stdout_handler) +DEFAULT_BASE_THRESHOLD = 0 -class SyncSimulatedPaymentSession: +class SyncSimulatedPaymentSession(): """ - A PaymentSession is used to create the min cost flow problem from the UncertaintyNetwork + A PaymentSesssion is used to create the min cost flow problem from the UncertaintyNetwork This happens by adding several parallel arcs coming from the piece wise linearization of the UncertaintyChannel to the min_cost_flow object. - The main API call ist `pickhardt_pay` which invokes a sequential loop to conduct trial and error - attempts. The loop could easily send out all onions concurrently but this does not make sense + The main API call is `pickhardt_pay` which invokes a sequential loop to conduct trial and error + attempts. The loop could easily send out all onions concurrently but this does not make sense against the simulated OracleLightningNetwork. """ @@ -63,7 +37,8 @@ def _prepare_integer_indices_for_nodes(self): """ necessary for the OR-lib by google and the min cost flow solver - let's initialize the look-up tables for node_ids to integers from [0,...,#number of nodes] + + let's initialize the look up tables for node_ids to integers from [0,...,#number of nodes] this is necessary because of the API of the Google Operations Research min cost flow solver """ self._mcf_id = {} @@ -72,16 +47,14 @@ def _prepare_integer_indices_for_nodes(self): self._mcf_id[node_id] = k self._node_key[k] = node_id - def _prepare_mcf_solver(self, src, dest, amt: int = 1, mu: int = 100_000_000, - base_fee: int = DEFAULT_BASE_THRESHOLD): + def _prepare_mcf_solver(self, src, dest, amt: int = 1, mu: int = 100_000_000, base_fee: int = DEFAULT_BASE_THRESHOLD): """ computes the uncertainty network given our prior belief and prepares the min cost flow solver - This function can define a value for mu to control how heavily we combine the uncertainty cost and fees Also - the function supports only taking channels into account that don't charge a base_fee higher or equal to `base` + This function can define a value for \mu to control how heavily we combine the uncertainty cost and fees + Also the function supports only taking channels into account that don't charge a base_fee higher or equal to `base` - returns the instantiated min_cost_flow object from the Google OR-lib that contains the piecewise linearized - problem + returns the instantiated min_cost_flow object from the google OR-lib that contains the piecewise linearized problem """ self._min_cost_flow = pywrapgraph.SimpleMinCostFlow() self._arc_to_channel = {} @@ -91,7 +64,7 @@ def _prepare_mcf_solver(self, src, dest, amt: int = 1, mu: int = 100_000_000, if channel.base_fee > base_fee: continue # FIXME: Remove Magic Number for pruning - # Prune channels away that have too low success probability! This is a huge runtime boost + # Prune channels away thay have too low success probability! This is a huge runtime boost # However the pruning would be much better to work on quantiles of normalized cost # So as soon as we have better Scaling, Centralization and feature engineering we can # probably have a more focused pruning @@ -128,20 +101,20 @@ def _next_hop(self, path): The path is a list of node ids. Each call returns a tuple src, dest of an edge in the path """ for i in range(1, len(path)): - src = path[i - 1] + src = path[i-1] dest = path[i] - yield src, dest + yield (src, dest) def _make_channel_path(self, G: nx.MultiDiGraph, path: List[str]): """ - network x returns a path as a list of node_ids. However, we need a list of `UncertaintyChannels` - Since the graph has parallel edges it is quite some work to get the actual channels that the + network x returns a path as a list of node_ids. However we need a list of `UncertaintyChannels` + Since the graph has parallel edges it is quite some work to get the actual channels that the min cost flow solver produced """ channel_path = [] - bottleneck = 2 ** 63 + bottleneck = 2**63 for src, dest in self._next_hop(path): - w = 2 ** 63 + w = 2**63 c = None flow = 0 for sid in G[src][dest].keys(): @@ -156,17 +129,19 @@ def _make_channel_path(self, G: nx.MultiDiGraph, path: List[str]): return channel_path, bottleneck - def _dissect_flow_to_paths(self, s, d): + def _disect_flow_to_paths(self, s, d): """ - A standard algorithm to dissect a flow into several paths. + A standard algorithm to disect a flow into several paths. - FIXME: Note that this dissection while accurate is probably not optimal in practise. + FIXME: Note that this dissection while accurate is probably not optimal in practice. As noted in our Probabilistic payment delivery paper the payment process is a bernoulli trial and I assume it makes sense to dissect the flow into paths of similar likelihood to make most progress but this is a mere conjecture at this point. I expect quite a bit of research will be necessary to resolve this issue. """ - # first collect all linearized edges which are assigned a non-zero flow put them into a networkx graph + total_flow = {} + + # first collect all linearized edges which are assigned a non zero flow put them into a networkx graph G = nx.MultiDiGraph() for i in range(self._min_cost_flow.NumArcs()): flow = self._min_cost_flow.Flow(i) # *QUANTIZATION @@ -179,21 +154,22 @@ def _dissect_flow_to_paths(self, s, d): G[src][dest][channel.short_channel_id]["flow"] += flow else: # FIXME: cost is not reflecting exactly the piecewise linearization - # Probably not such a big issue as we just dissect flow + # Probably not such a big issue as we just disect flow G.add_edge(src, dest, key=channel.short_channel_id, flow=flow, channel=channel, weight=channel.combined_linearized_unit_cost()) used_flow = 1 - attempts = [] + channel_paths = [] # allocate flow to shortest / cheapest paths from src to dest as long as this is possible - # decrease flow along those edges. This is a standard mechanism to dissect a flow into paths + # decrease flow along those edges. This is a standard mechanism to disect a flow int paths while used_flow > 0: + path = None try: path = nx.shortest_path(G, s, d) except: break channel_path, used_flow = self._make_channel_path(G, path) - attempts.append(Attempt(channel_path, used_flow)) + channel_paths.append((channel_path, used_flow)) # reduce the flow from the selected path for pos, hop in enumerate(self._next_hop(path)): @@ -202,25 +178,25 @@ def _dissect_flow_to_paths(self, s, d): G[src][dest][channel.short_channel_id]["flow"] -= used_flow if G[src][dest][channel.short_channel_id]["flow"] == 0: G.remove_edge(src, dest, key=channel.short_channel_id) - return attempts + return channel_paths - def _generate_candidate_paths(self, src, dest, amt: int, mu: int = 100_000_000, - base: int = DEFAULT_BASE_THRESHOLD): + def _generate_candidate_paths(self, src, dest, amt, mu: int = 100_000_000, base: int = DEFAULT_BASE_THRESHOLD): """ - computes the optimal payment split to deliver `amt` from `src` to `dest` and updates our belief about the - liquidity + computes the optimal payment split to deliver `amt` from `src` to `dest` and updates our belief about the liquidity This is one step within the payment loop. - Returns the residual amount of the `amt` that could not be delivered and the paid fees + Returns the residual amount of the `amt` that could ne be delivered and the paid fees (on a per channel base not including fees for downstream fees) for the delivered amount - the function also prints some results on statistics about the paths of the flow to stdout. + the function also prints some results an statistics about the paths of the flow to stdout. """ + # First we prepare the min cost flow by getting arcs from the uncertainty network self._prepare_mcf_solver(src, dest, amt, mu, base) + start = time.time() - # print("solving mcf...") + #print("solving mcf...") status = self._min_cost_flow.Solve() if status != self._min_cost_flow.OPTIMAL: @@ -228,35 +204,53 @@ def _generate_candidate_paths(self, src, dest, amt: int, mu: int = 100_000_000, print(f'Status: {status}') exit(1) - attempts_in_round = self._dissect_flow_to_paths(src, dest) + paths = self._disect_flow_to_paths(src, dest) end = time.time() - return attempts_in_round, end - start + return paths, end-start - def _attempt_payments(self, attempts: list[Attempt]): + def _estimate_payment_statistics(self, paths): + """ + estimates the success probability of paths and computes fees (without paying downstream fees) + + @returns the statistics in the `payments` dictionary + """ + # FIXME: Decide if an `Payments` or `Attempt` class shall be used + payments = {} + # compute fees and probabilities of candidate paths for evaluation + for i, onion in enumerate(paths): + path, amount = onion + fee, probability = self._uncertainty_network.get_features_of_candidate_path( + path, amount) + payments[i] = { + "routing_fee": fee, "probability": probability, "path": path, "amount": amount} + + # to correctly compute conditional probabilities of non disjoint paths in the same set of paths + self._uncertainty_network.allocate_amount_on_path(path, amount) + + # remove allocated amounts for all planned onions before doing actual attempts + for key, attempt in payments.items(): + self._uncertainty_network.allocate_amount_on_path( + attempt["path"], -attempt["amount"]) + + return payments + + def _attempt_payments(self, payments): """ we attempt all planned payments and test the success against the oracle in particular this method changes - depending on the outcome of each payment - our belief about the uncertainty - in the UncertaintyNetwork. - successful onions are collected to be transacted on the OracleNetwork if complete payment can be delivered + in the UncertaintyNetwork """ # test actual payment attempts - for attempt in attempts: + for key, attempt in payments.items(): success, erring_channel = self._oracle.send_onion( - attempt.path, attempt.amount) + attempt["path"], attempt["amount"]) + payments[key]["success"] = success + payments[key]["erring_channel"] = erring_channel if success: - # TODO: let this happen in Payment class? Or in Attempt class - with status change as settlement - attempt.status = AttemptStatus.ARRIVED - # handling amounts on path happens in Attempt Class. self._uncertainty_network.allocate_amount_on_path( - attempt.path, attempt.amount) + attempt["path"], attempt["amount"]) - - # unnecessary, because information is in attempt (Status INFLIGHT) - # settled_onions.append(payments[key]) - else: - attempt.status = AttemptStatus.FAILED - - def _evaluate_attempts(self, payment: Payment): + def _evaluate_attempts(self, payments): """ helper function to collect statistics about attempts and print them @@ -265,47 +259,63 @@ def _evaluate_attempts(self, payment: Payment): total_fees = 0 paid_fees = 0 residual_amt = 0 + number_failed_paths = 0 expected_sats_to_deliver = 0 amt = 0 - arrived_attempts = [] - failed_attempts = [] - print("\nStatistics about {} candidate onions:\n".format(len(payment.attempts))) + print("\nStatistics about {} candidate onions:\n".format(len(payments))) + + has_failed_attempt = False print("successful attempts:") print("--------------------") - for arrived_attempt in payment.filter_attempts(AttemptStatus.ARRIVED): - amt += arrived_attempt.amount - total_fees += arrived_attempt.routing_fee / 1000. - expected_sats_to_deliver += arrived_attempt.probability * arrived_attempt.amount - print(" p = {:6.2f}% amt: {:9} sats hops: {} ppm: {:5}".format( - arrived_attempt.probability * 100, arrived_attempt.amount, len(arrived_attempt.path), - int(arrived_attempt.routing_fee * 1000 / arrived_attempt.amount))) - paid_fees += arrived_attempt.routing_fee - - print("\nfailed attempts:") - print("----------------") - for failed_attempt in payment.filter_attempts(AttemptStatus.FAILED): - amt += failed_attempt.amount - total_fees += failed_attempt.routing_fee / 1000. - expected_sats_to_deliver += failed_attempt.probability * failed_attempt.amount + for attempt in payments.values(): + success = attempt["success"] + if success == False: + has_failed_attempt = True + continue + fee = attempt["routing_fee"] / 1000. + probability = attempt["probability"] + path = attempt["path"] + amount = attempt["amount"] + amt += amount + total_fees += fee + expected_sats_to_deliver += probability * amount print(" p = {:6.2f}% amt: {:9} sats hops: {} ppm: {:5}".format( - failed_attempt.probability * 100, failed_attempt.amount, len(failed_attempt.path), - int(failed_attempt.routing_fee * 1000 / failed_attempt.amount))) - residual_amt += failed_attempt.amount + probability*100, amount, len(path), int(fee*1000_000/amount))) + paid_fees += fee + + if has_failed_attempt: + print("\nfailed attempts:") + print("----------------") + for attempt in payments.values(): + success = attempt["success"] + if success: + continue + fee = attempt["routing_fee"] / 1000. + probability = attempt["probability"] + path = attempt["path"] + amount = attempt["amount"] + amt += amount + total_fees += fee + expected_sats_to_deliver += probability * amount + print(" p = {:6.2f}% amt: {:9} sats hops: {} ppm: {:5} ".format( + probability*100, amount, len(path), int(fee*1000_000/amount))) + number_failed_paths += 1 + residual_amt += amount print("\nAttempt Summary:") print("=================") - print("\nTried to deliver \t{:10} sats".format(amt)) - fraction = expected_sats_to_deliver * 100. / amt + print("\nTried to deliver {:10} sats".format(amt)) + fraction = expected_sats_to_deliver*100./amt print("expected to deliver {:10} sats \t({:4.2f}%)".format( int(expected_sats_to_deliver), fraction)) - fraction = (amt - residual_amt) * 100. / amt - print("actually delivered \t{:10} sats \t({:4.2f}%)".format( - amt - residual_amt, fraction)) - print("deviation: \t\t{:4.2f}".format( - (amt - residual_amt) / (expected_sats_to_deliver + 1))) - print("planned_fee: \t{:8.3f} sat".format(total_fees)) - print("paid fees: \t\t{:8.3f} sat".format(paid_fees)) - return residual_amt, paid_fees, len(payment.attempts), len(failed_attempts) + fraction = (amt-residual_amt)*100./(amt) + print("actually deliverd {:10} sats \t({:4.2f}%)".format( + amt-residual_amt, fraction)) + print("deviation: {:4.2f}".format( + (amt-residual_amt)/(expected_sats_to_deliver+1))) + print("planned_fee: {:8.3f} sat".format(total_fees)) + print("paid fees: {:8.3f} sat".format(paid_fees)) + return residual_amt, paid_fees, len(payments), number_failed_paths def forget_information(self): """ @@ -320,81 +330,60 @@ def activate_network_wide_uncertainty_reduction(self, n): self._uncertainty_network.activate_network_wide_uncertainty_reduction( n, self._oracle) - def pickhardt_pay(self, src, dest, amt, mu=1, base=DEFAULT_BASE_THRESHOLD): + def pickhardt_pay(self, src, dest, amt, mu=1, base=0): """ conduct one experiment! might need to call oracle.reset_uncertainty_network() first - I could not put it here as some experiments require sharing of liquidity information + I could not put it here as some experiments require sharing of liqudity information """ - - set_logger() - logging.info('*** new pickhardt payment ***') - - # Setup entropy_start = self._uncertainty_network.entropy() + start = time.time() + full_amt = amt cnt = 0 total_fees = 0 + number_number_of_onions = 0 total_number_failed_paths = 0 - # Initialise Payment - # currently with underscore to not mix up with existing variable 'payment' - payment = Payment(src, dest, amt) - # This is the main payment loop. It is currently blocking and synchronous but may be - # implemented in a concurrent way. Also, we stop after 10 rounds which is pretty arbitrary - # a better stop criteria would be if we compute infeasible flows or if the probabilities - # are too low or residual amounts decrease to slowly + # implemented in a concurrent way. Also we stop after 10 rounds which is pretty arbitrary + # a better stop criteria would be if we compute infeasable flows or if the probabilities + # are to low or residual amounts decrease to slowly while amt > 0 and cnt < 10: - print("Round number: ", cnt + 1) + print("Round number: ", cnt+1) print("Try to deliver", amt, "satoshi:") - sub_payment = Payment(payment.sender, payment.receiver, amt) - # transfer to a min cost flow problem and run the solver - # paths is the lists of channels, runtime the time it took to calculate all candidates in this round - paths, runtime = self._generate_candidate_paths(payment.sender, payment.receiver, amt, mu, base) - sub_payment.add_attempts(paths) + # transfer to a min cost flow problem and rund the solver + paths, runtime = self._generate_candidate_paths( + src, dest, amt, mu, base) - # make attempts, try to send onion and register if success or not - # update our information about the UncertaintyNetwork - self._attempt_payments(sub_payment.attempts) + # compute some statistics about candidate paths + payments = self._estimate_payment_statistics(paths) + + # matke attempts and update our information about the UncertaintyNetwork + self._attempt_payments(payments) # run some simple statistics and depict them amt, paid_fees, num_paths, number_failed_paths = self._evaluate_attempts( - sub_payment) - + payments) print("Runtime of flow computation: {:4.2f} sec ".format(runtime)) print("\n================================================================\n") + number_number_of_onions += num_paths total_number_failed_paths += number_failed_paths total_fees += paid_fees cnt += 1 - - # add attempts of sub_payment to payment - payment.add_attempts(sub_payment.attempts) - - # When residual amount is 0 / enough successful onions have been found, then settle payment. Else drop onions. - if amt == 0: - for onion in payment.filter_attempts(AttemptStatus.ARRIVED): - try: - self._oracle.settle_payment(onion.path, onion.amount) - onion.status = AttemptStatus.SETTLED - except Exception as e: - print(e) - return -1 - payment.successful = True - payment.end_time = time.time() - + end = time.time() entropy_end = self._uncertainty_network.entropy() print("SUMMARY:") print("========") - print("Rounds of mcf-computations:\t", cnt) - print("Number of attempts made:\t", len(payment.attempts)) - print("Number of failed attempts:\t", len(payment.filter_attempts(AttemptStatus.FAILED))) + print("Rounds of mcf-computations: ", cnt) + print("Number of onions sent: ", number_number_of_onions) + print("Number of failed onions: ", total_number_failed_paths) print("Failure rate: {:4.2f}% ".format( - len(payment.filter_attempts(AttemptStatus.FAILED)) * 100. / len(payment.attempts))) - print("total Payment lifetime (including inefficient memory management): {:4.3f} sec".format( - payment.end_time - payment.start_time)) - print("Learnt entropy: {:5.2f} bits".format(entropy_start - entropy_end)) - print("fee for settlement of delivery: {:8.3f} sat --> {} ppm".format( - payment.settlement_fees/1000, int(payment.settlement_fees * 1000 / payment.total_amount))) + total_number_failed_paths*100./number_number_of_onions)) + print("total runtime (including inefficient memory managment): {:4.3f} sec".format( + end-start)) + print("Learnt entropy: {:5.2f} bits".format(entropy_start-entropy_end)) + print("Fees for successfull delivery: {:8.3f} sat --> {} ppm".format( + total_fees, int(total_fees*1000*1000/full_amt))) print("used mu:", mu) diff --git a/pickhardtpayments/UncertaintyChannel.py b/pickhardtpayments/UncertaintyChannel.py index 826c0af..890fb03 100644 --- a/pickhardtpayments/UncertaintyChannel.py +++ b/pickhardtpayments/UncertaintyChannel.py @@ -13,9 +13,9 @@ class UncertaintyChannel(Channel): UncertaintyNetwork. Since we optimize for reliability via a probability estimate for liquidity that is based - on the capacity of the channel the class contains the `capacity` as seen in the funding tx output. + on the capacity of the channel, the class contains the `capacity` as seen in the funding tx output. - As we also optimize for fees and want to be able to compute the fees of a flow the classe + As we also optimize for fees and want to be able to compute the fees of a flow ,the class contains information for the feerate (`ppm`) and the base_fee (`base`). Most importantly the class stores our belief about the liquidity information of a channel. @@ -91,7 +91,7 @@ def allocate_amount(self, amt: int): # FIXME: store timestamps when using setters so that we know when we learnt our belief def forget_information(self): """ - resets the information that we belief to have about the channel. + resets the information that we believe to have about the channel. """ self.min_liquidity = 0 self.max_liquidity = self.capacity diff --git a/readme.md b/readme.md index df97158..9f6fd51 100644 --- a/readme.md +++ b/readme.md @@ -4,9 +4,9 @@ The `pickhardtpayments` package is a collection of classes and interfaces that h ## What are Pickhardt Payments? -Pickhardt Payments are the method of deliverying satoshis from on Lightning network Node to another by using [probabilistic payment delivery](https://arxiv.org/abs/2103.08576) in a round based `payment loop` that updeates our `belief` of the remote `liquidity` in the `Uncertainty Network` and generates [optimally reliable and cheap payment flows](https://arxiv.org/abs/2107.05322) in every round by solving a [piece wise linearized min integer cost flow problem](https://github.com/renepickhardt/mpp-splitter/blob/pickhardt-payments-simulation-dev/Minimal%20Linearized%20min%20cost%20flow%20example%20for%20MPP.ipynb) with a seperable cost function. +Pickhardt Payments are the method of deliverying satoshis from one Lightning network Node to another by using [probabilistic payment delivery](https://arxiv.org/abs/2103.08576) in a round based `payment loop` that updates our `belief` of the remote `liquidity` in the `Uncertainty Network` and generates [optimally reliable and cheap payment flows](https://arxiv.org/abs/2107.05322) in every round by solving a [piece wise linearized min integer cost flow problem](https://github.com/renepickhardt/mpp-splitter/blob/pickhardt-payments-simulation-dev/Minimal%20Linearized%20min%20cost%20flow%20example%20for%20MPP.ipynb) with a separable cost function. -As of now the two main features of the cost function are the `linearized_uncertainty_unit_cost` (effectively proportional to `1/channel_capacity`) and the `linearized_routing_unit_cost` (effectivly just the `ppm`). +As of now the two main features of the cost function are the `linearized_uncertainty_unit_cost` (effectively proportional to `1/channel_capacity`) and the `linearized_routing_unit_cost` (effectively just the `ppm`). ## Depenencies @@ -20,7 +20,7 @@ The dependencies can be found at: ## build and install -Onestep install is via pip by typing `pip install pickhardtpayments` to your command line +One step install is via pip by typing `pip install pickhardtpayments` to your command line If you want to build and install the library yourself you can do: @@ -43,7 +43,7 @@ from pickhardtpayments.OracleLightningNetwork import OracleLightningNetwork from pickhardtpayments.SyncSimulatedPaymentSession import SyncSimulatedPaymentSession -#we first need to import the chanenl graph from c-lightning jsondump +#we first need to import the chanenl graph from core lightning jsondump #you can get your own data set via: # $: lightning-cli listchannels > listchannels20220412.json # alternatively you can go to https://ln.rene-pickhardt.de to find a data dump @@ -51,7 +51,7 @@ channel_graph = ChannelGraph("listchannels20220412.json") uncertainty_network = UncertaintyNetwork(channel_graph) oracle_lightning_network = OracleLightningNetwork(channel_graph) -#we create ourselves a payment session which in this case operates by sending out the onions +#we create a payment session which in this case operates by sending out the onions #sequentially payment_session = SyncSimulatedPaymentSession(oracle_lightning_network, uncertainty_network,