From c54e6fc8031402ab7fdf7e4a06f9d268d81e2842 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Wed, 18 Oct 2017 22:14:54 -0700 Subject: [PATCH] zpay32: add new 'c' field to payreqs for specifying final cltv delta --- zpay32/invoice.go | 70 +++++++++++++++++++++++++++++++++++++++--- zpay32/invoice_test.go | 19 ++++++++++++ 2 files changed, 84 insertions(+), 5 deletions(-) diff --git a/zpay32/invoice.go b/zpay32/invoice.go index 9097197d..7841006c 100644 --- a/zpay32/invoice.go +++ b/zpay32/invoice.go @@ -8,6 +8,7 @@ import ( "time" "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/routing" "github.com/roasbeef/btcd/btcec" "github.com/roasbeef/btcd/chaincfg" "github.com/roasbeef/btcd/chaincfg/chainhash" @@ -60,12 +61,14 @@ const ( // fieldTypeR contains extra routing information. fieldTypeR = 3 + + // fieldTypeC contains an optional requested final CLTV delta. + fieldTypeC = 24 ) // MessageSigner is passed to the Encode method to provide a signature // corresponding to the node's pubkey. type MessageSigner struct { - // SignCompact signs the passed hash with the node's privkey. The // returned signature should be 65 bytes, where the last 64 are the // compact signature, and the first one is a header byte. This is the @@ -100,6 +103,18 @@ type Invoice struct { // and the pubkey will be extracted from the signature during decoding. Destination *btcec.PublicKey + // minFinalCLTVExpiry is the value that the creator of the invoice + // expects to be used for the + // + // NOTE: This value is optional, and should be set to nil if the + // invoice creator doesn't have a strong requirement on the CLTV expiry + // of the final HTLC extended to it. + // + // This field is un-exported and can only be read by the + // MinFinalCLTVExpiry() method. By forcing callers to read via this + // method, we can easily enforce the default if not specified. + minFinalCLTVExpiry *uint64 + // Description is a short description of the purpose of this invoice. // Optional. Non-nil iff DescriptionHash is nil. Description *string @@ -162,16 +177,27 @@ func Destination(destination *btcec.PublicKey) func(*Invoice) { // Description is a functional option that allows callers of NewInvoice to set // the payment description of the created Invoice. -// Note: Must be used if and only if DescriptionHash is not used. +// +// NOTE: Must be used if and only if DescriptionHash is not used. func Description(description string) func(*Invoice) { return func(i *Invoice) { i.Description = &description } } +// CLTVExpiry is an optional value which allows the receiver of the payment to +// specify the delta between the current height and the HTLC extended to the +// receiver. +func CLTVExpiry(delta uint64) func(*Invoice) { + return func(i *Invoice) { + i.minFinalCLTVExpiry = &delta + } +} + // DescriptionHash is a functional option that allows callers of NewInvoice to // set the payment description hash of the created Invoice. -// Note: Must be used if and only if Description is not used. +// +// NOTE: Must be used if and only if Description is not used. func DescriptionHash(descriptionHash [32]byte) func(*Invoice) { return func(i *Invoice) { i.DescriptionHash = &descriptionHash @@ -207,7 +233,8 @@ func RoutingInfo(routingInfo []ExtraRoutingInfo) func(*Invoice) { // NewInvoice creates a new Invoice object. The last parameter is a set of // variadic argumements for setting optional fields of the invoice. -// Note: Either Description or DescriptionHash must be provided for the Invoice +// +// NOTE: Either Description or DescriptionHash must be provided for the Invoice // to be considered valid. func NewInvoice(net *chaincfg.Params, paymentHash [32]byte, timestamp time.Time, options ...func(*Invoice)) (*Invoice, error) { @@ -459,6 +486,19 @@ func (invoice *Invoice) Expiry() time.Duration { return 3600 * time.Second } +// MinFinalCLTVExpiry returns the minimum final CLTV expiry delta as specified +// by the creator of the invoice. This value specifies the delta between the +// current height and the expiry height of the HTLC extended in the last hop. +func (invoice *Invoice) MinFinalCLTVExpiry() uint64 { + if invoice.minFinalCLTVExpiry != nil { + fmt.Println("USING SET CLTV") + return *invoice.minFinalCLTVExpiry + } + + fmt.Println("USING DEFAULT CLTV") + return routing.DefaultFinalCLTVDelta +} + // validateInvoice does a sanity check of the provided Invoice, making sure it // has all the necessary fields set for it to be considered valid by BOLT-0011. func validateInvoice(invoice *Invoice) error { @@ -651,6 +691,18 @@ func parseTaggedFields(invoice *Invoice, fields []byte, net *chaincfg.Params) er } dur := time.Duration(exp) * time.Second invoice.expiry = &dur + case fieldTypeC: + if invoice.minFinalCLTVExpiry != nil { + // We skip the field if we have already seen a + // supported one. + continue + } + + expiry, err := base32ToUint64(base32Data) + if err != nil { + return err + } + invoice.minFinalCLTVExpiry = &expiry case fieldTypeF: if invoice.FallbackAddr != nil { // We skip the field if we have already seen a @@ -675,7 +727,7 @@ func parseTaggedFields(invoice *Invoice, fields []byte, net *chaincfg.Params) er addr, err = btcutil.NewAddressWitnessScriptHash( witness, net) default: - return fmt.Errorf("unknow witness "+ + return fmt.Errorf("unknown witness "+ "program length: %d", len(witness)) } if err != nil { @@ -798,6 +850,14 @@ func writeTaggedFields(bufferBase32 *bytes.Buffer, invoice *Invoice) error { } } + if invoice.minFinalCLTVExpiry != nil { + finalDelta := uint64ToBase32(uint64(*invoice.minFinalCLTVExpiry)) + err := writeTaggedField(bufferBase32, fieldTypeC, finalDelta) + if err != nil { + return err + } + } + if invoice.expiry != nil { seconds := invoice.expiry.Seconds() expiry := uint64ToBase32(uint64(seconds)) diff --git a/zpay32/invoice_test.go b/zpay32/invoice_test.go index 520927bf..b38ce6ab 100644 --- a/zpay32/invoice_test.go +++ b/zpay32/invoice_test.go @@ -432,6 +432,25 @@ func TestDecodeEncode(t *testing.T) { i.Destination = nil }, }, + { + // Send 2500uBTC for a cup of coffee with a custom CLTV + // expiry value. + encodedInvoice: "lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jscqzysnp4q0n326hr8v9zprg8gsvezcch06gfaqqhde2aj730yg0durunfhv66ysxkvnxhcvhz48sn72lp77h4fxcur27z0he48u5qvk3sxse9mr9jhkltt962s8arjnzk8rk59yj5nw4p495747gksj30gza0crhzwjcpgxzy00", + valid: true, + decodedInvoice: func() *zpay32.Invoice { + i, _ := zpay32.NewInvoice( + &chaincfg.MainNetParams, + testPaymentHash, + time.Unix(1496314658, 0), + zpay32.Amount(testMillisat2500uBTC), + zpay32.Description(testCupOfCoffee), + zpay32.Destination(testPubKey), + zpay32.CLTVExpiry(144), + ) + + return i + }, + }, } for i, test := range tests {