Skip to content

Commit

Permalink
Introduction of AttemptClass and PaymentClass (Issue #20) (#24)
Browse files Browse the repository at this point in the history
* add .gitignore

* introduce register payment method on OracleLightningNetwork.py

* adding settlement_payment in OracleLightningNetwork

first stub of a method as described in  #16
Still needs to be tested

* rewriting setter for `actual_liquidity` using properties (python way)

* adjust channels in both directions when payment is made

* Settlement of onions with partial payments only after all onions were routed successfully

* deletion of unused import

* shell for a Payment Class

* Revert "shell for a Payment Class"

This reverts commit 8dffa28.

* shell for a Payment Class for issue #20

* shell for as Attempt Class for issue #20

* Indroduction of Payment and Attempt class, refactoring.

* Introduction of PaymentClass and AttemptClass, refactoring.

* error fixing, semantic changes and refinement of Payment ant Attempt class

* adding types in methods for Attempt and Payment Class and amending relative paths on imports

* initializes fee and probability w/ none (instead of -1) #20

* adjustment of relative paths for modules in import

* extending .gitignore

* correcting tipo for gitignore

* init for documentation

* configuration updated

* correction in filter function for Attempt.Status #24

* changing imports back to initial call

* fixing issues from partial review
#24 (review)

* deleting docs folder and moving _estimate_payment_statistics into attempt class

* deleting docs folder and moving _estimate_payment_statistics into attempt class

* removed doc folder

* adding CHANGELOG

* Cleaning up after Attempt and Payment PR review

* changed filter function in payment to generator

* added setter to OracleChannel and fixed imports in init.py

* Fix typos in documentation

* rewriting setter for `actual_liquidity` using properties (python way)

* Indroduction of Payment and Attempt class, refactoring.

* Introduction of PaymentClass and AttemptClass, refactoring.

* adjustment of relative paths for modules in import

* changing imports back to initial call

* another rebasing hassle ;)

Co-authored-by: Sebastian <[email protected]>
Co-authored-by: Rene Pickhardt <[email protected]>
Co-authored-by: nassersaazi <[email protected]>
  • Loading branch information
4 people authored Jun 22, 2022
1 parent efd11ba commit cd46871
Show file tree
Hide file tree
Showing 14 changed files with 588 additions and 171 deletions.
11 changes: 11 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
.DS_Store
__pycache__
.idea
pickhardtpayments/pickhardtpayments.egg-info
pickhardtpayments/__pycache__

# not including large file with Channel Graph
*.json
examples/.json

pickhardt_pay.log
28 changes: 28 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Changelog
All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

<!--
TODO: Insert version codename, and username of the contributor that named the release.
-->
## [Unreleased]

## [0.1.0] - 2022-06-21
### Added
- introduction of an Attempt Class and a Payment Class ([#28])
- introduction of an AttemptStatus to describe the state of the Attempt ([#28])
- settle_payment in OracleLightingNetwork is added ([#28])
- logging added in SyncSimulatedPaymentSession ([#28])

### Changed
- calculation of fees and probabilities is moved from SyncSimulatedPaymentSession to Attempt class ([#28])

### Deprecated

### Removed

### Fixed

### EXPERIMENTAL
File renamed without changes.
2 changes: 1 addition & 1 deletion examples/basicexample.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,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, mu=0, base=0)
136 changes: 136 additions & 0 deletions pickhardtpayments/Attempt.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
from enum import Enum

from Channel import Channel
from pickhardtpayments import UncertaintyChannel


class AttemptStatus(Enum):
PLANNED = 1
INFLIGHT = 2
ARRIVED = 4
FAILED = 8
SETTLED = 16


class Attempt:
"""
An Attempt describes a path (a list of channels) of an amount from sender to receiver.
# TODO Describe life cycle of an Attempt
When sending an amount of sats from sender to receiver, a payment is usually split up and sent across
several paths, to increase the probability of being successfully delivered. Each of these paths is referred to as an
Attempt.
An Attempt consists of a list of Channels (class:Channel) and the amount in sats to be sent through this path.
When an Attempt is instantiated, the given amount is allocated to the inflight amount in the channels of the
path and the AttemptStatus is set to PLANNED.
:param path: a list of Channel objects from sender to receiver
:type path: list[Channel]
:param amount: the amount to be transferred from source to destination
:type amount: int
"""

def __init__(self, path: list[UncertaintyChannel], amount: int = 0):
"""Constructor method
"""
if amount >= 0:
self._amount = amount
else:
raise ValueError("amount for payment attempts needs to be positive")

i = 1
valid_path = True
while i < len(path):
valid_path = valid_path and (path[i - 1].dest == path[i].src)
i += 1
if valid_path:
self._path = path

channel: UncertaintyChannel
self._routing_fee = 0
self._probability = 1
for channel in path:
self._routing_fee += channel.routing_cost_msat(amount)
self._probability *= channel.success_probability(amount)
# When Attempt is created, all amounts are set inflight. Needs to be updated with AttemptStatus change!
# This is to correctly compute conditional probabilities of non-disjoint paths in the same set of paths
# channel.in_flight(amount)
channel.allocate_amount(amount)
self._status = AttemptStatus.PLANNED

def __str__(self):
description = "Path with {} channels to deliver {} sats and status {}.".format(len(self._path),
self._amount, self._status.name)
if self._routing_fee and self._routing_fee > 0:
description += "\nsuccess probability of {:6.2f}% , fee of {:8.3f} sat and a ppm of {:5} ".format(
self._probability * 100, self._routing_fee/1000, int(self._routing_fee * 1000 / self._amount))
return description

@property
def path(self) -> list[Channel]:
"""Returns the path of the attempt.
:return: the list of Channels that the path consists of
:rtype: list[Channel]
"""
return self._path

@property
def amount(self) -> int:
"""Returns the amount of the attempt.
:return: the amount that was tried to send in this Attempt
:rtype: int
"""
return self._amount

@property
def status(self) -> AttemptStatus:
"""Returns the status of the attempt.
:return: returns the state of the attempt
:rtype: AttemptStatus
"""
return self._status

@status.setter
def status(self, value: AttemptStatus):
"""Sets the status of the attempt.
A flag to describe if the path failed, succeeded or was used for settlement (see enum SettlementStatus)
:param value: Current state of the Attempt
:type value: AttemptStatus
"""
if not self._status == value:
# remove allocated amounts when Attempt status changes from PLANNED
if self._status == AttemptStatus.PLANNED and not value == AttemptStatus.INFLIGHT:
for channel in self._path:
channel.allocate_amount(-self._amount)

if self._status == AttemptStatus.INFLIGHT and value == AttemptStatus.ARRIVED:
# TODO write amount from inflight to min_liquidity/max_liquidity
# for channel in self._path:
# channel.allocate_amount(-self._amount)
pass

self._status = value

@property
def routing_fee(self) -> int:
"""Returns the accrued routing fee in msat requested for this path.
:return: accrued routing fees for this attempt in msat
:rtype: int
"""
return self._routing_fee

@property
def probability(self) -> float:
"""Returns estimated success probability before the attempt
:return: estimated success probability before the attempt
:rtype: float
"""
return self._probability
4 changes: 2 additions & 2 deletions pickhardtpayments/Channel.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
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 core lighting
Expand All @@ -21,7 +21,7 @@ class ChannelFields():
SHORT_CHANNEL_ID = 'short_channel_id'


class Channel():
class Channel:
"""
Stores the public available information of a channel.
Expand Down
4 changes: 2 additions & 2 deletions pickhardtpayments/ChannelGraph.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from .Channel import Channel


class ChannelGraph():
class ChannelGraph:
"""
Represents the public information about the Lightning Network that we see from Gossip and the
Bitcoin Blockchain.
Expand All @@ -15,7 +15,7 @@ class ChannelGraph():

def _get_channel_json(self, filename: str):
"""
extracts the dictionary from the file that contains lightnig-cli listchannels json string
extracts the dictionary from the file that contains lightning-cli listchannels json string
"""
f = open(filename)
return json.load(f)["channels"]
Expand Down
18 changes: 13 additions & 5 deletions pickhardtpayments/OracleChannel.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ def __init__(self, channel: Channel, actual_liquidity: int = None):
self._actual_liquidity = random.randint(0, self.capacity)

def __str__(self):
return super().__str__()+" actual Liquidity: {}".format(self.actual_liquidity)
return super().__str__() + " actual Liquidity: {}".format(self.actual_liquidity)

@property
def actual_liquidity(self):
Expand All @@ -30,15 +30,23 @@ def actual_liquidity(self):
return self._actual_liquidity

@actual_liquidity.setter
def actual_liquidity(self,amt):
self._actual_liquidity = amt
def actual_liquidity(self, amt: int):
"""Sets the liquidity of a channel in the Oracle Network
:param amt: amount to be assigned to channel liquidity
:type amt: int
"""
if 0 <= amt <= self.capacity:
self._actual_liquidity = amt
else:
raise ValueError(f"Liquidity for channel {self.short_channel_id} cannot be set. Amount {amt} is negative or higher than capacity")



def can_forward(self, amt: int):
"""
check if the oracle channel can forward a certain amount
"""
if amt <= self.actual_liquidity:
return True
else:
return False
return False
34 changes: 30 additions & 4 deletions pickhardtpayments/OracleLightningNetwork.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from typing import List

from .ChannelGraph import ChannelGraph
from .OracleChannel import OracleChannel
import networkx as nx
Expand All @@ -13,7 +15,7 @@ def __init__(self, channel_graph: ChannelGraph):
for src, dest, short_channel_id, channel in channel_graph.network.edges(data="channel", keys=True):
oracle_channel = None

# If Channel in oposite direction already exists with liquidity information match the channel
# If Channel in opposite direction already exists with liquidity information match the channel
if self._network.has_edge(dest, src):
if short_channel_id in self._network[dest][src]:
capacity = channel.capacity
Expand All @@ -35,14 +37,18 @@ def network(self):
return self._network

def send_onion(self, path, amt):
"""
:rtype: object
"""
for channel in path:
oracle_channel = self.get_channel(
channel.src, channel.dest, channel.short_channel_id)
success_of_probe = oracle_channel.can_forward(
channel.in_flight+amt)
channel.in_flight + amt)
# print(channel,amt,success_of_probe)
channel.update_knowledge(amt, success_of_probe)
if success_of_probe == False:
if not success_of_probe:
return False, channel
return True, None

Expand All @@ -55,7 +61,7 @@ def theoretical_maximum_payable_amount(self, source: str, destination: str, base
"""
test_network = nx.DiGraph()
for src, dest, channel in self.network.edges(data="channel"):
#liqudity = 0
# liquidity = 0
# for channel in channels:
if channel.base_fee > base_fee:
continue
Expand All @@ -71,3 +77,23 @@ def theoretical_maximum_payable_amount(self, source: str, destination: str, base

mincut, _ = nx.minimum_cut(test_network, source, destination)
return mincut

def settle_payment(self, path: List[OracleChannel], payment_amount: int):
"""
receives a List of channels and payment amount and adjusts the balances of the channels along the path.
settle_payment should only be called after all send_onions for a payment terminated successfully!
# TODO testing
"""
for channel in path:
settlement_channel = self.get_channel(channel.src, channel.dest, channel.short_channel_id)
return_settlement_channel = self.get_channel(channel.dest, channel.src, channel.short_channel_id)
if settlement_channel.actual_liquidity > payment_amount:
# decrease channel balance in sending channel by amount
settlement_channel.actual_liquidity = settlement_channel.actual_liquidity - payment_amount
# increase channel balance in the other direction by amount
return_settlement_channel.actual_liquidity = return_settlement_channel.actual_liquidity + payment_amount
else:
raise Exception("""Channel liquidity on Channel {} is lower than payment amount.
\nPayment cannot settle.""".format(channel.short_channel_id))
return 0
Loading

0 comments on commit cd46871

Please sign in to comment.