-
Notifications
You must be signed in to change notification settings - Fork 2.1k
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
amp: introduce child preimage and hash derivation #4162
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Solid start! Still need to wrap my head around some of the nuance w.r.t splits/children, but I dig the overall abstraction level.
amp/share_desc.go
Outdated
// max depth 32. Each bit, starting with the least significant, | ||
// represents a left (0) or right (1) traversal. The traversal ends | ||
// after processing Depth bits. | ||
Path uint32 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What are the advantages of this approached compared to a "linear" scheme? By linear here I mean just fetching random values and XOR'ing them incrementally with the actual root seed.
In this case, all the leaves will XOR to uncover the actual root leaf?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Or I guess the value here is in deterministic share derivation with an elkrem-like structure?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
by linear scheme i'm assuming you mean something like:
share1 = r1
share2 = r2
- ...
share_n = root ^ r1 ^ ... ^ r_{n-1}
the primary issue is that the shares can succeed/fail independently. say you've sent out n
shares, share_n
succeeds but share_2
fails. you can no longer update share_n
to include another random value.
the construction is much simpler when there is no "distinguished" share, and just use recursion on state that is local to that shard (you only need the parent value to split). in fact, the only thing you can do in the example of above is to split share_2
into share_2l
and share_2r
, such that share_2 = share_2l ^ share2r
. this process can continue no matter how many time one needs to split.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am re-thinking the rationale for this type of multi-shard payments. What is exactly the benefit over sending multiple keysend payments that pay to the same payment address? (Possibly extended with a set id to make recurrent payments possible.)
With multiple keysends, the receiver can pull an incomplete set. But why is it important to prevent that? With xor-amp, it is still possible for the receiver to pull an incomplete set once all shards have arrived.
Something that is a difference, is that the receiver cannot pull if the set never becomes complete. But isn't this a feature that is only relevant in case the network is unreliable and incomplete sets happen frequently enough? Is that worth adding a new payment type? Even if incomplete sets happen often, why would the receiver pull the incomplete set?
Parking this idea here: Allow an external share to be part of the AMP set. So once all the htlcs have arrived, the receiver still needs to obtain one more share to find out the root secret. This last share could for example be the preimage of an on-chain reveal. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ready to land IMO so we can move forward with the other components, just one question to solidify my understanding of one of the design decisions re determinism or not.
return children | ||
} | ||
|
||
// split splits a share into two random values, that when XOR'd reproduce the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Love this, super simple yet extremely flexible!
func split(parent *Share) (Share, Share, error) { | ||
// Generate a random share for the left child. | ||
var left Share | ||
if _, err := rand.Read(left[:]); err != nil { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just to double check, the rationale for not instead using the root share as the seed to a CSPRNG here is that we don't retry failed payments on start up, so it doesn't matter that these splits are deterministic?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, we currently don't resume MPP payments on restarts and instead just wait for them to be cancelled. The plan is to keep this for AMP as well, which means we don't need to be able to rederive any of the secret values.
FWIW the splits could still use an RNG if we choose to store them all, the deterministic derivation was just an attempt at minimizing the storage requirements and minimize how much randomness we consume.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good to me, great work 💯
Excited to see movement on the AMP front.
Very clever with the binary tree construction, also reads very well with the abstractions!
I have two questions, mostly to get some more context on the scheme (non-blocking to this PR):
- Let's say we created 4 shares by splitting the root and then both left and right again and assigning them the indices 0 to 4 (
0=left_left, 1=left_right, 2=right_left, 3=right_right
). Now share 2 fails and we need to split further. How would we continue with assigning the indices? Would we just use a counter and for example assign 4 and 5 to the new children of 2 (and discard index 2 itself)? - How does the receiver know whether they have received all shares? Just by continually checking if XORing all shares together and plugging it into SHA results in the preimage/payment hash?
// DeriveChild computes the child preimage and child hash for a given (root, | ||
// share, index) tuple. The derivation is defined as: | ||
// | ||
// child_preimage = SHA256(root || share || be32(index)), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit/reminder: the inclusion of the share
in the child preimage is not yet mentioned/updated in lightning-rfc#658.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah the AMP rfc is out of date with this PR, hoping to get to that this week
🙏🙏🙏
The This can be done either by incrementing or, preferably, by picking values at random. An example, here just starting at 0 and incrementing for each fail, would be:
In addition an |
@joostjager great idea, I can make an issue for HODL-AMP! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM 🐲
Makes sense, thanks for the explanations, @cfromknecht! |
WIP since spec is still in flux, primarily looking for feedback on the interface.