zpay32: create new package for encoding/decoding payment requests

This commit adds a new package “zpay32”: which is used within the
daemon to encode/decode payment requests. A payment request currently
consists of: the public key of the payee, the payment hash to use, and
finally the amount to send over the network. The encoded payment
request consists of the mentioned fields concatenated to each other, a
cc32 checksum is added, then the blob is finally encoded using zbas32.
I call the resulting scheme “zpay32”.

A number of extensions may be explored in future commits including
adding a version byte, adding “hint” routing information,
cryptographically signed receipts and more,
This commit is contained in:
Olaoluwa Osuntokun 2017-01-02 15:16:57 -08:00
parent d2e712c19d
commit bd0cf51581
No known key found for this signature in database
GPG Key ID: 9CC5B105D03521A2
4 changed files with 235 additions and 3 deletions

8
glide.lock generated

@ -1,5 +1,5 @@
hash: faa87f5b7fca5bd929174fa19119f484d9f8d8fbe132c41b5130b88e88498271
updated: 2016-12-30T16:43:39.303527204-08:00
hash: 7207127b46b6c3902703280eb7dd230a2feec69740fb084a1e2de6eb6c9f990f
updated: 2017-01-02T15:14:12.437819837-08:00
imports:
- name: github.com/aead/chacha20
version: 7e1038a97ad08a9a16cb88ed7a6778b366ba4d99
@ -108,6 +108,8 @@ imports:
- internal/helpers
- wallet/internal/txsizes
- internal/legacy/rename
- name: github.com/tv42/zbase32
version: 501572607d0273fc75b3b261fa4904d63f6ffa0e
- name: github.com/urfave/cli
version: 0bdeddeeb0f650497d603c4ad7b20cfe685682f6
- name: golang.org/x/crypto
@ -122,7 +124,7 @@ imports:
- pbkdf2
- ssh/terminal
- name: golang.org/x/net
version: 8fd7f25955530b92e73e9e1932a41b522b22ccd9
version: 69d4b8aa71caaaa75c3dfc11211d1be495abec7c
subpackages:
- context
- http2

@ -60,3 +60,4 @@ import:
- package: github.com/btcsuite/btcd
subpackages:
- connmgr
- package: github.com/tv42/zbase32

133
zpay32/zbase32check.go Normal file

@ -0,0 +1,133 @@
package zpay32
import (
"bytes"
"encoding/binary"
"errors"
"hash/crc32"
"io"
"github.com/roasbeef/btcd/btcec"
"github.com/roasbeef/btcutil"
"github.com/tv42/zbase32"
)
// invoiceSize is the size of an encoded invoice without the added check-sum.
// The size of broken down as follows: 33-bytes (destination pub key), 32-bytes
// (payment hash), 8-bytes for the payment amount in satoshis.
const invoiceSize = 33 + 32 + 8
// ErrCheckSumMismatch is returned byt he Decode function fi when
// decoding an encoded invoice, the checksum doesn't match indicating
// an error somewhere in the bitstream.
var ErrCheckSumMismatch = errors.New("the checksum is incorrect")
// PaymentRequest is a bare-bones invoice for a payment within the Lightning
// Network. With the details of the invoice, the sender has all the data
// necessary to send a payment to the recipient.
type PaymentRequest struct {
// Destination is the public key of the node to be paid.
Destination *btcec.PublicKey
// PaymentHash is the has to use within the HTLC extended throughout
// the payment path to the destination.
PaymentHash [32]byte
// Amount is the amount to be sent to the destination expressed in
// satoshis.
Amount btcutil.Amount
}
// castagnoli is an initialized crc32 checksum generated which Castagnoli's
// polynomial.
var castagnoli = crc32.MakeTable(crc32.Castagnoli)
// checkSum calculates a 4-byte crc32 checksum of the passed data. The returned
// uint32 is serialized as a big-endian integer.
func checkSum(data []byte) []byte {
crc := crc32.New(castagnoli)
crc.Write(data)
return crc.Sum(nil)
}
// Encode encodes the passed payment request using zbase32 with an added 4-byte
// crc32 checksum. The resulting encoding is 77-bytes long and consists of 124
// ASCII characters.
// TODO(roasbeef): add version byte?
func Encode(payReq *PaymentRequest) string {
var (
invoiceBytes [invoiceSize]byte
n int
)
// First copy each of the elements of the payment request into the
// buffer. Creating a stream that resembles: dest || r_hash || amt
n += copy(invoiceBytes[:], payReq.Destination.SerializeCompressed())
n += copy(invoiceBytes[n:], payReq.PaymentHash[:])
binary.BigEndian.PutUint64(invoiceBytes[n:], uint64(payReq.Amount))
// Next, we append the checksum to the end of the buffer which covers
// the serialized payment request.
b := append(invoiceBytes[:], checkSum(invoiceBytes[:])...)
// Finally encode the raw bytes as a zbase32 encoded string.
return zbase32.EncodeToString(b)
}
// Decode attempts to decode the zbase32 encoded payment request. If the
// trailing checksum doesn't match, then an error is returned.
func Decode(payData string) (*PaymentRequest, error) {
// First we decode the zbase32 encoded string into a series of raw
// bytes.
payReqBytes, err := zbase32.DecodeString(payData)
if err != nil {
return nil, err
}
// With the bytes decoded, we first verify the checksum to ensure the
// payment request wasn't altered in its decoded form.
invoiceBytes := payReqBytes[:invoiceSize]
generatedSum := checkSum(invoiceBytes)
// If the checksums don't match, then we return an error to the
// possibly detected error.
encodedSum := payReqBytes[invoiceSize:]
if !bytes.Equal(encodedSum, generatedSum) {
return nil, ErrCheckSumMismatch
}
// Otherwise, we've verified the integrity of the encoded payment
// request and can safely decode the payReq, passing it back up to the
// caller.
invoiceReader := bytes.NewReader(invoiceBytes)
return decodePaymentRequest(invoiceReader)
}
func decodePaymentRequest(r io.Reader) (*PaymentRequest, error) {
var err error
i := &PaymentRequest{}
var pubKey [33]byte
if _, err := io.ReadFull(r, pubKey[:]); err != nil {
return nil, err
}
i.Destination, err = btcec.ParsePubKey(pubKey[:], btcec.S256())
if err != nil {
return nil, err
}
if _, err := io.ReadFull(r, i.PaymentHash[:]); err != nil {
return nil, err
}
var amt [8]byte
if _, err := io.ReadFull(r, amt[:]); err != nil {
return nil, err
}
i.Amount = btcutil.Amount(binary.BigEndian.Uint64(amt[:]))
return i, nil
}

@ -0,0 +1,96 @@
package zpay32
import (
"bytes"
"testing"
"github.com/davecgh/go-spew/spew"
"github.com/roasbeef/btcd/btcec"
"github.com/roasbeef/btcutil"
)
var (
testPrivKey = []byte{
0x81, 0xb6, 0x37, 0xd8, 0xfc, 0xd2, 0xc6, 0xda,
0x63, 0x59, 0xe6, 0x96, 0x31, 0x13, 0xa1, 0x17,
0xd, 0xe7, 0x95, 0xe4, 0xb7, 0x25, 0xb8, 0x4d,
0x1e, 0xb, 0x4c, 0xfd, 0x9e, 0xc5, 0x8c, 0xe9,
}
_, testPubKey = btcec.PrivKeyFromBytes(btcec.S256(), testPrivKey)
testPayHash = [32]byte{
0xb7, 0x94, 0x38, 0x5f, 0x2d, 0x1e, 0xf7, 0xab,
0x4d, 0x92, 0x73, 0xd1, 0x90, 0x63, 0x81, 0xb4,
0x4f, 0x2f, 0x6f, 0x25, 0x88, 0xa3, 0xef, 0xb9,
0x6a, 0x49, 0x18, 0x83, 0x31, 0x98, 0x47, 0x53,
}
)
func TestEncodeDecode(t *testing.T) {
testPubKey.Curve = nil
tests := []struct {
version int
payReq PaymentRequest
encoding string
}{
{
payReq: PaymentRequest{
Destination: testPubKey,
PaymentHash: testPayHash,
Amount: btcutil.Amount(50000),
},
encoding: "yj8p9uh793syszrd4giu66gtsqprp47cc6cqbdo3" +
"qaxi7sr63y6bbphw8bx148zzipg3rh6t1btadpnxf7z1mnfd76" +
"hsw1eaoca3ot4uyyyyyyyyydbib998je6o",
version: 1,
},
}
for i, test := range tests {
// First ensure encoding the test payment request string in the
// specified encoding.
encodedReq := Encode(&test.payReq)
if encodedReq != test.encoding {
t.Fatalf("encoding mismatch for %v: expected %v got %v",
spew.Sdump(test.payReq), test.encoding, encodedReq)
}
// Next ensure the correctness of the transformation in the
// other direction.
decodedReq, err := Decode(test.encoding)
if err != nil {
t.Fatalf("unable to decode invoice #%v: %v", i, err)
}
if !test.payReq.Destination.IsEqual(decodedReq.Destination) {
t.Fatalf("test #%v:, destination mismatch for decoded request: "+
"expected %v got %v", i, test.payReq.Destination,
decodedReq.Destination)
}
if !bytes.Equal(test.payReq.PaymentHash[:], decodedReq.PaymentHash[:]) {
t.Fatalf("test #%v: payment hash mismatch for decoded request: "+
"expected %x got %x", i, test.payReq.PaymentHash,
decodedReq.PaymentHash)
}
if test.payReq.Amount != decodedReq.Amount {
t.Fatalf("test #%v: amount mismatch for decoded request: "+
"expected %x got %x", i, test.payReq.Amount,
decodedReq.Amount)
}
}
}
func TestChecksumMismatch(t *testing.T) {
// We start with a pre-encoded invoice, which has a valid checksum.
payReqString := []byte("ycyr8brdjic6oak3bemztc5nupo56y3itq4z5q4qxwb35orf7fmj5phw8bx148zzipg3rh6t1btadpnxf7z1mnfd76hsw1eaoca3ot4uyyyyyyyyydbibt79jo1o")
// To modify the resulting checksum, we shift a few of the bytes within the
// string itself.
payReqString[1] = 98
payReqString[5] = 102
if _, err := Decode(string(payReqString)); err != ErrCheckSumMismatch {
t.Fatalf("decode should fail with checksum mismatch, instead: %v", err)
}
}