Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduction of AttemptClass and PaymentClass (Issue #20) #24

Merged
merged 42 commits into from
Jun 22, 2022

Conversation

sebulino
Copy link
Contributor

@sebulino sebulino commented May 20, 2022

Extraction of Attempt and Payment.
A Payment is a central object in the library, and in particular in the simulation. This is why it "deserves" its own Class.
Each onion is recorded as an Attempt and registered with the respective Payment. This not only allows for statistics for each Payment object but also collects all attempts where an amount could successfully flow from sender to receiver.

refactoring of SyncSimulatedPaymentSession has been done as well as a first version of the documentation of the two classes.

Copy link
Owner

@renepickhardt renepickhardt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks a lot! Despite me having a few question mark, comments and suggestions (I guess it is much easier to question code rather than writing it) it is a real pleasure reading your code. I have the feeling you write much cleaner code than I do!

strong concept ACK!

def __init__(self, path: list[Channel], amount: int = 0):
"""Constructor method
"""
self._paths = None
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is this plural?

for channel in path:
if not isinstance(channel, Channel):
raise ValueError("path needs to be a collection of Channels")
self._path = path
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is the difference to self._paths from line 32?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is the real one ;)

self._amount = amount

def __str__(self):
return "Path with {} channels to deliver {} sats and status {}.".format(len(self._path),
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking how I depict attempts in the evaluation we might want to change that string to include stuff like probability, fees, ppm. We could get inspiration by the printoutput of the simulation

for attempt in attempts:
if not isinstance(attempt, Channel):
raise ValueError("path needs to be a collection of Channels")
self._paths = attempts
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really don't get this :(

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is indeed unnecessary as well.
This is part of the setter for path in Attempt. When I originally added the path to attempt, I used this setter. And while I refactored step by step, at the time i wasn't sure that didn't make any mistakes when passing on parameters. That's why i checked before inclusion and raised errors to see where things failed.

The setter now can go, because I made this a mandatory parameter at instantiation and I require the correct class.

Thanks for pointing this out!

:return: estimated success probability before the attempt
:rtype: float
"""
return self._probability
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not entirely sure as the attempt class does not know the uncertainty network but we could compute this on the fly for the path (as the path contains such information)

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there is btw a strong attempt against it as non disjoint paths will change probability computation.

Copy link
Contributor Author

@sebulino sebulino May 20, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, this is to collect statistical a priori information:
In _estimate_payment_statistics this is added when calling uncertainty_network.get_features_of_candidate_path. it's tied to this instance of attempt, so another Attempt on the same graph will have its own (then perhaps different probability). It should be purely descriptive and is not used for further calculation.

Copy link
Contributor Author

@sebulino sebulino May 20, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bias in probabilities in UncertaintyGraph?

When collecting possible paths in a round/"payment loop", all path candidates are assumed to be sent and settled successfully.
This is my assumption, because we iterate over each attempt:

  • UncertaintyChannel.success_probability freshly calculates probabilities from current channel balances, and
  • then allocate_amount_on_path is updated before the loop commences with the next possibly not disjoint path, that then gets its probabilities from the just before adjusted inflight amount.

Or am I reading it wrong?

Ist this something we can live with?
What would be possible ways to correct for this (like trying to immediately figure out if the onion would fail or not)?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This indeed the really messy book keeping that I introduced. I am happy to find a better solution to this! I need to allocate the amounts for planned onions to have probability correction of the next path correct. (Ask me for an example if you need one!) I remove all of the allocated HTLCs then before I do the actual sending / probing. It is really the most ugly part of my code :(

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just started to look at the code. I think this is btw exactly the difference between PLANNED and INFLIGHT while internally I increase the inflight counter I am actually in the planning phase and to compute the probabilities of planned paths I need to take into account that I already plan to allocate something sats to an edge (without knowing if that will work yet) So maybe we just need to move over the logic from the Enum to the state of a channel where we also have planned sats before we go and send onions and switch those to inflight sats. in this way I would note have to remove them before testing onions. does that make any sense? I am actually not sure myself if this would be any good.

inflight_attempts = []
failed_attempts = []
print("\nStatistics about {} candidate onions:\n".format(len(attempts)))
for attempt in attempts:
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess this is motivated by how the code was histroically structured. But wouldn't it make sense to have this attempts list as an sub_paymet = Payments() object and later in the loop just add he sub_payment to the payment?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this would allow us that we could later call something like sub_payment.filter(SettlementStatus.FAILED) instead of using an additional list

from pickhardtpayments import Channel


class SettlementStatus(Enum):
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AttemptStatus?

if success:
attempt.status = SettlementStatus.INFLIGHT
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wouldn't that be AttmptStatus.ARRIVED?

src, dest, amt, mu, base)
# 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
attempts_in_round, runtime = self._generate_candidate_paths(
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe sub_payment of type Payment

end = time.time()
# When residual amount is 0 / enough successful onions have been found, then settle payment. Else drop onions.
if amt == 0:
for onion in payment.inflight_attempts:
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

technically inflight might not be at recipient yet and could still fail. So I guess we want ARRIVED here. I guess that was the confusion of this artificial distinction between INFLIGHT and ARRIVED

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. I think that indeed it is a lot clearer with ARRIVED.

@sebulino sebulino requested a review from renepickhardt May 26, 2022 10:56
Copy link
Owner

@renepickhardt renepickhardt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sry could not finnish review but wanted to at least drop the current comments

:return: estimated success probability before the attempt
:rtype: float
"""
return self._probability
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just started to look at the code. I think this is btw exactly the difference between PLANNED and INFLIGHT while internally I increase the inflight counter I am actually in the planning phase and to compute the probabilities of planned paths I need to take into account that I already plan to allocate something sats to an edge (without knowing if that will work yet) So maybe we just need to move over the logic from the Enum to the state of a channel where we also have planned sats before we go and send onions and switch those to inflight sats. in this way I would note have to remove them before testing onions. does that make any sense? I am actually not sure myself if this would be any good.

def __init__(self, path: list[UncertaintyChannel], amount: int = 0):
"""Constructor method
"""
self._routing_fee = -1
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Didn't we say None instead of -1? Note that in particular one might have negative routing fees in the protocol in the future.

Copy link
Contributor Author

@sebulino sebulino May 29, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, negative routing fees will indeed break the -1 idea. I only changed it in payment, now changed it in attempt as well.
I would actually extract the planned/inflight problem into its own issue. i fear that discussion around it will get lost in this Thread. -> #25

@sebulino sebulino requested a review from renepickhardt May 29, 2022 11:25
@sebulino sebulino requested a review from renepickhardt June 21, 2022 10:22
Copy link
Owner

@renepickhardt renepickhardt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lets discuss out of band?

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)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So far so good and makes sense

"""
if not self._status == value:
# remove allocated amounts when Attempt status changes from PLANNED
if self._status == AttemptStatus.PLANNED and not value == AttemptStatus.INFLIGHT:
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not inflight?

I am wondering if we should have a setter API or if we can only inc status by 1.

That being said I see the idea. While i think it is -given the design of the code- the only reasonable option i think it demonstrates that I messed something up. (It feels really wrong to do the amounts tweeking here)

@sebulino sebulino requested a review from renepickhardt June 21, 2022 16:00
Copy link
Owner

@renepickhardt renepickhardt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Concept ACK. Thank you very much for this excellent work and for your endurance!

I think we are on the brink of merging this and refactoring the rest in #18 . However I had a few things where I have the feeling that the refactoring has not been executed fully. I hope my comments are helpful (as mentioned I have really poor concentration today)

Some of the comments don't need intermediate action but are just for the follow up PR but since we still need some tiny changes I would kindly ask you to also rebase against the main branch so that I can hopefully merge tomorrow (:

pickhardtpayments/Payment.py Outdated Show resolved Hide resolved
pickhardtpayments/UncertaintyNetwork.py Outdated Show resolved Hide resolved
pickhardtpayments/SyncSimulatedPaymentSession.py Outdated Show resolved Hide resolved
@sebulino sebulino force-pushed the payments_attempts branch from 2618078 to 62296de Compare June 21, 2022 21:49
@sebulino sebulino requested a review from renepickhardt June 21, 2022 21:51
except ValueError:
logging.warning("ValueError in Payment.filtered_attempts")
return []
return [attempt for attempt in self._attempts if attempt.status.value == flag.value]
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Haha! That is not what I meant by creating a generstor using yield. But it is great to see how python can reduce "trivial" code blocks. (:

@renepickhardt
Copy link
Owner

Also LGTM!

@sebulino sebulino force-pushed the payments_attempts branch from 0ea743d to 111a691 Compare June 22, 2022 09:28
@renepickhardt renepickhardt merged commit cd46871 into renepickhardt:main Jun 22, 2022
@sebulino sebulino deleted the payments_attempts branch June 24, 2022 13:26
@adamritter
Copy link

It seems like this change broke the payment algorithm or simulation. PickhardtPaymentsExample.ipynb examples don't work anymore:

Round number: 10
Try to deliver 18879883 satoshi:

Statistics about 46 candidate onions:

successful attempts:

p = 100.00% amt: 216543 sats hops: 4 ppm: 1413
p = 94.37% amt: 84263 sats hops: 5 ppm: 1349
p = 90.69% amt: 59164 sats hops: 5 ppm: 1708
p = 75.00% amt: 337453 sats hops: 5 ppm: 1417
p = 73.91% amt: 112351 sats hops: 5 ppm: 1374
p = 99.52% amt: 42718 sats hops: 6 ppm: 1348
p = 100.00% amt: 35946 sats hops: 6 ppm: 1367
p = 94.97% amt: 242846 sats hops: 7 ppm: 1696
p = 77.42% amt: 2351 sats hops: 9 ppm: 1427

failed attempts:

p = 100.00% amt: 88000 sats hops: 2 ppm: 1339
p = 74.25% amt: 288187 sats hops: 2 ppm: 1341
p = 14.57% amt: 1574652 sats hops: 2 ppm: 1359
p = 100.00% amt: 102400 sats hops: 2 ppm: 1381
p = 100.00% amt: 102400 sats hops: 2 ppm: 1356
....

SUMMARY:

Rounds of mcf-computations: 10
Number of attempts made: 448
Number of failed attempts: 356
Failure rate: 79.46%
total Payment lifetime (including inefficient memory management): 8.661 sec
Learnt entropy: 229.60 bits
fee for settlement of delivery: 0.000 sat --> 0 ppm
used mu: 100

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants