lnd.xprv/invoice/invoice.go

934 lines
26 KiB
Go
Raw Normal View History

package invoice
import (
"bytes"
"encoding/binary"
"fmt"
"strings"
"time"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/roasbeef/btcd/btcec"
"github.com/roasbeef/btcd/chaincfg"
"github.com/roasbeef/btcd/chaincfg/chainhash"
"github.com/roasbeef/btcutil"
"github.com/roasbeef/btcutil/bech32"
)
const (
// mSatPerBtc is the number of millisatoshis in 1 BTC.
mSatPerBtc = 100000000000
// signatureBase32Len is the number of 5-bit groups needed to encode
// the 512 bit signature + 8 bit recovery ID.
signatureBase32Len = 104
// timestampBase32Len is the number of 5-bit groups needed to encode
// the 35-bit timestamp.
timestampBase32Len = 7
// hashBase32Len is the number of 5-bit groups needed to encode a
// 256-bit hash. Note that the last group will be padded with zeroes.
hashBase32Len = 52
// pubKeyBase32Len is the number of 5-bit groups needed to encode a
// 33-byte compressed pubkey. Note that the last group will be padded
// with zeroes.
pubKeyBase32Len = 53
// The following byte values correspond to the supported field types.
// The field name is the character representing that 5-bit value in the
// bech32 string.
// fieldTypeP is the field containing the payment hash.
fieldTypeP = 1
// fieldTypeD contains a short description of the payment.
fieldTypeD = 13
// fieldTypeN contains the pubkey of the target node.
fieldTypeN = 19
// fieldTypeH contains the hash of a description of the payment.
fieldTypeH = 23
// fieldTypeX contains the expiry in seconds of the invoice.
fieldTypeX = 6
// fieldTypeF contains a fallback on-chain address.
fieldTypeF = 9
// fieldTypeR contains extra routing information.
fieldTypeR = 3
)
// 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
// format returned by btcec.SignCompact.
SignCompact func(hash []byte) ([]byte, error)
}
// Invoice represents a decoded invoice, or to-be-encoded invoice. Some of the
// fields are optional, and will only be non-nil if the invoice this was parsed
// from contains that field. When encoding, only the non-nil fields will be
// added to the encoded invoice.
type Invoice struct {
// Net specifies what network this Lightning invoice is meant for.
Net *chaincfg.Params
// MilliSat specifies the amount of this invoice in millisatoshi.
// Optional.
MilliSat *lnwire.MilliSatoshi
// Timestamp specifies the time this invoice was created.
// Mandatory
Timestamp time.Time
// PaymentHash is the payment hash to be used for a payment to this
// invoice.
PaymentHash *[32]byte
// Destination is the public key of the target node. This will always
// be set after decoding, and can optionally be set before encoding to
// include the pubkey as an 'n' field. If this is not set before
// encoding then the destination pubkey won't be added as an 'n' field,
// and the pubkey will be extracted from the signature during decoding.
Destination *btcec.PublicKey
// Description is a short description of the purpose of this invoice.
// Optional. Non-nil iff DescriptionHash is nil.
Description *string
// DescriptionHash is the SHA256 hash of a description of the purpose of
// this invoice.
// Optional. Non-nil iff Description is nil.
DescriptionHash *[32]byte
// Expiry specifies the timespan this invoice will be valid.
// Optional. If not set, a default expiry of 60 min will be implied.
Expiry *time.Time
// FallbackAddr is an on-chain address that can be used for payment in
// case the Lightning payment fails.
// Optional.
FallbackAddr btcutil.Address
// RoutingInfo is one or more entries containing extra routing
// information for a private route to the target node.
// Optional.
RoutingInfo []ExtraRoutingInfo
}
// ExtraRoutingInfo holds the information needed to route a payment along one
// private channel.
type ExtraRoutingInfo struct {
// PubKey is the public key of the node at the start of this channel.
PubKey *btcec.PublicKey
// ShortChanID is the channel ID of the channel.
ShortChanID uint64
// Fee is the fee required for routing along this channel.
Fee uint64
// CltvExpDelta is this channel's cltv expiry delta.
CltvExpDelta uint16
}
// Amount is a functional option that allows callers of NewInvoice to set the
// amount in millisatoshis that the Invoice should encode.
func Amount(milliSat lnwire.MilliSatoshi) func(*Invoice) {
return func(i *Invoice) {
i.MilliSat = &milliSat
}
}
// Destination is a functional option that allows callers of NewInvoice to
// explicitly set the pubkey of the Invoice's destination node.
func Destination(destination *btcec.PublicKey) func(*Invoice) {
return func(i *Invoice) {
i.Destination = destination
}
}
// 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.
func Description(description string) func(*Invoice) {
return func(i *Invoice) {
i.Description = &description
}
}
// 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.
func DescriptionHash(descriptionHash [32]byte) func(*Invoice) {
return func(i *Invoice) {
i.DescriptionHash = &descriptionHash
}
}
// Expiry is a functional option that allows callers of NewInvoice to set the
// expiry of the created Invoice. If not set, a default expiry of 60 min will
// be implied.
func Expiry(expiry time.Time) func(*Invoice) {
return func(i *Invoice) {
i.Expiry = &expiry
}
}
// FallbackAddr is a functional option that allows callers of NewInvoice to set
// the Invoice's fallback on-chain address that can be used for payment in case
// the Lightning payment fails
func FallbackAddr(fallbackAddr btcutil.Address) func(*Invoice) {
return func(i *Invoice) {
i.FallbackAddr = fallbackAddr
}
}
// RoutingInfo is a functional option that allows callers of NewInvoice to set
// one or more entries containing extra routing information for a private route
// to the target node.
func RoutingInfo(routingInfo []ExtraRoutingInfo) func(*Invoice) {
return func(i *Invoice) {
i.RoutingInfo = routingInfo
}
}
// 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
// to be considered valid.
func NewInvoice(net *chaincfg.Params, paymentHash [32]byte,
timestamp time.Time, options ...func(*Invoice)) (*Invoice, error) {
invoice := &Invoice{
Net: net,
PaymentHash: &paymentHash,
Timestamp: timestamp,
}
for _, option := range options {
option(invoice)
}
if err := validateInvoice(invoice); err != nil {
return nil, err
}
return invoice, nil
}
// Decode parses the provided encoded invoice, and returns a decoded Invoice in
// case it is valid by BOLT-0011.
func Decode(invoice string) (*Invoice, error) {
decodedInvoice := Invoice{}
// Decode the invoice using the modified bech32 decoder.
hrp, data, err := decodeBech32(invoice)
if err != nil {
return nil, err
}
// We expect the human-readable part to at least have ln + two chars
// encoding the network.
if len(hrp) < 4 {
return nil, fmt.Errorf("hrp too short")
}
// First two characters of HRP should be "ln".
if hrp[:2] != "ln" {
return nil, fmt.Errorf("prefix should be \"ln\"")
}
// The next characters should be a valid prefix for a segwit BIP173
// address. This will also determine which network this invoice is
// meant for.
var net *chaincfg.Params
if strings.HasPrefix(hrp[2:], chaincfg.MainNetParams.Bech32HRPSegwit) {
net = &chaincfg.MainNetParams
} else if strings.HasPrefix(hrp[2:], chaincfg.TestNet3Params.Bech32HRPSegwit) {
net = &chaincfg.TestNet3Params
} else if strings.HasPrefix(hrp[2:], chaincfg.SimNetParams.Bech32HRPSegwit) {
net = &chaincfg.SimNetParams
} else {
return nil, fmt.Errorf("unknown network")
}
decodedInvoice.Net = net
// Optionally, if there's anything left of the HRP, it encodes the
// payment amount.
if len(hrp) > 4 {
amount, err := decodeAmount(hrp[4:])
if err != nil {
return nil, err
}
decodedInvoice.MilliSat = &amount
}
// Everything except the last 520 bits of the data encodes the invoice's
// timestamp and tagged fields.
invoiceData := data[:len(data)-signatureBase32Len]
// Parse the timestamp and tagged fields, and fill the Invoice struct.
if err := parseData(&decodedInvoice, invoiceData, net); err != nil {
return nil, err
}
// The last 520 bits (104 groups) make up the signature.
sigBase32 := data[len(data)-signatureBase32Len:]
sigBase256, err := bech32.ConvertBits(sigBase32, 5, 8, true)
if err != nil {
return nil, err
}
var sigBytes [64]byte
copy(sigBytes[:], sigBase256[:64])
recoveryID := sigBase256[64]
// The signature is over the hrp + the data the invoice, encoded in
// base 256.
taggedDataBytes, err := bech32.ConvertBits(invoiceData, 5, 8, true)
if err != nil {
return nil, err
}
toSign := append([]byte(hrp), taggedDataBytes...)
// We expect the signature to be over the single SHA-256 hash of that
// data.
hash := chainhash.HashB(toSign)
// If the destination pubkey was provided as a tagged field, use that
// to verify the signature, if not do public key recovery.
if decodedInvoice.Destination != nil {
var signature *btcec.Signature
err := lnwire.DeserializeSigFromWire(&signature, sigBytes)
if err != nil {
return nil, fmt.Errorf("unable to deserialize "+
"signature: %v", err)
}
if !signature.Verify(hash, decodedInvoice.Destination) {
return nil, fmt.Errorf("invalid invoice signature")
}
} else {
headerByte := recoveryID + 27 + 4
compactSign := append([]byte{headerByte}, sigBytes[:]...)
pubkey, _, err := btcec.RecoverCompact(btcec.S256(),
compactSign, hash)
if err != nil {
return nil, err
}
decodedInvoice.Destination = pubkey
}
// Now that we have created the invoice, make sure it has the required
// fields set.
if err := validateInvoice(&decodedInvoice); err != nil {
return nil, err
}
return &decodedInvoice, nil
}
// Encode takes the given MessageSigner and returns a string encoding this
// invoice signed by the node key of the signer.
func (invoice *Invoice) Encode(signer MessageSigner) (string, error) {
// First check that this invoice is valid before starting the encoding.
if err := validateInvoice(invoice); err != nil {
return "", err
}
// The buffer will encoded the invoice data using 5-bit groups (base32).
var bufferBase32 bytes.Buffer
// The timestamp will be encoded using 35 bits, in base32.
timestampBase32 := uint64ToBase32(uint64(invoice.Timestamp.Unix()))
// The timestamp must be exactly 35 bits, which means 7 groups. If it
// can fit into fewer groups we add leading zero groups, if it is too
// big we fail early, as there is not possible to encode it.
if len(timestampBase32) > timestampBase32Len {
return "", fmt.Errorf("timestamp too big: %d",
invoice.Timestamp.Unix())
}
// Add zero bytes to the first timestampBase32Len-len(timestampBase32)
// groups, then add the non-zero groups.
zeroes := make([]byte, timestampBase32Len-len(timestampBase32),
timestampBase32Len-len(timestampBase32))
_, err := bufferBase32.Write(zeroes)
if err != nil {
return "", fmt.Errorf("unable to write to buffer: %v", err)
}
_, err = bufferBase32.Write(timestampBase32)
if err != nil {
return "", fmt.Errorf("unable to write to buffer: %v", err)
}
// We now write the tagged fields to the buffer, which will fill the
// rest of the data part before the signature.
if err := writeTaggedFields(&bufferBase32, invoice); err != nil {
return "", err
}
// The human-readable part (hrp) is "ln" + net hrp + optional amount.
hrp := "ln" + invoice.Net.Bech32HRPSegwit
if invoice.MilliSat != nil {
// Encode the amount using the fewest possible characters.
am, err := encodeAmount(*invoice.MilliSat)
if err != nil {
return "", err
}
hrp += am
}
// The signature is over the single SHA-256 hash of the hrp + the
// tagged fields encoded in base256.
taggedFieldsBytes, err := bech32.ConvertBits(bufferBase32.Bytes(), 5, 8, true)
if err != nil {
return "", err
}
toSign := append([]byte(hrp), taggedFieldsBytes...)
hash := chainhash.HashB(toSign)
// We use compact signature format, and also encoded the recovery ID
// such that a reader of the invoice can recover our pubkey from the
// signature.
sign, err := signer.SignCompact(hash)
if err != nil {
return "", err
}
// From the header byte we can extract the recovery ID, and the last 64
// bytes encode the signature.
recoveryID := sign[0] - 27 - 4
var sigBytes [64]byte
copy(sigBytes[:], sign[1:])
// If the pubkey field was explicitly set, it must be set to the pubkey
// used to create the signature.
if invoice.Destination != nil {
var signature *btcec.Signature
err = lnwire.DeserializeSigFromWire(&signature, sigBytes)
if err != nil {
return "", fmt.Errorf("unable to deserialize "+
"signature: %v", err)
}
valid := signature.Verify(hash, invoice.Destination)
if !valid {
return "", fmt.Errorf("signature does not match " +
"provided pubkey")
}
}
// Convert the signature to base32 before writing it to the buffer.
signBase32, err := bech32.ConvertBits(append(sigBytes[:], recoveryID), 8, 5, true)
if err != nil {
return "", err
}
bufferBase32.Write(signBase32)
// Now we can create the bech32 encoded string from the base32 buffer.
b32, err := bech32.Encode(hrp, bufferBase32.Bytes())
if err != nil {
return "", err
}
return b32, nil
}
// 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 {
// The net must be set.
if invoice.Net == nil {
return fmt.Errorf("net params not set")
}
// The invoice must contain a payment hash.
if invoice.PaymentHash == nil {
return fmt.Errorf("no payment hash found")
}
// Either Description or DescriptionHash must be set, not both.
if invoice.Description != nil && invoice.DescriptionHash != nil {
return fmt.Errorf("both description and description hash set")
}
if invoice.Description == nil && invoice.DescriptionHash == nil {
return fmt.Errorf("neither description nor description hash set")
}
// Can have at most 20 extra hops for routing.
if len(invoice.RoutingInfo) > 20 {
return fmt.Errorf("too many extra hops: %d",
len(invoice.RoutingInfo))
}
// Check that we support the field lengths.
if len(invoice.PaymentHash) != 32 {
return fmt.Errorf("unsupported payment hash length: %d",
len(invoice.PaymentHash))
}
if invoice.DescriptionHash != nil && len(invoice.DescriptionHash) != 32 {
return fmt.Errorf("unsupported description hash length: %d",
len(invoice.DescriptionHash))
}
if invoice.Destination != nil &&
len(invoice.Destination.SerializeCompressed()) != 33 {
return fmt.Errorf("unsupported pubkey length: %d",
len(invoice.Destination.SerializeCompressed()))
}
return nil
}
// parseData parses the data part of the invoice. It expects base32 data
// returned from the bech32.Decode method, except signature.
func parseData(invoice *Invoice, data []byte, net *chaincfg.Params) error {
// It must contain the timestamp, encoded using 35 bits (7 groups).
if len(data) < timestampBase32Len {
return fmt.Errorf("data too short: %d", len(data))
}
// Timestamp: 35 bits, 7 groups.
t, err := base32ToUint64(data[:7])
if err != nil {
return err
}
invoice.Timestamp = time.Unix(int64(t), 0)
// The rest are tagged parts.
tagData := data[7:]
if err := parseTaggedFields(invoice, tagData, net); err != nil {
return err
}
return nil
}
// parseTimestamp converts a 35-bit timestamp (encoded in base32) to uint64.
func parseTimestamp(data []byte) (uint64, error) {
if len(data) != 7 {
return 0, fmt.Errorf("timestamp must be 35 bits, was %d",
len(data)*5)
}
return base32ToUint64(data)
}
// parseTaggedFields takes the base32 encoded tagged fields of the invoice, and
// fills the Invoice struct accordingly.
func parseTaggedFields(invoice *Invoice, fields []byte, net *chaincfg.Params) error {
index := 0
for {
// If less than 3 groups less, it cannot possibly contain more
// interesting information, as we need the type (1 group) and
// length (2 groups).
if len(fields)-index < 3 {
break
}
typ := fields[index]
dataLength := uint16(fields[index+1]<<5) | uint16(fields[index+2])
// If we don't have enough field data left to read this length,
// return error.
if len(fields) < index+3+int(dataLength) {
return fmt.Errorf("invalid field length")
}
base32Data := fields[index+3 : index+3+int(dataLength)]
// Advance the index in preparation for the next iteration.
index += 3 + int(dataLength)
switch typ {
case fieldTypeP:
if invoice.PaymentHash != nil {
// We skip the field if we have already seen a
// supported one.
continue
}
if dataLength != hashBase32Len {
// Skipping unknown field length.
continue
}
hash, err := bech32.ConvertBits(base32Data, 5, 8, false)
if err != nil {
return err
}
var pHash [32]byte
copy(pHash[:], hash[:])
invoice.PaymentHash = &pHash
case fieldTypeD:
if invoice.Description != nil {
// We skip the field if we have already seen a
// supported one.
continue
}
base256Data, err := bech32.ConvertBits(base32Data, 5, 8,
false)
if err != nil {
return err
}
desc := string(base256Data)
invoice.Description = &desc
case fieldTypeN:
if invoice.Destination != nil {
// We skip the field if we have already seen a
// supported one.
continue
}
if len(base32Data) != pubKeyBase32Len {
// Skip unknown length.
continue
}
base256Data, err := bech32.ConvertBits(base32Data, 5, 8,
false)
if err != nil {
return err
}
invoice.Destination, err = btcec.ParsePubKey(base256Data,
btcec.S256())
if err != nil {
return err
}
case fieldTypeH:
if invoice.DescriptionHash != nil {
// We skip the field if we have already seen a
// supported one.
continue
}
if len(base32Data) != hashBase32Len {
// Skip unknown length.
continue
}
hash, err := bech32.ConvertBits(base32Data, 5, 8, false)
if err != nil {
return err
}
var dHash [32]byte
copy(dHash[:], hash[:])
invoice.DescriptionHash = &dHash
case fieldTypeX:
if invoice.Expiry != nil {
// We skip the field if we have already seen a
// supported one.
continue
}
exp, err := base32ToUint64(base32Data)
if err != nil {
return err
}
unix := time.Unix(int64(exp), 0)
invoice.Expiry = &unix
case fieldTypeF:
if invoice.FallbackAddr != nil {
// We skip the field if we have already seen a
// supported one.
continue
}
var addr btcutil.Address
version := base32Data[0]
switch version {
case 0:
witness, err := bech32.ConvertBits(
base32Data[1:], 5, 8, false)
if err != nil {
return err
}
switch len(witness) {
case 20:
addr, err = btcutil.NewAddressWitnessPubKeyHash(
witness, net)
case 32:
addr, err = btcutil.NewAddressWitnessScriptHash(
witness, net)
default:
return fmt.Errorf("unknow witness "+
"program length: %d", len(witness))
}
if err != nil {
return err
}
case 17:
pkHash, err := bech32.ConvertBits(base32Data[1:],
5, 8, false)
if err != nil {
return err
}
addr, err = btcutil.NewAddressPubKeyHash(pkHash,
net)
if err != nil {
return err
}
case 18:
scriptHash, err := bech32.ConvertBits(
base32Data[1:], 5, 8, false)
if err != nil {
return err
}
addr, err = btcutil.NewAddressScriptHashFromHash(
scriptHash, net)
if err != nil {
return err
}
default:
// Skipping unknown witness version.
continue
}
invoice.FallbackAddr = addr
case fieldTypeR:
if invoice.RoutingInfo != nil {
// We skip the field if we have already seen a
// supported one.
continue
}
base256Data, err := bech32.ConvertBits(base32Data, 5, 8,
false)
if err != nil {
return err
}
for len(base256Data) > 0 {
info := ExtraRoutingInfo{}
info.PubKey, err = btcec.ParsePubKey(
base256Data[:33], btcec.S256())
if err != nil {
return err
}
info.ShortChanID = binary.BigEndian.Uint64(
base256Data[33:41])
info.Fee = binary.BigEndian.Uint64(
base256Data[41:49])
info.CltvExpDelta = binary.BigEndian.Uint16(
base256Data[49:51])
invoice.RoutingInfo = append(
invoice.RoutingInfo, info)
base256Data = base256Data[51:]
}
default:
// Ignore unknown type.
}
}
return nil
}
// writeTaggedFields writes the non-nil tagged fields of the Invoice to the
// base32 buffer.
func writeTaggedFields(bufferBase32 *bytes.Buffer, invoice *Invoice) error {
if invoice.PaymentHash != nil {
// Convert 32 byte hash to 52 5-bit groups.
base32, err := bech32.ConvertBits(invoice.PaymentHash[:], 8, 5,
true)
if err != nil {
return err
}
if len(base32) != hashBase32Len {
return fmt.Errorf("invalid payment hash length: %d",
len(invoice.PaymentHash))
}
err = writeTaggedField(bufferBase32, fieldTypeP, base32)
if err != nil {
return err
}
}
if invoice.Description != nil {
base32, err := bech32.ConvertBits([]byte(*invoice.Description),
8, 5, true)
if err != nil {
return err
}
err = writeTaggedField(bufferBase32, fieldTypeD, base32)
if err != nil {
return err
}
}
if invoice.DescriptionHash != nil {
// Convert 32 byte hash to 52 5-bit groups.
descBase32, err := bech32.ConvertBits(
invoice.DescriptionHash[:], 8, 5, true)
if err != nil {
return err
}
if len(descBase32) != hashBase32Len {
return fmt.Errorf("invalid description hash length: %d",
len(invoice.DescriptionHash))
}
err = writeTaggedField(bufferBase32, fieldTypeH, descBase32)
if err != nil {
return err
}
}
if invoice.Expiry != nil {
unix := invoice.Expiry.Unix()
expiry := uint64ToBase32(uint64(unix))
err := writeTaggedField(bufferBase32, fieldTypeX, expiry)
if err != nil {
return err
}
}
if invoice.FallbackAddr != nil {
var version byte
switch addr := invoice.FallbackAddr.(type) {
case *btcutil.AddressPubKeyHash:
version = 17
case *btcutil.AddressScriptHash:
version = 18
case *btcutil.AddressWitnessPubKeyHash:
version = addr.WitnessVersion()
case *btcutil.AddressWitnessScriptHash:
version = addr.WitnessVersion()
default:
return fmt.Errorf("unknown fallback address type")
}
base32Addr, err := bech32.ConvertBits(
invoice.FallbackAddr.ScriptAddress(), 8, 5, true)
if err != nil {
return err
}
err = writeTaggedField(bufferBase32, fieldTypeF,
append([]byte{version}, base32Addr...))
if err != nil {
return err
}
}
if len(invoice.RoutingInfo) > 0 {
// Each extra routing info is encoded using 51 bytes.
routingDataBase256 := make([]byte, 0, 51*len(invoice.RoutingInfo))
for _, r := range invoice.RoutingInfo {
base256 := make([]byte, 51)
copy(base256[:33], r.PubKey.SerializeCompressed())
binary.BigEndian.PutUint64(base256[33:41], r.ShortChanID)
binary.BigEndian.PutUint64(base256[41:49], r.Fee)
binary.BigEndian.PutUint16(base256[49:51], r.CltvExpDelta)
routingDataBase256 = append(routingDataBase256, base256...)
}
routingDataBase32, err := bech32.ConvertBits(routingDataBase256,
8, 5, true)
if err != nil {
return err
}
err = writeTaggedField(bufferBase32, fieldTypeR, routingDataBase32)
if err != nil {
return err
}
}
if invoice.Destination != nil {
// Convert 33 byte pubkey to 53 5-bit groups.
pubKeyBase32, err := bech32.ConvertBits(
invoice.Destination.SerializeCompressed(), 8, 5, true)
if err != nil {
return nil
}
if len(pubKeyBase32) != pubKeyBase32Len {
return fmt.Errorf("invalid pubkey length: %d",
len(invoice.Destination.SerializeCompressed()))
}
err = writeTaggedField(bufferBase32, fieldTypeN, pubKeyBase32)
if err != nil {
return err
}
}
return nil
}
// writeTaggedField takes the type of a tagged data field, and the data of
// the tagged field (encoded in base32), and writes the type, length and data
// to the buffer.
func writeTaggedField(bufferBase32 *bytes.Buffer, dataType byte, data []byte) error {
// Length must be exactly 10 bits, so add leading zero groups if
// needed.
lenBase32 := uint64ToBase32(uint64(len(data)))
for len(lenBase32) < 2 {
lenBase32 = append([]byte{0}, lenBase32...)
}
if len(lenBase32) != 2 {
return fmt.Errorf("data length too big to fit within 10 bits: %d",
len(data))
}
err := bufferBase32.WriteByte(dataType)
if err != nil {
return fmt.Errorf("unable to write to buffer: %v", err)
}
_, err = bufferBase32.Write(lenBase32)
if err != nil {
return fmt.Errorf("unable to write to buffer: %v", err)
}
_, err = bufferBase32.Write(data)
if err != nil {
return fmt.Errorf("unable to write to buffer: %v", err)
}
return nil
}
// base32ToUint64 converts a base32 encoded number to uint64.
func base32ToUint64(data []byte) (uint64, error) {
// Maximum that fits in uint64 is 64 / 5 = 12 groups.
if len(data) > 12 {
return 0, fmt.Errorf("cannot parse data of length %d as uint64",
len(data))
}
val := uint64(0)
for i := 0; i < len(data); i++ {
val = val<<5 | uint64(data[i])
}
return val, nil
}
// uint64ToBase32 converts a uint64 to a base32 encoded integer encoded using
// as few 5-bit groups as possible.
func uint64ToBase32(num uint64) []byte {
// Return at least one group.
if num == 0 {
return []byte{0}
}
// To fit an uint64, we need at most is 64 / 5 = 12 groups.
arr := make([]byte, 12)
i := 12
for num > 0 {
i--
arr[i] = byte(num & uint64(31)) // 0b11111 in binary
num = num >> 5
}
// We only return non-zero leading groups.
return arr[i:]
}