zpay32: modify an invoice's r field to allow multiple routes

Before this commit, if an invoice encoded multiple `r` fields, we would
decode them as one single route. We fix this by allowing an invoice to
store multiple routes.
This commit is contained in:
Wilmer Paulino 2018-03-27 00:42:35 -04:00
parent beb08b3887
commit ea9e609ef0
No known key found for this signature in database
GPG Key ID: 6DF57B9F9514972F
3 changed files with 141 additions and 121 deletions

@ -37,9 +37,9 @@ const (
// with zeroes. // with zeroes.
pubKeyBase32Len = 53 pubKeyBase32Len = 53
// routingInfoLen is the number of bytes needed to encode the extra // hopHintLen is the number of bytes needed to encode the hop hint of a
// routing info of a single private route. // single private route.
routingInfoLen = 51 hopHintLen = 51
// The following byte values correspond to the supported field types. // The following byte values correspond to the supported field types.
// The field name is the character representing that 5-bit value in the // The field name is the character representing that 5-bit value in the
@ -141,31 +141,12 @@ type Invoice struct {
// Optional. // Optional.
FallbackAddr btcutil.Address FallbackAddr btcutil.Address
// RoutingInfo is one or more entries containing extra routing // RouteHints represents one or more different route hints. Each route
// information for a private route to the target node. // hint can be individually used to reach the destination. These usually
// Optional. // represent private routes.
RoutingInfo []ExtraRoutingInfo //
} // NOTE: This is optional.
RouteHints [][]routing.HopHint
// 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
} }
// Amount is a functional option that allows callers of NewInvoice to set the // 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 // RouteHint is a functional option that allows callers of NewInvoice to add
// one or more entries containing extra routing information for a private route // one or more hop hints that represent a private route to the destination.
// to the target node. func RouteHint(routeHint []routing.HopHint) func(*Invoice) {
func RoutingInfo(routingInfo []ExtraRoutingInfo) func(*Invoice) {
return func(i *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") return fmt.Errorf("neither description nor description hash set")
} }
// Can have at most 20 extra hops for routing. // We'll restrict invoices to include up to 20 different private route
if len(invoice.RoutingInfo) > 20 { // hints. We do this to avoid overly large invoices.
return fmt.Errorf("too many extra hops: %d", if len(invoice.RouteHints) > 20 {
len(invoice.RoutingInfo)) 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. // 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) invoice.FallbackAddr, err = parseFallbackAddr(base32Data, net)
case fieldTypeR: case fieldTypeR:
if invoice.RoutingInfo != nil { // An `r` field can be included in an invoice multiple
// We skip the field if we have already seen a // times, so we won't skip it if we have already seen
// supported one. // one.
continue routeHint, err := parseRouteHint(base32Data)
if err != nil {
return err
} }
invoice.RoutingInfo, err = parseRoutingInfo(base32Data) invoice.RouteHints = append(invoice.RouteHints, routeHint)
default: default:
// Ignore unknown type. // Ignore unknown type.
} }
@ -850,35 +841,38 @@ func parseFallbackAddr(data []byte, net *chaincfg.Params) (btcutil.Address, erro
return addr, nil return addr, nil
} }
// parseRoutingInfo converts the data (encoded in base32) into an array // parseRouteHint converts the data (encoded in base32) into an array containing
// containing one or more entries of extra routing info. // one or more routing hop hints that represent a single route hint.
func parseRoutingInfo(data []byte) ([]ExtraRoutingInfo, error) { func parseRouteHint(data []byte) ([]routing.HopHint, error) {
base256Data, err := bech32.ConvertBits(data, 5, 8, false) base256Data, err := bech32.ConvertBits(data, 5, 8, false)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if len(base256Data)%routingInfoLen != 0 { if len(base256Data)%hopHintLen != 0 {
return nil, fmt.Errorf("expected length multiple of %d bytes, got %d", return nil, fmt.Errorf("expected length multiple of %d bytes, "+
routingInfoLen, len(base256Data)) "got %d", hopHintLen, len(base256Data))
} }
var routingInfo []ExtraRoutingInfo var routeHint []routing.HopHint
info := ExtraRoutingInfo{}
for len(base256Data) > 0 { 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 { if err != nil {
return nil, err return nil, err
} }
info.ShortChanID = binary.BigEndian.Uint64(base256Data[33:41]) hopHint.ChannelID = binary.BigEndian.Uint64(base256Data[33:41])
info.FeeBaseMsat = binary.BigEndian.Uint32(base256Data[41:45]) hopHint.FeeBaseMSat = binary.BigEndian.Uint32(base256Data[41:45])
info.FeeProportionalMillionths = binary.BigEndian.Uint32(base256Data[45:49]) hopHint.FeeProportionalMillionths = binary.BigEndian.Uint32(base256Data[45:49])
info.CltvExpDelta = binary.BigEndian.Uint16(base256Data[49:51]) hopHint.CLTVExpiryDelta = binary.BigEndian.Uint16(base256Data[49:51])
routingInfo = append(routingInfo, info)
routeHint = append(routeHint, hopHint)
base256Data = base256Data[51:] base256Data = base256Data[51:]
} }
return routingInfo, nil return routeHint, nil
} }
// writeTaggedFields writes the non-nil tagged fields of the Invoice to the // 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 { for _, routeHint := range invoice.RouteHints {
// Each extra routing info is encoded using 51 bytes. // Each hop hint is encoded using 51 bytes, so we'll make to
routingDataBase256 := make([]byte, 0, 51*len(invoice.RoutingInfo)) // sure to allocate enough space for the whole route hint.
for _, r := range invoice.RoutingInfo { routeHintBase256 := make([]byte, 0, hopHintLen*len(routeHint))
base256 := make([]byte, 51)
copy(base256[:33], r.PubKey.SerializeCompressed()) for _, hopHint := range routeHint {
binary.BigEndian.PutUint64(base256[33:41], r.ShortChanID) hopHintBase256 := make([]byte, hopHintLen)
binary.BigEndian.PutUint32(base256[41:45], r.FeeBaseMsat) copy(hopHintBase256[:33], hopHint.NodeID.SerializeCompressed())
binary.BigEndian.PutUint32(base256[45:49], r.FeeProportionalMillionths) binary.BigEndian.PutUint64(
binary.BigEndian.PutUint16(base256[49:51], r.CltvExpDelta) hopHintBase256[33:41], hopHint.ChannelID,
routingDataBase256 = append(routingDataBase256, base256...) )
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 { if err != nil {
return err return err
} }
err = writeTaggedField(bufferBase32, fieldTypeR, routingDataBase32) err = writeTaggedField(bufferBase32, fieldTypeR, routeHintBase32)
if err != nil { if err != nil {
return err return err
} }

@ -8,6 +8,7 @@ import (
"time" "time"
"github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/routing"
"github.com/roasbeef/btcd/btcec" "github.com/roasbeef/btcd/btcec"
"github.com/roasbeef/btcd/chaincfg" "github.com/roasbeef/btcd/chaincfg"
"github.com/roasbeef/btcutil" "github.com/roasbeef/btcutil"
@ -706,18 +707,18 @@ func TestParseFallbackAddr(t *testing.T) {
} }
} }
// TestParseRoutingInfo checks that the routing info is properly parsed. // TestParseRouteHint checks that the routing info is properly parsed.
func TestParseRoutingInfo(t *testing.T) { func TestParseRouteHint(t *testing.T) {
t.Parallel() t.Parallel()
var testSingleHopData []byte var testSingleHopData []byte
for _, r := range testSingleHop { for _, r := range testSingleHop {
base256 := make([]byte, 51) base256 := make([]byte, 51)
copy(base256[:33], r.PubKey.SerializeCompressed()) copy(base256[:33], r.NodeID.SerializeCompressed())
binary.BigEndian.PutUint64(base256[33:41], r.ShortChanID) binary.BigEndian.PutUint64(base256[33:41], r.ChannelID)
binary.BigEndian.PutUint32(base256[41:45], r.FeeBaseMsat) binary.BigEndian.PutUint32(base256[41:45], r.FeeBaseMSat)
binary.BigEndian.PutUint32(base256[45:49], r.FeeProportionalMillionths) 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 = append(testSingleHopData, base256...)
} }
testSingleHopData, _ = bech32.ConvertBits(testSingleHopData, 8, 5, true) testSingleHopData, _ = bech32.ConvertBits(testSingleHopData, 8, 5, true)
@ -725,11 +726,11 @@ func TestParseRoutingInfo(t *testing.T) {
var testDoubleHopData []byte var testDoubleHopData []byte
for _, r := range testDoubleHop { for _, r := range testDoubleHop {
base256 := make([]byte, 51) base256 := make([]byte, 51)
copy(base256[:33], r.PubKey.SerializeCompressed()) copy(base256[:33], r.NodeID.SerializeCompressed())
binary.BigEndian.PutUint64(base256[33:41], r.ShortChanID) binary.BigEndian.PutUint64(base256[33:41], r.ChannelID)
binary.BigEndian.PutUint32(base256[41:45], r.FeeBaseMsat) binary.BigEndian.PutUint32(base256[41:45], r.FeeBaseMSat)
binary.BigEndian.PutUint32(base256[45:49], r.FeeProportionalMillionths) 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 = append(testDoubleHopData, base256...)
} }
testDoubleHopData, _ = bech32.ConvertBits(testDoubleHopData, 8, 5, true) testDoubleHopData, _ = bech32.ConvertBits(testDoubleHopData, 8, 5, true)
@ -737,7 +738,7 @@ func TestParseRoutingInfo(t *testing.T) {
tests := []struct { tests := []struct {
data []byte data []byte
valid bool valid bool
result []ExtraRoutingInfo result []routing.HopHint
}{ }{
{ {
data: []byte{0x0, 0x0, 0x0, 0x0}, data: []byte{0x0, 0x0, 0x0, 0x0},
@ -746,7 +747,7 @@ func TestParseRoutingInfo(t *testing.T) {
{ {
data: []byte{}, data: []byte{},
valid: true, valid: true,
result: []ExtraRoutingInfo{}, result: []routing.HopHint{},
}, },
{ {
data: testSingleHopData, data: testSingleHopData,
@ -765,13 +766,13 @@ func TestParseRoutingInfo(t *testing.T) {
} }
for i, test := range tests { for i, test := range tests {
routingInfo, err := parseRoutingInfo(test.data) routeHint, err := parseRouteHint(test.data)
if (err == nil) != test.valid { if (err == nil) != test.valid {
t.Errorf("routing info decoding test %d failed: %v", i, err) t.Errorf("routing info decoding test %d failed: %v", i, err)
return return
} }
if test.valid { 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) t.Fatalf("test %d failed decoding routing info: %v", i, err)
} }
} }

@ -12,6 +12,7 @@ import (
"time" "time"
"github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/routing"
"github.com/roasbeef/btcd/btcec" "github.com/roasbeef/btcd/btcec"
"github.com/roasbeef/btcd/chaincfg" "github.com/roasbeef/btcd/chaincfg"
"github.com/roasbeef/btcd/chaincfg/chainhash" "github.com/roasbeef/btcd/chaincfg/chainhash"
@ -47,34 +48,34 @@ var (
testAddrMainnetP2WPKH, _ = btcutil.DecodeAddress("bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4", &chaincfg.MainNetParams) testAddrMainnetP2WPKH, _ = btcutil.DecodeAddress("bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4", &chaincfg.MainNetParams)
testAddrMainnetP2WSH, _ = btcutil.DecodeAddress("bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3", &chaincfg.MainNetParams) testAddrMainnetP2WSH, _ = btcutil.DecodeAddress("bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3", &chaincfg.MainNetParams)
testRoutingInfoPubkeyBytes, _ = hex.DecodeString("029e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255") testHopHintPubkeyBytes1, _ = hex.DecodeString("029e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255")
testRoutingInfoPubkey, _ = btcec.ParsePubKey(testRoutingInfoPubkeyBytes, btcec.S256()) testHopHintPubkey1, _ = btcec.ParsePubKey(testHopHintPubkeyBytes1, btcec.S256())
testRoutingInfoPubkeyBytes2, _ = hex.DecodeString("039e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255") testHopHintPubkeyBytes2, _ = hex.DecodeString("039e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255")
testRoutingInfoPubkey2, _ = btcec.ParsePubKey(testRoutingInfoPubkeyBytes2, btcec.S256()) testHopHintPubkey2, _ = btcec.ParsePubKey(testHopHintPubkeyBytes2, btcec.S256())
testSingleHop = []ExtraRoutingInfo{ testSingleHop = []routing.HopHint{
{ {
PubKey: testRoutingInfoPubkey, NodeID: testHopHintPubkey1,
ShortChanID: 0x0102030405060708, ChannelID: 0x0102030405060708,
FeeBaseMsat: 0, FeeBaseMSat: 0,
FeeProportionalMillionths: 20, FeeProportionalMillionths: 20,
CltvExpDelta: 3, CLTVExpiryDelta: 3,
}, },
} }
testDoubleHop = []ExtraRoutingInfo{ testDoubleHop = []routing.HopHint{
{ {
PubKey: testRoutingInfoPubkey, NodeID: testHopHintPubkey1,
ShortChanID: 0x0102030405060708, ChannelID: 0x0102030405060708,
FeeBaseMsat: 1, FeeBaseMSat: 1,
FeeProportionalMillionths: 20, FeeProportionalMillionths: 20,
CltvExpDelta: 3, CLTVExpiryDelta: 3,
}, },
{ {
PubKey: testRoutingInfoPubkey2, NodeID: testHopHintPubkey2,
ShortChanID: 0x030405060708090a, ChannelID: 0x030405060708090a,
FeeBaseMsat: 2, FeeBaseMSat: 2,
FeeProportionalMillionths: 30, FeeProportionalMillionths: 30,
CltvExpDelta: 4, CLTVExpiryDelta: 4,
}, },
} }
@ -413,7 +414,7 @@ func TestDecodeEncode(t *testing.T) {
DescriptionHash: &testDescriptionHash, DescriptionHash: &testDescriptionHash,
Destination: testPubKey, Destination: testPubKey,
FallbackAddr: testRustyAddr, FallbackAddr: testRustyAddr,
RoutingInfo: testSingleHop, RouteHints: [][]routing.HopHint{testSingleHop},
} }
}, },
beforeEncoding: func(i *Invoice) { beforeEncoding: func(i *Invoice) {
@ -436,7 +437,7 @@ func TestDecodeEncode(t *testing.T) {
DescriptionHash: &testDescriptionHash, DescriptionHash: &testDescriptionHash,
Destination: testPubKey, Destination: testPubKey,
FallbackAddr: testRustyAddr, FallbackAddr: testRustyAddr,
RoutingInfo: testDoubleHop, RouteHints: [][]routing.HopHint{testDoubleHop},
} }
}, },
beforeEncoding: func(i *Invoice) { beforeEncoding: func(i *Invoice) {
@ -680,7 +681,7 @@ func TestNewInvoice(t *testing.T) {
Amount(testMillisat20mBTC), Amount(testMillisat20mBTC),
DescriptionHash(testDescriptionHash), DescriptionHash(testDescriptionHash),
FallbackAddr(testRustyAddr), FallbackAddr(testRustyAddr),
RoutingInfo(testDoubleHop), RouteHint(testDoubleHop),
) )
}, },
valid: true, valid: true,
@ -802,7 +803,19 @@ func compareInvoices(expected, actual *Invoice) error {
expected.FallbackAddr, actual.FallbackAddr) 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 { func comparePubkeys(a, b *btcec.PublicKey) bool {
@ -831,36 +844,36 @@ func compareHashes(a, b *[32]byte) bool {
return bytes.Equal(a[:], b[:]) return bytes.Equal(a[:], b[:])
} }
func compareRoutingInfos(a, b []ExtraRoutingInfo) error { func compareRouteHints(a, b []routing.HopHint) error {
if len(a) != len(b) { if len(a) != len(b) {
return fmt.Errorf("expected len routingInfo %d, got %d", return fmt.Errorf("expected len routingInfo %d, got %d",
len(a), len(b)) len(a), len(b))
} }
for i := 0; i < len(a); i++ { for i := 0; i < len(a); i++ {
if !comparePubkeys(a[i].PubKey, b[i].PubKey) { if !comparePubkeys(a[i].NodeID, b[i].NodeID) {
return fmt.Errorf("expected routingInfo pubkey %x, "+ return fmt.Errorf("expected routeHint nodeID %x, "+
"got %x", a[i].PubKey, b[i].PubKey) "got %x", a[i].NodeID, b[i].NodeID)
} }
if a[i].ShortChanID != b[i].ShortChanID { if a[i].ChannelID != b[i].ChannelID {
return fmt.Errorf("expected routingInfo shortChanID "+ return fmt.Errorf("expected routeHint channelID "+
"%d, got %d", a[i].ShortChanID, b[i].ShortChanID) "%d, got %d", a[i].ChannelID, b[i].ChannelID)
} }
if a[i].FeeBaseMsat != b[i].FeeBaseMsat { if a[i].FeeBaseMSat != b[i].FeeBaseMSat {
return fmt.Errorf("expected routingInfo feeBaseMsat %d, got %d", return fmt.Errorf("expected routeHint feeBaseMsat %d, got %d",
a[i].FeeBaseMsat, b[i].FeeBaseMsat) a[i].FeeBaseMSat, b[i].FeeBaseMSat)
} }
if a[i].FeeProportionalMillionths != b[i].FeeProportionalMillionths { 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) a[i].FeeProportionalMillionths, b[i].FeeProportionalMillionths)
} }
if a[i].CltvExpDelta != b[i].CltvExpDelta { if a[i].CLTVExpiryDelta != b[i].CLTVExpiryDelta {
return fmt.Errorf("expected routingInfo cltvExpDelta "+ return fmt.Errorf("expected routeHint cltvExpiryDelta "+
"%d, got %d", a[i].CltvExpDelta, b[i].CltvExpDelta) "%d, got %d", a[i].CLTVExpiryDelta, b[i].CLTVExpiryDelta)
} }
} }