diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..a7e90b7 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "subprojects/MinCostFlow"] + path = subprojects/MinCostFlow + url = https://github.com/Lagrang3/mincostflow.git diff --git a/benchmark/analize.py b/benchmark/analize.py new file mode 100644 index 0000000..fd413cf --- /dev/null +++ b/benchmark/analize.py @@ -0,0 +1,73 @@ +import json +import matplotlib.pyplot as plt +import numpy + +def open_json_data(filename): + with open(filename,'r') as f: + return json.load(f) + +ortool_data = open_json_data('ortools.json') +apath_data = open_json_data('augmenting_path.json') +cscaling_data = open_json_data('cost_scaling.json') + +def plot_success_rate(data): + x = [ int(i) for i in data.keys() ] + y = [ data[i]['nsuccess']/(data[i]['nsuccess']+data[i]['nfails']) for i in data.keys() ] + fig = plt.figure() + ax = fig.subplots() + ax.grid() + ax.set_title('Success rate') + ax.set_xlabel('amount (sats)') + ax.set_ylabel('success rate') + ax.loglog(x,y) + fig.savefig('plot_success.png') + plt.show() + +def plot_time(data): + fig=plt.figure() + ax=fig.subplots() + ax.grid() + ax.set_title('MCF timing') + ax.set_xlabel('amount (sats)') + ax.set_ylabel('time (ms)') + for k in data: + D = data[k] + xser = [ int(i) for i in D.keys() ] + tser = [ numpy.mean(D[i]['time_mcf'])*1000 for i in D.keys() ] + tmin = [ numpy.min(D[i]['time_mcf'])*1000 for i in D.keys() ] + tmax = [ numpy.max(D[i]['time_mcf'])*1000 for i in D.keys() ] + ax.fill_between(xser,tmin,tmax,alpha = 0.2) + ax.loglog(xser,tser,label=k) + ax.legend() + fig.savefig('plot_time.png') + plt.show() + #x = [ int(i) for i in data['ortools'].keys() ] + +def plot_totaltime(data): + fig=plt.figure() + ax=fig.subplots() + ax.grid() + ax.set_title('Total Payment Time') + ax.set_xlabel('amount (sats)') + ax.set_ylabel('time (ms)') + for k in data: + D = data[k] + xser = [ int(i) for i in D.keys() ] + tser = [ numpy.mean(D[i]['time_total'])*1000 for i in D.keys() ] + tmin = [ numpy.min(D[i]['time_total'])*1000 for i in D.keys() ] + tmax = [ numpy.max(D[i]['time_total'])*1000 for i in D.keys() ] + ax.fill_between(xser,tmin,tmax,alpha = 0.2) + ax.loglog(xser,tser,label=k) + ax.legend() + fig.savefig('plot_totaltime.png') + plt.show() + + +#print(ortool_data) +#print(apath_data) +#print(cscaling_data) + +plot_success_rate(ortool_data) +in_data = {"ortools" : ortool_data, "cost_scaling" : cscaling_data, "augmenting_path" : apath_data} +plot_time(in_data) +plot_totaltime(in_data) diff --git a/benchmark/benchmark.py b/benchmark/benchmark.py new file mode 100644 index 0000000..3284389 --- /dev/null +++ b/benchmark/benchmark.py @@ -0,0 +1,83 @@ +import random +import json + +#prefix='cost_scaling' +prefix='augmenting_path' +#prefix='ortools' +random.seed(64) + +from pickhardtpayments.ChannelGraph import ChannelGraph +from pickhardtpayments.UncertaintyNetwork import UncertaintyNetwork +from pickhardtpayments.OracleLightningNetwork import OracleLightningNetwork +from pickhardtpayments.SyncSimulatedPaymentSession import SyncSimulatedPaymentSession + + +#we first need to import the chanenl graph from c-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 +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 +#sequentially +payment_session = SyncSimulatedPaymentSession(oracle_lightning_network, + uncertainty_network, + mu = 0, + base = 0, + prune_network=False) + +#we need to make sure we forget all learnt information on the Uncertainty Nework + +net = uncertainty_network._channel_graph +nodes = net.nodes() +funds = {} +for n in nodes: + funds[n] = 0 + +for a,b,d in net.edges(data=True): + cap = d['channel'].capacity + funds[a] += cap + +# exit(0) + +def random_node_pairs(n_pairs,amt): + list_of_pairs = [] + for i in range(n_pairs): + while(True): + a,b = random.sample(nodes,2) + if min(funds[a],funds[b])>=amt: + if oracle_lightning_network.theoretical_maximum_payable_amount(a,b,base_fee=0)>=amt: + list_of_pairs.append((a,b)) + break + return list_of_pairs + +#we run the simulation of pickhardt payments and track all the results + +n_pairs = 10 +amount_list = [ 2**i for i in range(24,25)] +stat = {} + +for a in amount_list: + stat[a] = { 'time_mcf': [], 'time_total': [], 'nfails': 0 , 'nsuccess': 0 } + +# random_node_pairs(n_pairs) + +with open(prefix+'.log','w') as flog: + for amt in amount_list: + for (A,B) in random_node_pairs(n_pairs,amt): + print("trying a payment of",amt,"sats from",A,"to",B) + print("trying a payment of",amt,"sats from",A,"to",B, file=flog) + payment_session.forget_information() + try: + time_mcf,time_total = payment_session.pickhardt_pay(A,B, amt,log_out=flog) + stat[amt]['time_mcf'].append( time_mcf ) + stat[amt]['time_total'].append( time_total ) + stat[amt]['nsuccess'] += 1 + except: + stat[amt]['nfails'] +=1 + continue + +with open(prefix+'.json','w') as f: + json.dump(stat,f) diff --git a/c-documentation.md b/c-documentation.md new file mode 100644 index 0000000..8c6cf11 --- /dev/null +++ b/c-documentation.md @@ -0,0 +1,182 @@ +# C-API for Pickhardt Payments + +## Overview + +This document describes *Pickhardt payments* C library API. + +## Primitive data types + +Nodes ID are encoded as 33 bytes numbers. +``` +typedef char[33] nodeID_t; +``` + +Channels ID are encoded as 8 bytes numbers. +``` +typedef uint64_t channelID_t; +``` + +## Payment Session + +A `paymentSession` is an object that contains all the information known about the Lightning Network +from the initial knowledge of the public channels and the accumulated information +resulting from successful and failed attempts to forward payments while the session is in use. +``` +typedef struct paymentSession paymentSession; +``` + +To create and initialize a `paymentSession` the function `paymentSession_new` must be called. +This function returns a pointer to a newly allocated `paymentSession`. +This function returns `NULL` if the necessary memory couldn't be allocated. +``` +paymentSession* paymentSession_new(); +``` + +To cleanup and release the memory one needs to call `paymentSession_delete`. +This function does not fail. +``` +void paymentSession_delete(paymentSession* s); +``` + +Internally a Session represents the Lightning-Network graph of nodes and channels. +Initially a newly created Session has no knowled of any channel nor nodes, so one has to provide +this information by means of the function `paymentSession_addChannel`. +This function returns an error code that could have the values: +`PAYMENTSESSION_SUCCESS` if no errors occurred or +`PAYMENTSESSION_BADALLOC` if this function failed to allocate the memory for the new channel. +``` +int paymentSession_addChannel(paymentSession* s, + channelID_t short_channel_id, + nodeID_t Source, + nodeID_t Dest, + int64_t capacity, + int64_t fee_rate, + int64_t base_fee); +``` + +A channel can be removed by calling `paymentSession_rmChannel`. +This function returns an error code. +``` +int paymentSession_rmChannel(paymentSession* s, + channelID_t short_channel_id, + nodeID_t Source, + nodeID_t Dest); +``` + +A Session can be used to compute a multipart-payment with maximum probability of success based on +the knowledge of the Lightning-Network topology, channels capacity and previous knowledge of the +state of the liquidity in the channels. +By calling `paymentSession_optimizedPayment` one obtains a pointer to a `multiPartPayment` object +that contains a multipart-payment canditate based on the `amount` requested to be forwarded between +nodes `Source` and `Dest`. +This function returns `NULL` if the `paymentSession_optimizedPayment` fails. +The variable `status` contains an error code to log the cause of the failure. +``` +multiPartPayment* paymentSession_optimizedPayment(paymentSession* s, + nodeID_t Source, + nodeID_t Dest, + int64_t amount, + char* status); +``` + +Once an onion is sent to the Lightning-Network from the success or failure of this attempt we can +update our knowledge of the liquidity of the channels involved in that onion. That information can +be used by the Session to compute future payments attempts. +To update the knowledge of the Network one must call `paymentSession_updateKnowledge`. +This function returns an error code. +``` +int paymentSession_updateKnowledge(paymentSession* s, + int64_t short_channel_id, + nodeID_t Source, + nodeID_t Dest, + int64_t amount, + char status); +``` + +The accumulated knowledge in the Session can be erased by calling +`paymentSession_forgetInformation`. +This function returns an error code. +``` +int paymentSession_forgetInformation(paymentSession* s); +``` + +## Multipart Payments + +The function `paymentSession_optimizedPayment` produces a multipart payment (MPP) based on the +knowledge accumulated in the Session in use. The object that implements the concept of that MPP is +the `multiPartPayment`. We do not expose a constructor for this object in the public interface, +because it will always represent the result of an `paymentSession_optimizedPayment` and nothing +else. The `multiPartPayment` exposes an interface to query information about the MPP that the +Session has computed, therefore all of the `multiPartPayment_*` functions, except for the +destructor, do not modify the state of the MPP, the `multiPartPayment` is read-only. +``` +typedef struct multiPartPayment multiPartPayment; +``` + +Once a `multiPartPayment` object is no longer used, it can be freed by calling +`multiPartPayment_delete`. This function never fails. +``` +void multiPartPayment_delete(multiPartPayment* mpp); +``` + +`multiPartPayment` can be queried to get the information about the MPP information they encode. +To query the amount of satoshis this payment sends one calls `multiPartPayment_amount`. +This function never fails. +``` +int64_t multiPartPayment_amount(const multiPartPayment* mpp); +``` + +To query the source and the destination of the payment the functions +`multiPartPayment_source` and `multiPartPayment_destination` are called respectively. +These functions never fail. +``` +nodeID_t multiPartPayment_source(const multiPartPayment* mpp); +``` +``` +nodeID_t multiPartPayment_destination(const multiPartPayment* mpp); +``` + +Each MPP consist of a list of paths in the channels/nodes graph. +By calling `multiPartPayment_numParts` one can query for the number of paths that constitute the +MPP. +This functions never fails. +``` +uint64_t multiPartPayment_numParts(const multiPartPayment* mpp); +``` + +By calling `multiPartPayment_pathLengths` one can query the length of the individual paths that make +the MPP. The pointer `lengths` must be previously allocated to hold at least as many paths as the +MPP. +This function never fails. +``` +void multiPartPayment_pathLengths(const multiPartPayment* mpp, + uint64_t* lengths); +``` + +Similar to the `multiPartPayment_pathLengths`, the function `multiPartPayment_pathValues` queries +the MPP to obtain the amount of satoshis commited to each individual path. +The pointer `values` must be previously allocated to hold at least as many paths as the +MPP. +This function never fails. +``` +void multiPartPayment_pathValues(const multiPartPayment* mpp, + int64_t* values); +``` + +The function `multiPartPayment_pathChannels` writes the list of channels for every path in the MPP +to the input array `short_channel_id`, and for each of these channels a value of the `orientation` +is set either to `CHANNEL_FORWARD` or `CHANNEL_BACKWARD` to indicate the direction the liquidity is +transfered. If the satoshis are transfered from a node with lowest lexicographical order to the +other then the `orientation` is set to `CHANNEL_FORWARD`, otherwise it is `CHANNEL_BACKWARD`. +This function never fails. +``` +void multiPartPayment_pathChannels(const multiPartPayment* mpp, + int64_t* short_channel_id, + char* orientation); +``` + +## Example + +``` +// TODO +``` diff --git a/examples/basicexample.py b/examples/basicexample.py index dcf6b7b..bf1bf0b 100644 --- a/examples/basicexample.py +++ b/examples/basicexample.py @@ -16,6 +16,7 @@ #sequentially payment_session = SyncSimulatedPaymentSession(oracle_lightning_network, uncertainty_network, + mu=0,base=0, prune_network=False) #we need to make sure we forget all learnt information on the Uncertainty Nework @@ -29,4 +30,4 @@ C_OTTO = "027ce055380348d7812d2ae7745701c9f93e70c1adeb2657f053f91df4f2843c71" tested_amount = 10_000_000 #10 million sats -payment_session.pickhardt_pay(RENE, C_OTTO, tested_amount, mu=0, base=0) +payment_session.pickhardt_pay(RENE, C_OTTO, tested_amount) diff --git a/meson.build b/meson.build new file mode 100644 index 0000000..9d5d994 --- /dev/null +++ b/meson.build @@ -0,0 +1,11 @@ +project('pickhardtpayments','cpp','c', + default_options : ['cpp_std=c++17', + 'warning_level=3', + 'optimization=3'], + version: '0.0.0') + + +libMCF_proj = subproject('MinCostFlow') +libMCF_dep = libMCF_proj.get_variable('dep_mincostflow') +libMCF = libMCF_proj.get_variable('libmincostflow') + diff --git a/pickhardtpayments/SyncSimulatedPaymentSession.py b/pickhardtpayments/SyncSimulatedPaymentSession.py index 8eeab0d..21b8a64 100644 --- a/pickhardtpayments/SyncSimulatedPaymentSession.py +++ b/pickhardtpayments/SyncSimulatedPaymentSession.py @@ -13,11 +13,13 @@ from .Payment import Payment from .UncertaintyNetwork import UncertaintyNetwork from .OracleLightningNetwork import OracleLightningNetwork +from .UncertaintyChannel import DEFAULT_N -from ortools.graph import pywrapgraph +from MinCostFlow import MCFNetwork import time import networkx as nx +import sys DEFAULT_BASE_THRESHOLD = 0 @@ -52,27 +54,83 @@ class SyncSimulatedPaymentSession: def __init__(self, oracle: OracleLightningNetwork, uncertainty_network: UncertaintyNetwork, + mu = 1, base = 0, prune_network: bool = True): self._oracle = oracle self._uncertainty_network = uncertainty_network self._prune_network = prune_network - self._prepare_integer_indices_for_nodes() + self._mu = mu + self._base_fee_threshold = base + self._prepare_mcf_solver() - def _prepare_integer_indices_for_nodes(self): - """ - necessary for the OR-lib by google and the min cost flow solver + def _prepare_mcf_demands(self,src,dest,amt: int = 1): + # add amount to sending node + self._min_cost_flow.SetNodeSupply( + src, int(amt)) # /QUANTIZATION)) - 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 = {} - self._node_key = {} - for k, node_id in enumerate(self._uncertainty_network.network.nodes()): - 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): + # add -amount to recipient nods + self._min_cost_flow.SetNodeSupply( + dest, -int(amt)) # /QUANTIZATION)) + + def _mcf_channel_encode_part(self,channel,part: int=0): + direction = 0 + if channel.src>channel.dest: + direction = 1 + return direction + part*2 + + def _mcf_good_channel(self,channel): + # ignore channels with too large base fee + if channel.base_fee > self._base_fee_threshold: + return False + # FIXME: Remove Magic Number for pruning + # 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 + if self._prune_network and channel.success_probability(250_000) < 0.9: + return False + return True + + def _mcf_delete_channel(self,channel): + for part in range(DEFAULT_N): + index = self._min_cost_flow.RemoveArc(channel.short_channel_id, + self._mcf_channel_encode_part(channel,part)) + self._arc_to_channel.pop(index) + + def _mcf_update_channel(self,channel): + if not self._mcf_good_channel(channel): + self._mcf_delete_channel(channel) + return + # QUANTIZATION): + for part,(capacity, cost) in enumerate(channel.get_piecewise_linearized_costs(mu=self._mu)): + self._min_cost_flow.UpdateArc(channel.short_channel_id, + self._mcf_channel_encode_part(channel,part), + capacity, + cost) + + def _mcf_update_used_channels(self, attempts: List[Attempt]): + for attempt in attempts: + for channel in attempt.path: + self._mcf_update_channel(channel) + + def _mcf_new_arc(self,channel): + if not self._mcf_good_channel(channel): + return + cnt = 0 + # QUANTIZATION): + for part,(capacity, cost) in enumerate(channel.get_piecewise_linearized_costs(mu=self._mu)): + index = self._min_cost_flow.AddArc(channel.src, + channel.dest, + channel.short_channel_id, + self._mcf_channel_encode_part(channel,part), + capacity, + cost) + self._arc_to_channel[index] = (channel.src, channel.dest, channel, 0) + if self._prune_network and cnt > 1: + break + cnt += 1 + + def _prepare_mcf_solver(self): """ computes the uncertainty network given our prior belief and prepares the min cost flow solver @@ -82,43 +140,15 @@ def _prepare_mcf_solver(self, src, dest, amt: int = 1, mu: int = 100_000_000, 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._min_cost_flow = MCFNetwork() self._arc_to_channel = {} for s, d, channel in self._uncertainty_network.network.edges(data="channel"): - # ignore channels with too large base fee - 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 - # 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 - if self._prune_network and channel.success_probability(250_000) < 0.9: - continue - cnt = 0 - # QUANTIZATION): - for capacity, cost in channel.get_piecewise_linearized_costs(mu=mu): - index = self._min_cost_flow.AddArcWithCapacityAndUnitCost(self._mcf_id[s], - self._mcf_id[d], - capacity, - cost) - self._arc_to_channel[index] = (s, d, channel, 0) - if self._prune_network and cnt > 1: - break - cnt += 1 + self._mcf_new_arc(channel) # Add node supply to 0 for all nodes for i in self._uncertainty_network.network.nodes(): - self._min_cost_flow.SetNodeSupply(self._mcf_id[i], 0) - - # add amount to sending node - self._min_cost_flow.SetNodeSupply( - self._mcf_id[src], int(amt)) # /QUANTIZATION)) - - # add -amount to recipient nods - self._min_cost_flow.SetNodeSupply( - self._mcf_id[dest], -int(amt)) # /QUANTIZATION)) + self._min_cost_flow.SetNodeSupply(i, 0) def _next_hop(self, path): """ @@ -168,11 +198,15 @@ def _dissect_flow_to_paths(self, s, d): """ # 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 - if flow == 0: - continue - + + #for i in range(self._min_cost_flow.NumArcs()): + + index_list, flow_list = self._min_cost_flow.FlowArray() + for i,flow in zip(index_list,flow_list): + #flow = self._min_cost_flow.Flow(i) # *QUANTIZATION + #if flow == 0: + # continue + # print(i,flow) src, dest, channel, _ = self._arc_to_channel[i] if G.has_edge(src, dest): if channel.short_channel_id in G[src][dest]: @@ -204,8 +238,7 @@ def _dissect_flow_to_paths(self, s, d): G.remove_edge(src, dest, key=channel.short_channel_id) return attempts - 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): """ computes the optimal payment split to deliver `amt` from `src` to `dest` and updates our belief about the liquidity @@ -221,15 +254,16 @@ def _generate_candidate_paths(self, src, dest, amt: int, mu: int = 100_000_000, attempts_in_round = List[Attempt] # First we prepare the min cost flow by getting arcs from the uncertainty network - self._prepare_mcf_solver(src, dest, amt, mu, base) + self._prepare_mcf_demands(src, dest, amt) start = time.time() # print("solving mcf...") - status = self._min_cost_flow.Solve() - - if status != self._min_cost_flow.OPTIMAL: - print('There was an issue with the min cost flow input.') - print(f'Status: {status}') - exit(1) + try: + self._min_cost_flow.Solve() + #self._min_cost_flow._Solve_by_AugmentingPaths() + #self._min_cost_flow._Solve_by_CostScaling() + except: + raise BaseException('There was an issue with the min cost flow. Error code %d' % + self._min_cost_flow.error_code) attempts_in_round = self._dissect_flow_to_paths(src, dest) end = time.time() @@ -280,7 +314,7 @@ def _attempt_payments(self, attempts: List[Attempt]): else: attempt.status = AttemptStatus.FAILED - def _evaluate_attempts(self, payment: Payment): + def _evaluate_attempts(self, payment: Payment, log_out = sys.stdout): """ helper function to collect statistics about attempts and print them @@ -293,42 +327,42 @@ def _evaluate_attempts(self, payment: Payment): amt = 0 arrived_attempts = [] failed_attempts = [] - print("\nStatistics about {} candidate onions:\n".format(len(payment.attempts))) - print("successful attempts:") - print("--------------------") + print("\nStatistics about {} candidate onions:\n".format(len(payment.attempts)), file=log_out) + print("successful attempts:", file=log_out) + print("--------------------", file=log_out) 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))) + int(arrived_attempt.routing_fee * 1000 / arrived_attempt.amount)), file=log_out) paid_fees += arrived_attempt.routing_fee - print("\nfailed attempts:") - print("----------------") + print("\nfailed attempts:", file=log_out) + print("----------------", file=log_out) 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 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))) + int(failed_attempt.routing_fee * 1000 / failed_attempt.amount)), file=log_out) residual_amt += failed_attempt.amount - print("\nAttempt Summary:") - print("=================") - print("\nTried to deliver \t{:10} sats".format(amt)) + print("\nAttempt Summary:", file=log_out) + print("=================", file=log_out) + print("\nTried to deliver \t{:10} sats".format(amt), file=log_out) fraction = expected_sats_to_deliver * 100. / amt print("expected to deliver {:10} sats \t({:4.2f}%)".format( - int(expected_sats_to_deliver), fraction)) + int(expected_sats_to_deliver), fraction), file=log_out) fraction = (amt - residual_amt) * 100. / (amt) print("actually delivered {:10} sats \t({:4.2f}%)".format( - amt - residual_amt, fraction)) + amt - residual_amt, fraction), file=log_out) print("deviation: \t\t{: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)) + (amt - residual_amt) / (expected_sats_to_deliver + 1)), file=log_out) + print("planned_fee: {:8.3f} sat".format(total_fees), file=log_out) + print("paid fees: {:8.3f} sat".format(paid_fees), file=log_out) return residual_amt, paid_fees, len(payment.attempts), len(failed_attempts) def forget_information(self): @@ -336,6 +370,7 @@ def forget_information(self): forgets all the information in the UncertaintyNetwork that is a member of the PaymentSession """ self._uncertainty_network.reset_uncertainty_network() + self._min_cost_flow.Forget() def activate_network_wide_uncertainty_reduction(self, n): """ @@ -344,7 +379,7 @@ 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, log_out = sys.stdout): """ 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 @@ -363,31 +398,34 @@ def pickhardt_pay(self, src, dest, amt, mu=1, base=DEFAULT_BASE_THRESHOLD): # Initialise Payment # currently with underscore to not mix up with existing variable 'payment' payment = Payment(src, dest, amt) + mcf_time = 0 # 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 while amt > 0 and cnt < 10: - print("Round number: ", cnt + 1) - print("Try to deliver", amt, "satoshi:") + print("Round number: ", cnt + 1, file=log_out) + print("Try to deliver", amt, "satoshi:", file=log_out) 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) + paths, runtime = self._generate_candidate_paths(payment.sender, payment.receiver, amt) + mcf_time += runtime sub_payment.add_attempts(paths) # make attempts, try to send onion and register if success or not # update our information about the UncertaintyNetwork self._attempt_payments(sub_payment.attempts) + self._mcf_update_used_channels(sub_payment.attempts) # run some simple statistics and depict them amt, paid_fees, num_paths, number_failed_paths = self._evaluate_attempts( - sub_payment) + sub_payment, log_out) - print("Runtime of flow computation: {:4.2f} sec ".format(runtime)) - print("\n================================================================\n") + print("Runtime of flow computation: {:4.2f} sec ".format(runtime), file=log_out) + print("\n================================================================\n", file=log_out) total_number_failed_paths += number_failed_paths total_fees += paid_fees @@ -395,7 +433,7 @@ def pickhardt_pay(self, src, dest, amt, mu=1, base=DEFAULT_BASE_THRESHOLD): # 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): @@ -409,16 +447,18 @@ def pickhardt_pay(self, src, dest, amt, mu=1, base=DEFAULT_BASE_THRESHOLD): payment.end_time = 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(list(payment.filter_attempts(AttemptStatus.FAILED)))) + print("SUMMARY:", file=log_out) + print("========", file=log_out) + print("Rounds of mcf-computations:\t", cnt, file=log_out) + print("Number of attempts made:\t", len(payment.attempts), file=log_out) + print("Number of failed attempts:\t", len(list(payment.filter_attempts(AttemptStatus.FAILED))), file=log_out) print("Failure rate: {:4.2f}% ".format( - len(list(payment.filter_attempts(AttemptStatus.FAILED))) * 100. / len(payment.attempts))) + len(list(payment.filter_attempts(AttemptStatus.FAILED))) * 100. / len(payment.attempts)), file=log_out) 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)) + payment.end_time - payment.start_time), file=log_out) + print("Learnt entropy: {:5.2f} bits".format(entropy_start - entropy_end), file=log_out) print("fee for settlement of delivery: {:8.3f} sat --> {} ppm".format( - payment.settlement_fees/1000, int(payment.settlement_fees * 1000 / payment.total_amount))) - print("used mu:", mu) + payment.settlement_fees/1000, int(payment.settlement_fees * 1000 / payment.total_amount)), + file=log_out) + print("used mu:", self._mu, file=log_out) + return mcf_time, payment.end_time - payment.start_time diff --git a/python-documentation.md b/python-documentation.md new file mode 100644 index 0000000..808c71f --- /dev/null +++ b/python-documentation.md @@ -0,0 +1,36 @@ +# Python-API for Pickhardt Payments + +## Overview + +This document describes *Pickhardt payments* Python library API. + +## Example + +Below there is an example use case of the Python API + +``` +# perform a payment from node A to node B, of amt satoshis + +import pickhardtpayments + +gossip = get_gossip() # whatever resource that provides a list of the channels in LN +oracle = new_oracle() # a simulation of LN or a direct connection to it + +session = pickhardtpayments.paymentSession() + +for channel in gossip: + session.add_channel(channel) + +res_amt = amt +while res_amt>0: + mpp = session.optimizedPayment(A,B,res_amt) + + for path,value in mpp: + success,bad_channel = oracle.send_onion(path,value) + + if success: + res_amt -= value + session.updateSuccess(path,value) + else: + session.updateFailure(path,value,bad_channel) +``` diff --git a/subprojects/MinCostFlow b/subprojects/MinCostFlow new file mode 160000 index 0000000..6c1925e --- /dev/null +++ b/subprojects/MinCostFlow @@ -0,0 +1 @@ +Subproject commit 6c1925e661ccf92bf82b58838d8bb869333ef291