Merge pull request #3432 from cfromknecht/bolt11-features

zpay32: BOLT 11 Feature Bits
This commit is contained in:
Johan T. Halseth 2019-08-28 15:23:13 +02:00 committed by GitHub
commit 2026d3b45e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 160 additions and 8 deletions

@ -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
@ -156,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)
@ -180,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 {
@ -187,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))
}

@ -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
}

@ -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
}