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

amp: introduce child preimage and hash derivation #4162

Merged
merged 3 commits into from
Jan 28, 2021
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 92 additions & 0 deletions amp/child.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package amp

import (
"crypto/sha256"
"encoding/binary"
"fmt"

"github.com/lightningnetwork/lnd/lntypes"
)

// Share represents an n-of-n sharing of a secret 32-byte value. The secret can
// be recovered by XORing all n shares together.
type Share [32]byte

// Xor stores the byte-wise xor of shares x and y in z.
func (z *Share) Xor(x, y *Share) {
for i := range z {
z[i] = x[i] ^ y[i]
}
}

// ChildDesc contains the information necessary to derive a child hash/preimage
// pair that is attached to a particular HTLC. This information will be known by
// both the sender and receiver in the process of fulfilling an AMP payment.
type ChildDesc struct {
// Share is one of n shares of the root seed. Once all n shares are
// known to the receiver, the Share will also provide entropy to the
// derivation of child hash and preimage.
Share Share

// Index is 32-bit value that can be used to derive up to 2^32 child
// hashes and preimages from a single Share. This allows the payment
// hashes sent over the network to be refreshed without needing to
// modify the Share.
Index uint32
}

// Child is a payment hash and preimage pair derived from the root seed. In
// addition to the derived values, a Child carries all information required in
// the derivation apart from the root seed (unless n=1).
type Child struct {
// ChildDesc contains the data required to derive the child hash and
// preimage below.
ChildDesc

// Preimage is the child payment preimage that can be used to settle the
// HTLC carrying Hash.
Preimage lntypes.Preimage

// Hash is the child payment hash that to be carried by the HTLC.
Hash lntypes.Hash
}

// String returns a human-readable description of a Child.
func (c *Child) String() string {
return fmt.Sprintf("share=%x, index=%d -> preimage=%v, hash=%v",
c.Share, c.Index, c.Preimage, c.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)),
Copy link
Collaborator

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.

Copy link
Contributor Author

@cfromknecht cfromknecht Jan 26, 2021

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

// child_hash = SHA256(child_preimage).
func DeriveChild(root Share, desc ChildDesc) *Child {
Roasbeef marked this conversation as resolved.
Show resolved Hide resolved
var (
indexBytes [4]byte
preimage lntypes.Preimage
hash lntypes.Hash
)

// Serialize the child index in big-endian order.
binary.BigEndian.PutUint32(indexBytes[:], desc.Index)

// Compute child_preimage as SHA256(root || share || child_index).
Roasbeef marked this conversation as resolved.
Show resolved Hide resolved
h := sha256.New()
_, _ = h.Write(root[:])
_, _ = h.Write(desc.Share[:])
_, _ = h.Write(indexBytes[:])
copy(preimage[:], h.Sum(nil))

// Compute child_hash as SHA256(child_preimage).
h = sha256.New()
_, _ = h.Write(preimage[:])
copy(hash[:], h.Sum(nil))

return &Child{
ChildDesc: desc,
Preimage: preimage,
Hash: hash,
}
}
113 changes: 113 additions & 0 deletions amp/derivation_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package amp_test

import (
"testing"

"github.com/lightningnetwork/lnd/amp"
"github.com/stretchr/testify/require"
)

type sharerTest struct {
name string
numShares int
}

var sharerTests = []sharerTest{
{
name: "root only",
numShares: 1,
},
{
name: "two shares",
numShares: 2,
},
{
name: "many shares",
numShares: 10,
},
}

// TestSharer executes the end-to-end derivation between sender and receiver,
// asserting that shares are properly computed and, when reconstructed by the
// receiver, produce identical child hashes and preimages as the sender.
func TestSharer(t *testing.T) {
for _, test := range sharerTests {
test := test
t.Run(test.name, func(t *testing.T) {
t.Parallel()

testSharer(t, test)
cfromknecht marked this conversation as resolved.
Show resolved Hide resolved
})
}
}

func testSharer(t *testing.T, test sharerTest) {
// Construct a new sharer with a random seed.
var (
sharer amp.Sharer
err error
)
sharer, err = amp.NewSeedSharer()
require.NoError(t, err)

// Assert that we can instantiate an equivalent root sharer using the
// root share.
root := sharer.Root()
sharerFromRoot := amp.SeedSharerFromRoot(&root)
require.Equal(t, sharer, sharerFromRoot)

// Generate numShares-1 randomized shares.
children := make([]*amp.Child, 0, test.numShares)
for i := 0; i < test.numShares-1; i++ {
var left amp.Sharer
left, sharer, err = sharer.Split()
Roasbeef marked this conversation as resolved.
Show resolved Hide resolved
require.NoError(t, err)

child := left.Child(0)

assertChildShare(t, child, 0)
children = append(children, child)
}

// Compute the final share and finalize the sharing.
child := sharer.Child(0)

assertChildShare(t, child, 0)
children = append(children, child)

assertReconstruction(t, children...)
}

// assertChildShare checks that the child has the expected child index, and that
// the child's preimage is valid for the its hash.
func assertChildShare(t *testing.T, child *amp.Child, expIndex int) {
t.Helper()

require.Equal(t, uint32(expIndex), child.Index)
require.True(t, child.Preimage.Matches(child.Hash))
}

// assertReconstruction takes a list of children and simulates the receiver
// recombining the shares, and then deriving the child preimage and hash for
// each HTLC. This asserts that the receiver can always rederive the full set of
// children knowing only the shares and child indexes for each.
func assertReconstruction(t *testing.T, children ...*amp.Child) {
t.Helper()

// Reconstruct a child descriptor for each of the provided children.
// In practice, the receiver will only know the share and the child
// index it learns for each HTLC.
descs := make([]amp.ChildDesc, 0, len(children))
for _, child := range children {
descs = append(descs, amp.ChildDesc{
Share: child.Share,
Index: child.Index,
})
}

// Now, recombine the shares and rederive a child for each of the
// descriptors above. The resulting set of children should exactly match
// the set provided.
children2 := amp.ReconstructChildren(descs...)
require.Equal(t, children, children2)
}
152 changes: 152 additions & 0 deletions amp/sharer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
package amp

import (
"crypto/rand"
)

// Sharer facilitates dynamic splitting of a root share value and derivation of
// child preimage and hashes for individual HTLCs in an AMP payment. A sharer
// represents a specific node in an abstract binary tree that can generate up to
// 2^32-1 unique child preimage-hash pairs for the same share value. A node can
// also be split into it's left and right child in the tree. The Sharer
// guarantees that the share value of the left and right child XOR to the share
// value of the parent. This allows larger HTLCs to split into smaller
// subpayments, while ensuring that the reconstructed secret will exactly match
// the root seed.
type Sharer interface {
// Root returns the root share of the derivation tree. This is the value
// that will be reconstructed when combining the set of all child
// shares.
Root() Share

// Child derives a child preimage and child hash given a 32-bit index.
// Passing a different index will generate a unique preimage-hash pair
// with high probability, allowing the payment hash carried on HTLCs to
// be refreshed without needing to modify the share value. This would
// typically be used when an partial payment needs to be retried if it
// encounters routine network failures.
Child(index uint32) *Child

// Split returns a Sharer for the left and right child of the parent
// Sharer. XORing the share values of both sharers always yields the
// share value of the parent. The sender should use this to recursively
// divide payments that are too large into smaller subpayments, knowing
// that the shares of all nodes descending from the parent will XOR to
// the parent's share.
Split() (Sharer, Sharer, error)
}

// SeedSharer orchestrates the sharing of the root AMP seed along multiple
// paths. It also supports derivation of the child payment hashes that get
// attached to HTLCs, and the child preimages used by the receiver to settle
// individual HTLCs in the set.
type SeedSharer struct {
root Share
curr Share
}

// NewSeedSharer generates a new SeedSharer instance with a seed drawn at
// random.
func NewSeedSharer() (*SeedSharer, error) {
var root Share
if _, err := rand.Read(root[:]); err != nil {
return nil, err
}

return SeedSharerFromRoot(&root), nil
}

// SeedSharerFromRoot instantiates a SeedSharer with an externally provided
// seed.
func SeedSharerFromRoot(root *Share) *SeedSharer {
return initSeedSharer(root, root)
}

func initSeedSharer(root, curr *Share) *SeedSharer {
return &SeedSharer{
root: *root,
curr: *curr,
}
}

// Seed returns the sharer's seed, the primary source of entropy for deriving
// shares of the root.
func (s *SeedSharer) Root() Share {
return s.root
}

// Split constructs two child Sharers whose shares sum to the parent Sharer.
// This allows an HTLC whose payment amount could not be routed to be
// recursively split into smaller subpayments. After splitting a sharer the
// parent share should no longer be used, and the caller should use the Child
// method on each to derive preimage/hash pairs for the HTLCs.
func (s *SeedSharer) Split() (Sharer, Sharer, error) {
shareLeft, shareRight, err := split(&s.curr)
if err != nil {
return nil, nil, err
}

left := initSeedSharer(&s.root, &shareLeft)
right := initSeedSharer(&s.root, &shareRight)

return left, right, nil
}

// Child derives a preimage/hash pair to be used for an AMP HTLC.
// All children of s will use the same underlying share, but have unique
// preimage and hash. This can be used to rerandomize the preimage/hash pair for
// a given HTLC if a new route is needed.
func (s *SeedSharer) Child(index uint32) *Child {
desc := ChildDesc{
Share: s.curr,
Index: index,
}

return DeriveChild(s.root, desc)
}

// ReconstructChildren derives the set of children hashes and preimages from the
// provided descriptors. The shares from each child descriptor are first used to
// compute the root, afterwards the child hashes and preimages are
// deterministically computed. For child descriptor at index i in the input,
// it's derived child will occupy index i of the returned children.
func ReconstructChildren(descs ...ChildDesc) []*Child {
// Recompute the root by XORing the provided shares.
var root Share
for _, desc := range descs {
root.Xor(&root, &desc.Share)
}

// With the root computed, derive the child hashes and preimages from
// the child descriptors.
children := make([]*Child, len(descs))
for i, desc := range descs {
children[i] = DeriveChild(root, desc)
}

return children
}

// split splits a share into two random values, that when XOR'd reproduce the
Copy link
Member

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!

// original share. Given a share s, the two shares are derived as:
// left <-$- random
// right = parent ^ left.
//
// When reconstructed, we have that:
// left ^ right = left ^ parent ^ left
// = parent.
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 {
Copy link
Member

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?

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, 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.

return Share{}, Share{}, err
}

// Compute right = parent ^ left.
var right Share
right.Xor(parent, &left)

return left, right, nil
}

var _ Sharer = (*SeedSharer)(nil)