diff --git a/glide.lock b/glide.lock index cad2d1c32..0dac65c72 100644 --- a/glide.lock +++ b/glide.lock @@ -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 diff --git a/glide.yaml b/glide.yaml index ffafc69e0..b99e90d5d 100644 --- a/glide.yaml +++ b/glide.yaml @@ -60,3 +60,4 @@ import: - package: github.com/btcsuite/btcd subpackages: - connmgr +- package: github.com/tv42/zbase32 diff --git a/zpay32/zbase32check.go b/zpay32/zbase32check.go new file mode 100644 index 000000000..ba69c4a0f --- /dev/null +++ b/zpay32/zbase32check.go @@ -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 +} diff --git a/zpay32/zbase32check_test.go b/zpay32/zbase32check_test.go new file mode 100644 index 000000000..5039fa5e7 --- /dev/null +++ b/zpay32/zbase32check_test.go @@ -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) + } +}