Merge pull request #4162 from cfromknecht/amp-keys
amp: introduce child preimage and hash derivation
This commit is contained in:
commit
d10d1cbd86
92
amp/child.go
Normal file
92
amp/child.go
Normal file
@ -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,
|
||||
}
|
||||
}
|
113
amp/derivation_test.go
Normal file
113
amp/derivation_test.go
Normal file
@ -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)
|
||||
}
|
152
amp/sharer.go
Normal file
152
amp/sharer.go
Normal file
@ -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)
|
Loading…
Reference in New Issue
Block a user