Merge pull request #3432 from cfromknecht/bolt11-features
zpay32: BOLT 11 Feature Bits
This commit is contained in:
commit
2026d3b45e
@ -127,6 +127,20 @@ func (fv *RawFeatureVector) Unset(feature FeatureBit) {
|
|||||||
// SerializeSize returns the number of bytes needed to represent feature vector
|
// SerializeSize returns the number of bytes needed to represent feature vector
|
||||||
// in byte format.
|
// in byte format.
|
||||||
func (fv *RawFeatureVector) SerializeSize() int {
|
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
|
// Find the largest feature bit index
|
||||||
max := -1
|
max := -1
|
||||||
for feature := range fv.features {
|
for feature := range fv.features {
|
||||||
@ -139,8 +153,7 @@ func (fv *RawFeatureVector) SerializeSize() int {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// We calculate byte-length via the largest bit index
|
return max/width + 1
|
||||||
return max/8 + 1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Encode writes the feature vector in byte representation. Every feature
|
// 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 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.
|
// Generate the data and write it.
|
||||||
data := make([]byte, length)
|
data := make([]byte, length)
|
||||||
for feature := range fv.features {
|
for feature := range fv.features {
|
||||||
byteIndex := int(feature / 8)
|
byteIndex := int(feature) / width
|
||||||
bitIndex := feature % 8
|
bitIndex := int(feature) % width
|
||||||
data[length-byteIndex-1] |= 1 << bitIndex
|
data[length-byteIndex-1] |= 1 << uint(bitIndex)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := w.Write(data)
|
_, err := w.Write(data)
|
||||||
@ -180,6 +206,19 @@ func (fv *RawFeatureVector) Decode(r io.Reader) error {
|
|||||||
}
|
}
|
||||||
length := binary.BigEndian.Uint16(l[:])
|
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.
|
// Read the feature vector data.
|
||||||
data := make([]byte, length)
|
data := make([]byte, length)
|
||||||
if _, err := io.ReadFull(r, data); err != nil {
|
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.
|
// Set feature bits from parsed data.
|
||||||
bitsNumber := len(data) * 8
|
bitsNumber := len(data) * width
|
||||||
for i := 0; i < bitsNumber; i++ {
|
for i := 0; i < bitsNumber; i++ {
|
||||||
byteIndex := uint16(i / 8)
|
byteIndex := int(i / width)
|
||||||
bitIndex := uint(i % 8)
|
bitIndex := uint(i % width)
|
||||||
if (data[length-byteIndex-1]>>bitIndex)&1 == 1 {
|
if (data[length-byteIndex-1]>>bitIndex)&1 == 1 {
|
||||||
fv.Set(FeatureBit(i))
|
fv.Set(FeatureBit(i))
|
||||||
}
|
}
|
||||||
|
@ -67,6 +67,16 @@ const (
|
|||||||
|
|
||||||
// fieldTypeC contains an optional requested final CLTV delta.
|
// fieldTypeC contains an optional requested final CLTV delta.
|
||||||
fieldTypeC = 24
|
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
|
// MessageSigner is passed to the Encode method to provide a signature
|
||||||
@ -146,6 +156,10 @@ type Invoice struct {
|
|||||||
//
|
//
|
||||||
// NOTE: This is optional.
|
// NOTE: This is optional.
|
||||||
RouteHints [][]HopHint
|
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
|
// 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)
|
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:
|
default:
|
||||||
// Ignore unknown type.
|
// Ignore unknown type.
|
||||||
}
|
}
|
||||||
@ -874,6 +896,25 @@ func parseRouteHint(data []byte) ([]HopHint, error) {
|
|||||||
return routeHint, nil
|
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
|
// writeTaggedFields writes the non-nil tagged fields of the Invoice to the
|
||||||
// base32 buffer.
|
// base32 buffer.
|
||||||
func writeTaggedFields(bufferBase32 *bytes.Buffer, invoice *Invoice) error {
|
func writeTaggedFields(bufferBase32 *bytes.Buffer, invoice *Invoice) error {
|
||||||
@ -1024,6 +1065,18 @@ func writeTaggedFields(bufferBase32 *bytes.Buffer, invoice *Invoice) error {
|
|||||||
return err
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -24,12 +24,14 @@ import (
|
|||||||
var (
|
var (
|
||||||
testMillisat24BTC = lnwire.MilliSatoshi(2400000000000)
|
testMillisat24BTC = lnwire.MilliSatoshi(2400000000000)
|
||||||
testMillisat2500uBTC = lnwire.MilliSatoshi(250000000)
|
testMillisat2500uBTC = lnwire.MilliSatoshi(250000000)
|
||||||
|
testMillisat25mBTC = lnwire.MilliSatoshi(2500000000)
|
||||||
testMillisat20mBTC = lnwire.MilliSatoshi(2000000000)
|
testMillisat20mBTC = lnwire.MilliSatoshi(2000000000)
|
||||||
|
|
||||||
testPaymentHashSlice, _ = hex.DecodeString("0001020304050607080900010203040506070809000102030405060708090102")
|
testPaymentHashSlice, _ = hex.DecodeString("0001020304050607080900010203040506070809000102030405060708090102")
|
||||||
|
|
||||||
testEmptyString = ""
|
testEmptyString = ""
|
||||||
testCupOfCoffee = "1 cup coffee"
|
testCupOfCoffee = "1 cup coffee"
|
||||||
|
testCoffeeBeans = "coffee beans"
|
||||||
testCupOfNonsense = "ナンセンス 1杯"
|
testCupOfNonsense = "ナンセンス 1杯"
|
||||||
testPleaseConsider = "Please consider supporting this project"
|
testPleaseConsider = "Please consider supporting this project"
|
||||||
|
|
||||||
@ -468,6 +470,59 @@ func TestDecodeEncode(t *testing.T) {
|
|||||||
i.Destination = nil
|
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
|
// On mainnet, with fallback (p2wpkh) address bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4
|
||||||
encodedInvoice: "lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsfppqw508d6qejxtdg4y5r3zarvary0c5xw7kknt6zz5vxa8yh8jrnlkl63dah48yh6eupakk87fjdcnwqfcyt7snnpuz7vp83txauq4c60sys3xyucesxjf46yqnpplj0saq36a554cp9wt865",
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user