diff --git a/amp/child.go b/amp/child.go new file mode 100644 index 0000000000..32bbe29839 --- /dev/null +++ b/amp/child.go @@ -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)), +// child_hash = SHA256(child_preimage). +func DeriveChild(root Share, desc ChildDesc) *Child { + 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). + 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, + } +} diff --git a/amp/derivation_test.go b/amp/derivation_test.go new file mode 100644 index 0000000000..4ed533be9d --- /dev/null +++ b/amp/derivation_test.go @@ -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) + }) + } +} + +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() + 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) +} diff --git a/amp/sharer.go b/amp/sharer.go new file mode 100644 index 0000000000..de27550cec --- /dev/null +++ b/amp/sharer.go @@ -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 +// 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 { + return Share{}, Share{}, err + } + + // Compute right = parent ^ left. + var right Share + right.Xor(parent, &left) + + return left, right, nil +} + +var _ Sharer = (*SeedSharer)(nil)