diff --git a/zpay32/invoice.go b/zpay32/invoice.go index fc9f7d81..ffed10fd 100644 --- a/zpay32/invoice.go +++ b/zpay32/invoice.go @@ -37,9 +37,9 @@ const ( // with zeroes. pubKeyBase32Len = 53 - // routingInfoLen is the number of bytes needed to encode the extra - // routing info of a single private route. - routingInfoLen = 51 + // hopHintLen is the number of bytes needed to encode the hop hint of a + // single private route. + hopHintLen = 51 // The following byte values correspond to the supported field types. // The field name is the character representing that 5-bit value in the @@ -141,31 +141,12 @@ type Invoice struct { // Optional. FallbackAddr btcutil.Address - // RoutingInfo is one or more entries containing extra routing - // information for a private route to the target node. - // Optional. - RoutingInfo []ExtraRoutingInfo -} - -// ExtraRoutingInfo holds the information needed to route a payment along one -// private channel. -type ExtraRoutingInfo struct { - // PubKey is the public key of the node at the start of this channel. - PubKey *btcec.PublicKey - - // ShortChanID is the channel ID of the channel. - ShortChanID uint64 - - // FeeBaseMsat is the base fee in millisatoshis required for routing - // along this channel. - FeeBaseMsat uint32 - - // FeeProportionalMillionths is the proportional fee in millionths of a - // satoshi required for routing along this channel. - FeeProportionalMillionths uint32 - - // CltvExpDelta is this channel's cltv expiry delta. - CltvExpDelta uint16 + // RouteHints represents one or more different route hints. Each route + // hint can be individually used to reach the destination. These usually + // represent private routes. + // + // NOTE: This is optional. + RouteHints [][]routing.HopHint } // Amount is a functional option that allows callers of NewInvoice to set the @@ -231,12 +212,11 @@ func FallbackAddr(fallbackAddr btcutil.Address) func(*Invoice) { } } -// RoutingInfo is a functional option that allows callers of NewInvoice to set -// one or more entries containing extra routing information for a private route -// to the target node. -func RoutingInfo(routingInfo []ExtraRoutingInfo) func(*Invoice) { +// RouteHint is a functional option that allows callers of NewInvoice to add +// one or more hop hints that represent a private route to the destination. +func RouteHint(routeHint []routing.HopHint) func(*Invoice) { return func(i *Invoice) { - i.RoutingInfo = routingInfo + i.RouteHints = append(i.RouteHints, routeHint) } } @@ -525,10 +505,19 @@ func validateInvoice(invoice *Invoice) error { return fmt.Errorf("neither description nor description hash set") } - // Can have at most 20 extra hops for routing. - if len(invoice.RoutingInfo) > 20 { - return fmt.Errorf("too many extra hops: %d", - len(invoice.RoutingInfo)) + // We'll restrict invoices to include up to 20 different private route + // hints. We do this to avoid overly large invoices. + if len(invoice.RouteHints) > 20 { + return fmt.Errorf("too many private routes: %d", + len(invoice.RouteHints)) + } + + // Each route hint can have at most 20 hops. + for i, routeHint := range invoice.RouteHints { + if len(routeHint) > 20 { + return fmt.Errorf("route hint %d has too many extra "+ + "hops: %d", i, len(routeHint)) + } } // Check that we support the field lengths. @@ -666,13 +655,15 @@ func parseTaggedFields(invoice *Invoice, fields []byte, net *chaincfg.Params) er invoice.FallbackAddr, err = parseFallbackAddr(base32Data, net) case fieldTypeR: - if invoice.RoutingInfo != nil { - // We skip the field if we have already seen a - // supported one. - continue + // An `r` field can be included in an invoice multiple + // times, so we won't skip it if we have already seen + // one. + routeHint, err := parseRouteHint(base32Data) + if err != nil { + return err } - invoice.RoutingInfo, err = parseRoutingInfo(base32Data) + invoice.RouteHints = append(invoice.RouteHints, routeHint) default: // Ignore unknown type. } @@ -850,35 +841,38 @@ func parseFallbackAddr(data []byte, net *chaincfg.Params) (btcutil.Address, erro return addr, nil } -// parseRoutingInfo converts the data (encoded in base32) into an array -// containing one or more entries of extra routing info. -func parseRoutingInfo(data []byte) ([]ExtraRoutingInfo, error) { +// parseRouteHint converts the data (encoded in base32) into an array containing +// one or more routing hop hints that represent a single route hint. +func parseRouteHint(data []byte) ([]routing.HopHint, error) { base256Data, err := bech32.ConvertBits(data, 5, 8, false) if err != nil { return nil, err } - if len(base256Data)%routingInfoLen != 0 { - return nil, fmt.Errorf("expected length multiple of %d bytes, got %d", - routingInfoLen, len(base256Data)) + if len(base256Data)%hopHintLen != 0 { + return nil, fmt.Errorf("expected length multiple of %d bytes, "+ + "got %d", hopHintLen, len(base256Data)) } - var routingInfo []ExtraRoutingInfo - info := ExtraRoutingInfo{} + var routeHint []routing.HopHint + for len(base256Data) > 0 { - info.PubKey, err = btcec.ParsePubKey(base256Data[:33], btcec.S256()) + hopHint := routing.HopHint{} + hopHint.NodeID, err = btcec.ParsePubKey(base256Data[:33], btcec.S256()) if err != nil { return nil, err } - info.ShortChanID = binary.BigEndian.Uint64(base256Data[33:41]) - info.FeeBaseMsat = binary.BigEndian.Uint32(base256Data[41:45]) - info.FeeProportionalMillionths = binary.BigEndian.Uint32(base256Data[45:49]) - info.CltvExpDelta = binary.BigEndian.Uint16(base256Data[49:51]) - routingInfo = append(routingInfo, info) + hopHint.ChannelID = binary.BigEndian.Uint64(base256Data[33:41]) + hopHint.FeeBaseMSat = binary.BigEndian.Uint32(base256Data[41:45]) + hopHint.FeeProportionalMillionths = binary.BigEndian.Uint32(base256Data[45:49]) + hopHint.CLTVExpiryDelta = binary.BigEndian.Uint16(base256Data[49:51]) + + routeHint = append(routeHint, hopHint) + base256Data = base256Data[51:] } - return routingInfo, nil + return routeHint, nil } // writeTaggedFields writes the non-nil tagged fields of the Invoice to the @@ -977,25 +971,37 @@ func writeTaggedFields(bufferBase32 *bytes.Buffer, invoice *Invoice) error { } } - if len(invoice.RoutingInfo) > 0 { - // Each extra routing info is encoded using 51 bytes. - routingDataBase256 := make([]byte, 0, 51*len(invoice.RoutingInfo)) - for _, r := range invoice.RoutingInfo { - base256 := make([]byte, 51) - copy(base256[:33], r.PubKey.SerializeCompressed()) - binary.BigEndian.PutUint64(base256[33:41], r.ShortChanID) - binary.BigEndian.PutUint32(base256[41:45], r.FeeBaseMsat) - binary.BigEndian.PutUint32(base256[45:49], r.FeeProportionalMillionths) - binary.BigEndian.PutUint16(base256[49:51], r.CltvExpDelta) - routingDataBase256 = append(routingDataBase256, base256...) + for _, routeHint := range invoice.RouteHints { + // Each hop hint is encoded using 51 bytes, so we'll make to + // sure to allocate enough space for the whole route hint. + routeHintBase256 := make([]byte, 0, hopHintLen*len(routeHint)) + + for _, hopHint := range routeHint { + hopHintBase256 := make([]byte, hopHintLen) + copy(hopHintBase256[:33], hopHint.NodeID.SerializeCompressed()) + binary.BigEndian.PutUint64( + hopHintBase256[33:41], hopHint.ChannelID, + ) + binary.BigEndian.PutUint32( + hopHintBase256[41:45], hopHint.FeeBaseMSat, + ) + binary.BigEndian.PutUint32( + hopHintBase256[45:49], hopHint.FeeProportionalMillionths, + ) + binary.BigEndian.PutUint16( + hopHintBase256[49:51], hopHint.CLTVExpiryDelta, + ) + routeHintBase256 = append(routeHintBase256, hopHintBase256...) } - routingDataBase32, err := bech32.ConvertBits(routingDataBase256, - 8, 5, true) + + routeHintBase32, err := bech32.ConvertBits( + routeHintBase256, 8, 5, true, + ) if err != nil { return err } - err = writeTaggedField(bufferBase32, fieldTypeR, routingDataBase32) + err = writeTaggedField(bufferBase32, fieldTypeR, routeHintBase32) if err != nil { return err } diff --git a/zpay32/invoice_internal_test.go b/zpay32/invoice_internal_test.go index 613c3a13..cee1b5e0 100644 --- a/zpay32/invoice_internal_test.go +++ b/zpay32/invoice_internal_test.go @@ -8,6 +8,7 @@ import ( "time" "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/routing" "github.com/roasbeef/btcd/btcec" "github.com/roasbeef/btcd/chaincfg" "github.com/roasbeef/btcutil" @@ -706,18 +707,18 @@ func TestParseFallbackAddr(t *testing.T) { } } -// TestParseRoutingInfo checks that the routing info is properly parsed. -func TestParseRoutingInfo(t *testing.T) { +// TestParseRouteHint checks that the routing info is properly parsed. +func TestParseRouteHint(t *testing.T) { t.Parallel() var testSingleHopData []byte for _, r := range testSingleHop { base256 := make([]byte, 51) - copy(base256[:33], r.PubKey.SerializeCompressed()) - binary.BigEndian.PutUint64(base256[33:41], r.ShortChanID) - binary.BigEndian.PutUint32(base256[41:45], r.FeeBaseMsat) + copy(base256[:33], r.NodeID.SerializeCompressed()) + binary.BigEndian.PutUint64(base256[33:41], r.ChannelID) + binary.BigEndian.PutUint32(base256[41:45], r.FeeBaseMSat) binary.BigEndian.PutUint32(base256[45:49], r.FeeProportionalMillionths) - binary.BigEndian.PutUint16(base256[49:51], r.CltvExpDelta) + binary.BigEndian.PutUint16(base256[49:51], r.CLTVExpiryDelta) testSingleHopData = append(testSingleHopData, base256...) } testSingleHopData, _ = bech32.ConvertBits(testSingleHopData, 8, 5, true) @@ -725,11 +726,11 @@ func TestParseRoutingInfo(t *testing.T) { var testDoubleHopData []byte for _, r := range testDoubleHop { base256 := make([]byte, 51) - copy(base256[:33], r.PubKey.SerializeCompressed()) - binary.BigEndian.PutUint64(base256[33:41], r.ShortChanID) - binary.BigEndian.PutUint32(base256[41:45], r.FeeBaseMsat) + copy(base256[:33], r.NodeID.SerializeCompressed()) + binary.BigEndian.PutUint64(base256[33:41], r.ChannelID) + binary.BigEndian.PutUint32(base256[41:45], r.FeeBaseMSat) binary.BigEndian.PutUint32(base256[45:49], r.FeeProportionalMillionths) - binary.BigEndian.PutUint16(base256[49:51], r.CltvExpDelta) + binary.BigEndian.PutUint16(base256[49:51], r.CLTVExpiryDelta) testDoubleHopData = append(testDoubleHopData, base256...) } testDoubleHopData, _ = bech32.ConvertBits(testDoubleHopData, 8, 5, true) @@ -737,7 +738,7 @@ func TestParseRoutingInfo(t *testing.T) { tests := []struct { data []byte valid bool - result []ExtraRoutingInfo + result []routing.HopHint }{ { data: []byte{0x0, 0x0, 0x0, 0x0}, @@ -746,7 +747,7 @@ func TestParseRoutingInfo(t *testing.T) { { data: []byte{}, valid: true, - result: []ExtraRoutingInfo{}, + result: []routing.HopHint{}, }, { data: testSingleHopData, @@ -765,13 +766,13 @@ func TestParseRoutingInfo(t *testing.T) { } for i, test := range tests { - routingInfo, err := parseRoutingInfo(test.data) + routeHint, err := parseRouteHint(test.data) if (err == nil) != test.valid { t.Errorf("routing info decoding test %d failed: %v", i, err) return } if test.valid { - if err := compareRoutingInfos(test.result, routingInfo); err != nil { + if err := compareRouteHints(test.result, routeHint); err != nil { t.Fatalf("test %d failed decoding routing info: %v", i, err) } } diff --git a/zpay32/invoice_test.go b/zpay32/invoice_test.go index 78ede39f..e52d6098 100644 --- a/zpay32/invoice_test.go +++ b/zpay32/invoice_test.go @@ -12,6 +12,7 @@ import ( "time" "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/routing" "github.com/roasbeef/btcd/btcec" "github.com/roasbeef/btcd/chaincfg" "github.com/roasbeef/btcd/chaincfg/chainhash" @@ -47,34 +48,34 @@ var ( testAddrMainnetP2WPKH, _ = btcutil.DecodeAddress("bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4", &chaincfg.MainNetParams) testAddrMainnetP2WSH, _ = btcutil.DecodeAddress("bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3", &chaincfg.MainNetParams) - testRoutingInfoPubkeyBytes, _ = hex.DecodeString("029e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255") - testRoutingInfoPubkey, _ = btcec.ParsePubKey(testRoutingInfoPubkeyBytes, btcec.S256()) - testRoutingInfoPubkeyBytes2, _ = hex.DecodeString("039e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255") - testRoutingInfoPubkey2, _ = btcec.ParsePubKey(testRoutingInfoPubkeyBytes2, btcec.S256()) + testHopHintPubkeyBytes1, _ = hex.DecodeString("029e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255") + testHopHintPubkey1, _ = btcec.ParsePubKey(testHopHintPubkeyBytes1, btcec.S256()) + testHopHintPubkeyBytes2, _ = hex.DecodeString("039e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255") + testHopHintPubkey2, _ = btcec.ParsePubKey(testHopHintPubkeyBytes2, btcec.S256()) - testSingleHop = []ExtraRoutingInfo{ + testSingleHop = []routing.HopHint{ { - PubKey: testRoutingInfoPubkey, - ShortChanID: 0x0102030405060708, - FeeBaseMsat: 0, + NodeID: testHopHintPubkey1, + ChannelID: 0x0102030405060708, + FeeBaseMSat: 0, FeeProportionalMillionths: 20, - CltvExpDelta: 3, + CLTVExpiryDelta: 3, }, } - testDoubleHop = []ExtraRoutingInfo{ + testDoubleHop = []routing.HopHint{ { - PubKey: testRoutingInfoPubkey, - ShortChanID: 0x0102030405060708, - FeeBaseMsat: 1, + NodeID: testHopHintPubkey1, + ChannelID: 0x0102030405060708, + FeeBaseMSat: 1, FeeProportionalMillionths: 20, - CltvExpDelta: 3, + CLTVExpiryDelta: 3, }, { - PubKey: testRoutingInfoPubkey2, - ShortChanID: 0x030405060708090a, - FeeBaseMsat: 2, + NodeID: testHopHintPubkey2, + ChannelID: 0x030405060708090a, + FeeBaseMSat: 2, FeeProportionalMillionths: 30, - CltvExpDelta: 4, + CLTVExpiryDelta: 4, }, } @@ -413,7 +414,7 @@ func TestDecodeEncode(t *testing.T) { DescriptionHash: &testDescriptionHash, Destination: testPubKey, FallbackAddr: testRustyAddr, - RoutingInfo: testSingleHop, + RouteHints: [][]routing.HopHint{testSingleHop}, } }, beforeEncoding: func(i *Invoice) { @@ -436,7 +437,7 @@ func TestDecodeEncode(t *testing.T) { DescriptionHash: &testDescriptionHash, Destination: testPubKey, FallbackAddr: testRustyAddr, - RoutingInfo: testDoubleHop, + RouteHints: [][]routing.HopHint{testDoubleHop}, } }, beforeEncoding: func(i *Invoice) { @@ -680,7 +681,7 @@ func TestNewInvoice(t *testing.T) { Amount(testMillisat20mBTC), DescriptionHash(testDescriptionHash), FallbackAddr(testRustyAddr), - RoutingInfo(testDoubleHop), + RouteHint(testDoubleHop), ) }, valid: true, @@ -802,7 +803,19 @@ func compareInvoices(expected, actual *Invoice) error { expected.FallbackAddr, actual.FallbackAddr) } - return compareRoutingInfos(expected.RoutingInfo, actual.RoutingInfo) + if len(expected.RouteHints) != len(actual.RouteHints) { + return fmt.Errorf("expected %d RouteHints, got %d", + len(expected.RouteHints), len(actual.RouteHints)) + } + + for i, routeHint := range expected.RouteHints { + err := compareRouteHints(routeHint, actual.RouteHints[i]) + if err != nil { + return err + } + } + + return nil } func comparePubkeys(a, b *btcec.PublicKey) bool { @@ -831,36 +844,36 @@ func compareHashes(a, b *[32]byte) bool { return bytes.Equal(a[:], b[:]) } -func compareRoutingInfos(a, b []ExtraRoutingInfo) error { +func compareRouteHints(a, b []routing.HopHint) error { if len(a) != len(b) { return fmt.Errorf("expected len routingInfo %d, got %d", len(a), len(b)) } for i := 0; i < len(a); i++ { - if !comparePubkeys(a[i].PubKey, b[i].PubKey) { - return fmt.Errorf("expected routingInfo pubkey %x, "+ - "got %x", a[i].PubKey, b[i].PubKey) + if !comparePubkeys(a[i].NodeID, b[i].NodeID) { + return fmt.Errorf("expected routeHint nodeID %x, "+ + "got %x", a[i].NodeID, b[i].NodeID) } - if a[i].ShortChanID != b[i].ShortChanID { - return fmt.Errorf("expected routingInfo shortChanID "+ - "%d, got %d", a[i].ShortChanID, b[i].ShortChanID) + if a[i].ChannelID != b[i].ChannelID { + return fmt.Errorf("expected routeHint channelID "+ + "%d, got %d", a[i].ChannelID, b[i].ChannelID) } - if a[i].FeeBaseMsat != b[i].FeeBaseMsat { - return fmt.Errorf("expected routingInfo feeBaseMsat %d, got %d", - a[i].FeeBaseMsat, b[i].FeeBaseMsat) + if a[i].FeeBaseMSat != b[i].FeeBaseMSat { + return fmt.Errorf("expected routeHint feeBaseMsat %d, got %d", + a[i].FeeBaseMSat, b[i].FeeBaseMSat) } if a[i].FeeProportionalMillionths != b[i].FeeProportionalMillionths { - return fmt.Errorf("expected routingInfo feeProportionalMillionths %d, got %d", + return fmt.Errorf("expected routeHint feeProportionalMillionths %d, got %d", a[i].FeeProportionalMillionths, b[i].FeeProportionalMillionths) } - if a[i].CltvExpDelta != b[i].CltvExpDelta { - return fmt.Errorf("expected routingInfo cltvExpDelta "+ - "%d, got %d", a[i].CltvExpDelta, b[i].CltvExpDelta) + if a[i].CLTVExpiryDelta != b[i].CLTVExpiryDelta { + return fmt.Errorf("expected routeHint cltvExpiryDelta "+ + "%d, got %d", a[i].CLTVExpiryDelta, b[i].CLTVExpiryDelta) } }