Skip to content

Commit

Permalink
zpay32: add BOLT 11 feature bits and test vectors
Browse files Browse the repository at this point in the history
  • Loading branch information
cfromknecht committed Aug 26, 2019
1 parent d182660 commit 86b5a6b
Show file tree
Hide file tree
Showing 2 changed files with 113 additions and 0 deletions.
53 changes: 53 additions & 0 deletions zpay32/invoice.go
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
}
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}
Expand Down
60 changes: 60 additions & 0 deletions zpay32/invoice_test.go
Expand Up @@ -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"

Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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
}

Expand Down

0 comments on commit 86b5a6b

Please sign in to comment.