From 2953f3532a606ded97ea15ccd7cce8e1137565c4 Mon Sep 17 00:00:00 2001 From: Conner Fromknecht Date: Mon, 26 Aug 2019 12:54:04 -0700 Subject: [PATCH 1/3] lnwire/features: add SerializeSize32 for base32 encodings --- lnwire/features.go | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/lnwire/features.go b/lnwire/features.go index 4cdbd054..875ef401 100644 --- a/lnwire/features.go +++ b/lnwire/features.go @@ -127,6 +127,20 @@ func (fv *RawFeatureVector) Unset(feature FeatureBit) { // SerializeSize returns the number of bytes needed to represent feature vector // in byte format. func (fv *RawFeatureVector) SerializeSize() int { + // We calculate byte-length via the largest bit index. + return fv.serializeSize(8) +} + +// SerializeSize32 returns the number of bytes needed to represent feature +// vector in base32 format. +func (fv *RawFeatureVector) SerializeSize32() int { + // We calculate base32-length via the largest bit index. + return fv.serializeSize(5) +} + +// serializeSize returns the number of bytes required to encode the feature +// vector using at most width bits per encoded byte. +func (fv *RawFeatureVector) serializeSize(width int) int { // Find the largest feature bit index max := -1 for feature := range fv.features { @@ -139,8 +153,7 @@ func (fv *RawFeatureVector) SerializeSize() int { return 0 } - // We calculate byte-length via the largest bit index - return max/8 + 1 + return max/width + 1 } // Encode writes the feature vector in byte representation. Every feature From 8c2176fbf81da275fd2e66f7da607185e5c335c3 Mon Sep 17 00:00:00 2001 From: Conner Fromknecht Date: Mon, 26 Aug 2019 13:07:21 -0700 Subject: [PATCH 2/3] lnwire/features: add EncodeBase32 and DecodeBase32 w/ generic helpers --- lnwire/features.go | 38 ++++++++++++++++++++++++++++++++------ 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/lnwire/features.go b/lnwire/features.go index 875ef401..e86a69ee 100644 --- a/lnwire/features.go +++ b/lnwire/features.go @@ -169,12 +169,25 @@ func (fv *RawFeatureVector) Encode(w io.Writer) error { return err } + return fv.encode(w, length, 8) +} + +// EncodeBase32 writes the feature vector in base32 representation. Every feature +// encoded as a bit, and the bit vector is serialized using the least number of +// bytes. +func (fv *RawFeatureVector) EncodeBase32(w io.Writer) error { + length := fv.SerializeSize32() + return fv.encode(w, length, 5) +} + +// encode writes the feature vector +func (fv *RawFeatureVector) encode(w io.Writer, length, width int) error { // Generate the data and write it. data := make([]byte, length) for feature := range fv.features { - byteIndex := int(feature / 8) - bitIndex := feature % 8 - data[length-byteIndex-1] |= 1 << bitIndex + byteIndex := int(feature) / width + bitIndex := int(feature) % width + data[length-byteIndex-1] |= 1 << uint(bitIndex) } _, err := w.Write(data) @@ -193,6 +206,19 @@ func (fv *RawFeatureVector) Decode(r io.Reader) error { } length := binary.BigEndian.Uint16(l[:]) + return fv.decode(r, int(length), 8) +} + +// DecodeBase32 reads the feature vector from its base32 representation. Every +// feature encoded as a bit, and the bit vector is serialized using the least +// number of bytes. +func (fv *RawFeatureVector) DecodeBase32(r io.Reader, length int) error { + return fv.decode(r, length, 5) +} + +// decode reads a feature vector from the next length bytes of the io.Reader, +// assuming each byte has width feature bits encoded per byte. +func (fv *RawFeatureVector) decode(r io.Reader, length, width int) error { // Read the feature vector data. data := make([]byte, length) if _, err := io.ReadFull(r, data); err != nil { @@ -200,10 +226,10 @@ func (fv *RawFeatureVector) Decode(r io.Reader) error { } // Set feature bits from parsed data. - bitsNumber := len(data) * 8 + bitsNumber := len(data) * width for i := 0; i < bitsNumber; i++ { - byteIndex := uint16(i / 8) - bitIndex := uint(i % 8) + byteIndex := int(i / width) + bitIndex := uint(i % width) if (data[length-byteIndex-1]>>bitIndex)&1 == 1 { fv.Set(FeatureBit(i)) } From 1311baf51fd9513c5e93a56f0974378ab5051d0d Mon Sep 17 00:00:00 2001 From: Conner Fromknecht Date: Mon, 26 Aug 2019 13:24:14 -0700 Subject: [PATCH 3/3] zpay32: add BOLT 11 feature bits and test vectors --- zpay32/invoice.go | 53 +++++++++++++++++++++++++++++++++++++ zpay32/invoice_test.go | 60 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 113 insertions(+) diff --git a/zpay32/invoice.go b/zpay32/invoice.go index fb79e3cc..f779bb48 100644 --- a/zpay32/invoice.go +++ b/zpay32/invoice.go @@ -67,6 +67,16 @@ const ( // fieldTypeC contains an optional requested final CLTV delta. fieldTypeC = 24 + + // fieldType9 contains one or more bytes for signaling features + // supported or required by the receiver. + fieldType9 = 5 +) + +var ( + // InvoiceFeatures holds the set of all known feature bits that are + // exposed as BOLT 11 features. + InvoiceFeatures = map[lnwire.FeatureBit]string{} ) // MessageSigner is passed to the Encode method to provide a signature @@ -146,6 +156,10 @@ type Invoice struct { // // NOTE: This is optional. RouteHints [][]HopHint + + // Features represents an optional field used to signal optional or + // required support for features by the receiver. + Features *lnwire.FeatureVector } // Amount is a functional option that allows callers of NewInvoice to set the @@ -663,6 +677,14 @@ func parseTaggedFields(invoice *Invoice, fields []byte, net *chaincfg.Params) er } invoice.RouteHints = append(invoice.RouteHints, routeHint) + case fieldType9: + if invoice.Features != nil { + // We skip the field if we have already seen a + // supported one. + continue + } + + invoice.Features, err = parseFeatures(base32Data) default: // Ignore unknown type. } @@ -874,6 +896,25 @@ func parseRouteHint(data []byte) ([]HopHint, error) { return routeHint, nil } +// parseFeatures decodes any feature bits directly from the base32 +// representation. +func parseFeatures(data []byte) (*lnwire.FeatureVector, error) { + rawFeatures := lnwire.NewRawFeatureVector() + err := rawFeatures.DecodeBase32(bytes.NewReader(data), len(data)) + if err != nil { + return nil, err + } + + fv := lnwire.NewFeatureVector(rawFeatures, InvoiceFeatures) + unknownFeatures := fv.UnknownRequiredFeatures() + if len(unknownFeatures) > 0 { + return nil, fmt.Errorf("invoice contains unknown required "+ + "features: %v", unknownFeatures) + } + + return fv, nil +} + // writeTaggedFields writes the non-nil tagged fields of the Invoice to the // base32 buffer. func writeTaggedFields(bufferBase32 *bytes.Buffer, invoice *Invoice) error { @@ -1024,6 +1065,18 @@ func writeTaggedFields(bufferBase32 *bytes.Buffer, invoice *Invoice) error { return err } } + if invoice.Features != nil && invoice.Features.SerializeSize32() > 0 { + var b bytes.Buffer + err := invoice.Features.RawFeatureVector.EncodeBase32(&b) + if err != nil { + return err + } + + err = writeTaggedField(bufferBase32, fieldType9, b.Bytes()) + if err != nil { + return err + } + } return nil } diff --git a/zpay32/invoice_test.go b/zpay32/invoice_test.go index ce69e3a4..9b75fab7 100644 --- a/zpay32/invoice_test.go +++ b/zpay32/invoice_test.go @@ -24,12 +24,14 @@ import ( var ( testMillisat24BTC = lnwire.MilliSatoshi(2400000000000) testMillisat2500uBTC = lnwire.MilliSatoshi(250000000) + testMillisat25mBTC = lnwire.MilliSatoshi(2500000000) testMillisat20mBTC = lnwire.MilliSatoshi(2000000000) testPaymentHashSlice, _ = hex.DecodeString("0001020304050607080900010203040506070809000102030405060708090102") testEmptyString = "" testCupOfCoffee = "1 cup coffee" + testCoffeeBeans = "coffee beans" testCupOfNonsense = "ナンセンス 1杯" testPleaseConsider = "Please consider supporting this project" @@ -468,6 +470,59 @@ func TestDecodeEncode(t *testing.T) { i.Destination = nil }, }, + { + // On mainnet, please send $30 coffee beans supporting + // features 1 and 9. + encodedInvoice: "lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdees9qzsze992adudgku8p05pstl6zh7av6rx2f297pv89gu5q93a0hf3g7lynl3xq56t23dpvah6u7y9qey9lccrdml3gaqwc6nxsl5ktzm464sq73t7cl", + valid: true, + decodedInvoice: func() *Invoice { + return &Invoice{ + Net: &chaincfg.MainNetParams, + MilliSat: &testMillisat25mBTC, + Timestamp: time.Unix(1496314658, 0), + PaymentHash: &testPaymentHash, + Description: &testCoffeeBeans, + Destination: testPubKey, + Features: lnwire.NewFeatureVector( + lnwire.NewRawFeatureVector(1, 9), + InvoiceFeatures, + ), + } + }, + beforeEncoding: func(i *Invoice) { + // Since this destination pubkey was recovered + // from the signature, we must set it nil before + // encoding to get back the same invoice string. + i.Destination = nil + }, + }, + { + // On mainnet, please send $30 coffee beans supporting + // features 1, 9, and 100. + encodedInvoice: "lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdees9q4pqqqqqqqqqqqqqqqqqqszk3ed62snp73037h4py4gry05eltlp0uezm2w9ajnerhmxzhzhsu40g9mgyx5v3ad4aqwkmvyftzk4k9zenz90mhjcy9hcevc7r3lx2sphzfxz7", + valid: false, + skipEncoding: true, + decodedInvoice: func() *Invoice { + return &Invoice{ + Net: &chaincfg.MainNetParams, + MilliSat: &testMillisat25mBTC, + Timestamp: time.Unix(1496314658, 0), + PaymentHash: &testPaymentHash, + Description: &testCoffeeBeans, + Destination: testPubKey, + Features: lnwire.NewFeatureVector( + lnwire.NewRawFeatureVector(1, 9, 100), + InvoiceFeatures, + ), + } + }, + beforeEncoding: func(i *Invoice) { + // Since this destination pubkey was recovered + // from the signature, we must set it nil before + // encoding to get back the same invoice string. + i.Destination = nil + }, + }, { // On mainnet, with fallback (p2wpkh) address bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 encodedInvoice: "lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsfppqw508d6qejxtdg4y5r3zarvary0c5xw7kknt6zz5vxa8yh8jrnlkl63dah48yh6eupakk87fjdcnwqfcyt7snnpuz7vp83txauq4c60sys3xyucesxjf46yqnpplj0saq36a554cp9wt865", @@ -814,6 +869,11 @@ func compareInvoices(expected, actual *Invoice) error { } } + if !reflect.DeepEqual(expected.Features, actual.Features) { + return fmt.Errorf("expected features %v, got %v", + expected.Features.RawFeatureVector, actual.Features.RawFeatureVector) + } + return nil }