diff --git a/sidecar/codec.go b/sidecar/codec.go new file mode 100644 index 000000000..0b7bb16fb --- /dev/null +++ b/sidecar/codec.go @@ -0,0 +1,96 @@ +package sidecar + +import ( + "bytes" + "crypto/sha256" + "encoding/base64" + "fmt" +) + +const ( + // checksumLen is the number of bytes we add as a checksum. We'll take + // this many bytes out of the full SHA256 hash and add it to the string + // encoded version of the ticket as an additional integrity check. + checksumLen = 4 +) + +var ( + // defaultStringEncoding is the default encoding algorithm we use to + // encode the gzipped binary data as a string. + defaultStringEncoding = base64.URLEncoding + + // sidecarPrefix is a specially prepared prefix that will spell out + // "sidecar" when encoded as base64. It will cause all our sidecar + // tickets to be easily recognizable with a human readable part. + sidecarPrefix = []byte{0xb2, 0x27, 0x5e, 0x71, 0xaa, 0xc0} +) + +// EncodeToString serializes and encodes the ticket as an URL safe string that +// contains a human readable prefix and a checksum. +func EncodeToString(t *Ticket) (string, error) { + var buf bytes.Buffer + + // Add a prefix to make the ticket easily recognizable by human eyes. + if _, err := buf.Write(sidecarPrefix); err != nil { + return "", err + } + + // Serialize the raw ticket itself. + if err := SerializeTicket(&buf, t); err != nil { + return "", err + } + + // Create a checksum (SHA256) of all the bytes so far (prefix + raw + // ticket) and add 4 bytes of that to the string encoded version. This + // will prevent the ticket from appearing valid even if parts of it were + // not copy/pasted correctly. + hash := sha256.New() + _, _ = hash.Write(buf.Bytes()) + checksum := hash.Sum(nil)[0:4] + if _, err := buf.Write(checksum); err != nil { + return "", err + } + + return defaultStringEncoding.EncodeToString(buf.Bytes()), nil +} + +// DecodeString decodes and then deserializes the given sidecar ticket string. +func DecodeString(s string) (*Ticket, error) { + rawBytes, err := defaultStringEncoding.DecodeString(s) + if err != nil { + return nil, err + } + + if len(rawBytes) < len(sidecarPrefix)+checksumLen { + return nil, fmt.Errorf("not a sidecar ticket, invalid length") + } + + totalLength := len(rawBytes) + prefixLength := len(sidecarPrefix) + payloadLength := totalLength - prefixLength - checksumLen + + prefix := rawBytes[0:prefixLength] + payload := rawBytes[prefixLength : prefixLength+payloadLength] + checksum := rawBytes[totalLength-checksumLen : totalLength] + + // Make sure the prefix matches our static prefix. + if !bytes.Equal(prefix, sidecarPrefix) { + return nil, fmt.Errorf("not a sicecar ticket, invalid prefix "+ + "%x", prefix) + } + + // Calculate the SHA256 sum of the prefix and payload and compare it to + // the checksum we also expect to be in the full string serialized + // ticket. + hash := sha256.New() + _, _ = hash.Write(prefix) + _, _ = hash.Write(payload) + calculatedChecksum := hash.Sum(nil)[0:4] + if !bytes.Equal(checksum, calculatedChecksum) { + return nil, fmt.Errorf("invalid sidecar ticket, checksum " + + "mismatch") + } + + // Everything's fine, let's decode the actual payload. + return DeserializeTicket(bytes.NewReader(payload)) +} diff --git a/sidecar/codec_test.go b/sidecar/codec_test.go new file mode 100644 index 000000000..fc27076cc --- /dev/null +++ b/sidecar/codec_test.go @@ -0,0 +1,68 @@ +package sidecar + +import ( + "math/big" + "testing" + + "github.com/btcsuite/btcd/btcec" + "github.com/stretchr/testify/require" +) + +var ( + hardcodedTicket = "sidecarAAQgHBgUEAwIBAAIBYwMBAgp5CwgAAAAAAAADCQwIAA" + + "AAAAAAA3gNIQLVLm5gAB7Vh7eEvz8Y3CkF2DsvNuSWJj8oOxp1iSh5xw5AAA" + + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAAAAAAAAAAAAAAAAAA" + + "AAAAAAAAAAAAAAAAAAAAAAFhRGFSEC1S5uYAAe1Ye3hL8_GNwpBdg7Lzbkli" + + "Y_KDsadYkoeccWIQMZ1hb2o3OyT-XUX7KWS-pAVBloIPQe8aCTqcjiNOV89x" + + "5kHyALFiEsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACBAAAAAAAAAAA" + + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGMAAAAAAAAAAAAAAAAAAAAAAAAAAA" + + "AAAAAAAAAAAAAAISgiKSBjWE0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + + "AAAA3VE-c=" +) + +// TestEncodeDecode tests that a ticket can be encoded and decoded from/to a +// string and back. +func TestEncodeDecode(t *testing.T) { + // Test with all struct members set. + ticketMaximal := &Ticket{ + ID: [8]byte{7, 6, 5, 4, 3, 2, 1, 0}, + Version: Version(99), + State: StateRegistered, + Offer: Offer{ + Capacity: 777, + PushAmt: 888, + SignPubKey: testPubKey, + SigOfferDigest: &btcec.Signature{ + R: new(big.Int).SetInt64(44), + S: new(big.Int).SetInt64(22), + }, + }, + Recipient: &Recipient{ + NodePubKey: testPubKey, + MultiSigPubKey: testPubKey2, + }, + Order: &Order{ + BidNonce: [32]byte{11, 22, 33, 44}, + SigOrderDigest: &btcec.Signature{ + R: new(big.Int).SetInt64(99), + S: new(big.Int).SetInt64(33), + }, + }, + Execution: &Execution{ + PendingChannelID: [32]byte{99, 88, 77}, + }, + } + + serialized, err := EncodeToString(ticketMaximal) + require.NoError(t, err) + + deserializedTicket, err := DecodeString(serialized) + require.NoError(t, err) + require.Equal(t, ticketMaximal, deserializedTicket) + + // Make sure nothing changed in the encoding without us noticing by + // comparing it to a hard coded version. + deserializedTicket, err = DecodeString(hardcodedTicket) + require.NoError(t, err) + require.Equal(t, ticketMaximal, deserializedTicket) +} diff --git a/sidecar/interface.go b/sidecar/interface.go new file mode 100644 index 000000000..9e49ad69f --- /dev/null +++ b/sidecar/interface.go @@ -0,0 +1,258 @@ +package sidecar + +import ( + "bytes" + "crypto/rand" + "crypto/sha256" + "fmt" + + "github.com/btcsuite/btcd/btcec" + "github.com/btcsuite/btcutil" + "github.com/lightningnetwork/lnd/lnwire" +) + +// Version is the version of the sidecar ticket format. +type Version uint8 + +const ( + // VersionDefault is the initial version of the ticket format. + VersionDefault Version = 0 +) + +// State is the state a sidecar ticket currently is in. Each updater of the +// ticket is responsible for also setting the state correctly after adding their +// data according to their role. +type State uint8 + +const ( + // StateCreated is the state a ticket is in after it's been first + // created and the offer information was added but not signed yet. + StateCreated State = 0 + + // StateOffered is the state a ticket is in after the offer data was + // signed by the sidecar provider. + StateOffered State = 1 + + // StateRegistered is the state a ticket is in after the recipient has + // registered it in their system and added their information. + StateRegistered State = 2 + + // StateOrdered is the state a ticket is in after the bid order for the + // sidecar channel was submitted and the order information was added to + // the ticket. The ticket now also contains a signature of the provider + // over the order digest. + StateOrdered State = 3 + + // StateExpectingChannel is the state a ticket is in after it's been + // returned to the channel recipient and their node was instructed to + // start listening for batch events for it. + StateExpectingChannel State = 4 + + // StateCompleted is the state a ticket is in after the sidecar channel + // was successfully opened and the bid order completed. + StateCompleted State = 5 + + // StateCanceled is the state a ticket is in after the sidecar channel + // bid order was canceled by the taker. + StateCanceled State = 6 +) + +// String returns the string representation of a sidecar ticket state. +func (s State) String() string { + switch s { + case StateCreated: + return "created" + + case StateOffered: + return "offered" + + case StateRegistered: + return "registered" + + case StateOrdered: + return "ordered" + + case StateExpectingChannel: + return "expecting channel" + + case StateCompleted: + return "completed" + + case StateCanceled: + return "canceled" + + default: + return fmt.Sprintf("unknown <%d>", s) + } +} + +// Offer is a struct holding the information that a sidecar channel provider is +// committing to when offering to buy a channel for the recipient. The sidecar +// channel flow is initiated by the provider creating a ticket and adding its +// signed offer to it. +type Offer struct { + // Capacity is the channel capacity of the sidecar channel in satoshis. + Capacity btcutil.Amount + + // PushAmt is the amount in satoshis that will be pushed to the + // recipient of the channel to kick start them with inbound capacity. + // If this is non-zero then the provider must pay for the push amount as + // well as all other costs from their Pool account. Makers can opt out + // of supporting push amounts when submitting ask orders so this will + // reduce the matching chances somewhat. + PushAmt btcutil.Amount + + // SignPubKey is the public key for corresponding to the private key + // that signed the SigOfferDigest below and, in a later state, the + // SigOrderDigest of the Order struct. + SignPubKey *btcec.PublicKey + + // SigOfferDigest is a signature over the offer digest, signed with the + // private key that corresponds to the SignPubKey above. + SigOfferDigest *btcec.Signature +} + +// Recipient is a struct holding the information about the recipient of the +// sidecar channel in question. +type Recipient struct { + // NodePubKey is the recipient nodes' identity public key that is + // advertised in the bid order. + NodePubKey *btcec.PublicKey + + // MultiSigPubKey is a public key to which the recipient node has the + // private key to. It is the key that will be used as one of the 2-of-2 + // multisig keys of the channel funding transaction output and is + // advertised in the bid order. + MultiSigPubKey *btcec.PublicKey +} + +// Order is a struct holding the information about the sidecar bid order after +// it's been submitted by the provider. +type Order struct { + // BidNonce is the order nonce of the bid order that was submitted for + // purchasing the sidecar channel. + BidNonce [32]byte + + // SigOrderDigest is a signature over the order digest, signed with the + // private key that corresponds to the SignPubKey in the Offer struct. + SigOrderDigest *btcec.Signature +} + +// Execution is a struct holding information about the sidecar bid order during +// its execution. +type Execution struct { + // PendingChannelID is the pending channel ID of the currently + // registered funding shim that was added by the recipient node to + // receive the channel. + PendingChannelID [32]byte +} + +// Ticket is a struct holding all the information for establishing/buying a +// sidecar channel. It is meant to be used in a PSBT like manner where each +// participant incrementally adds their information according to their role and +// the current step. +type Ticket struct { + // ID is a pseudorandom identifier of the ticket. + ID [8]byte + + // Version is the version of the ticket serialization format. + Version Version + + // State is the current state of the ticket. The state is updated by + // each participant and is therefore not covered in any of the signature + // digests. + State State + + // Offer contains the initial conditions offered by the sidecar channel + // provider. Every ticket must start with an offer and therefore this + // member can never be empty or nil. + Offer Offer + + // Recipient contains the information about the receiver node of the + // sidecar channel. This field must be set for all states greater or + // equal to StateRegistered. + Recipient *Recipient + + // Order contains the information about the order that was submitted for + // leasing the sidecar channel represented by this ticket. This field + // must be set for all states greater or equal to StateOrdered. + Order *Order + + // Execution contains the information about the execution part of the + // sidecar channel. This information is not meant to be exchanged + // between the participating parties but is included in the ticket to + // make it easy to serialize/deserialize a ticket state within the local + // database. + Execution *Execution +} + +// NewTicket creates a new sidecar ticket with the given version and offer +// information. +func NewTicket(version Version, capacity, pushAmt btcutil.Amount, + offerPubKey *btcec.PublicKey) (*Ticket, error) { + + t := &Ticket{ + Version: version, + State: StateOffered, + Offer: Offer{ + Capacity: capacity, + PushAmt: pushAmt, + SignPubKey: offerPubKey, + }, + } + + if _, err := rand.Read(t.ID[:]); err != nil { + return nil, err + } + + return t, nil +} + +// OfferDigest returns a message digest over the offer fields. +func (t *Ticket) OfferDigest() ([32]byte, error) { + var ( + msg bytes.Buffer + result [sha256.Size]byte + ) + switch t.Version { + case VersionDefault: + err := lnwire.WriteElements( + &msg, t.ID[:], uint8(t.Version), t.Offer.Capacity, + t.Offer.PushAmt, + ) + if err != nil { + return result, err + } + + default: + return result, fmt.Errorf("unknown version %d", t.Version) + } + return sha256.Sum256(msg.Bytes()), nil +} + +// OrderDigest returns a message digest over the order fields. +func (t *Ticket) OrderDigest() ([32]byte, error) { + var ( + msg bytes.Buffer + result [sha256.Size]byte + ) + + if t.State < StateOrdered || t.Order == nil { + return result, fmt.Errorf("invalid state for order digest") + } + + switch t.Version { + case VersionDefault: + err := lnwire.WriteElements( + &msg, t.ID[:], uint8(t.Version), t.Offer.Capacity, + t.Offer.PushAmt, t.Order.BidNonce[:], + ) + if err != nil { + return result, err + } + + default: + return result, fmt.Errorf("unknown version %d", t.Version) + } + return sha256.Sum256(msg.Bytes()), nil +} diff --git a/sidecar/tlv.go b/sidecar/tlv.go new file mode 100644 index 000000000..21304ea18 --- /dev/null +++ b/sidecar/tlv.go @@ -0,0 +1,386 @@ +package sidecar + +import ( + "bytes" + "fmt" + "io" + + "github.com/btcsuite/btcd/btcec" + "github.com/btcsuite/btcutil" + "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/tlv" +) + +const ( + idType tlv.Type = 1 + versionType tlv.Type = 2 + stateType tlv.Type = 3 + + offerType tlv.Type = 10 + capacityType tlv.Type = 11 + pushAmtType tlv.Type = 12 + signPubKeyType tlv.Type = 13 + sigOfferDigestType tlv.Type = 14 + + recipientType tlv.Type = 20 + nodePubKeyType tlv.Type = 21 + multiSigPubKeyType tlv.Type = 22 + + orderType tlv.Type = 30 + bidNonceType tlv.Type = 31 + sigOrderDigestType tlv.Type = 32 + + executionType tlv.Type = 40 + pendingChannelIDType tlv.Type = 41 +) + +var ( + // ZeroSignature is an empty signature with all bits set to zero. + ZeroSignature = [64]byte{} +) + +// SerializeTicket binary serializes the given ticket to the writer using the +// tlv format. +func SerializeTicket(w io.Writer, ticket *Ticket) error { + if ticket == nil { + return fmt.Errorf("ticket cannot be nil") + } + + var ( + version = uint8(ticket.Version) + state = uint8(ticket.State) + offerBytes []byte + err error + ) + + offerBytes, err = serializeOffer(ticket.Offer) + if err != nil { + return err + } + + tlvRecords := []tlv.Record{ + tlv.MakeStaticRecord(idType, &ticket.ID, 8, EBytes8, DBytes8), + tlv.MakePrimitiveRecord(versionType, &version), + tlv.MakePrimitiveRecord(stateType, &state), + tlv.MakePrimitiveRecord(offerType, &offerBytes), + } + + if ticket.Recipient != nil { + recipientBytes, err := serializeRecipient(*ticket.Recipient) + if err != nil { + return err + } + tlvRecords = append(tlvRecords, tlv.MakePrimitiveRecord( + recipientType, &recipientBytes, + )) + } + + if ticket.Order != nil { + orderBytes, err := serializeOrder(*ticket.Order) + if err != nil { + return err + } + tlvRecords = append(tlvRecords, tlv.MakePrimitiveRecord( + orderType, &orderBytes, + )) + } + + if ticket.Execution != nil { + executionBytes, err := serializeExecution(*ticket.Execution) + if err != nil { + return err + } + tlvRecords = append(tlvRecords, tlv.MakePrimitiveRecord( + executionType, &executionBytes, + )) + } + + tlvStream, err := tlv.NewStream(tlvRecords...) + if err != nil { + return err + } + + return tlvStream.Encode(w) +} + +// DeserializeTicket deserializes a ticket from the given reader, expecting the +// data to be encoded in the tlv format. +func DeserializeTicket(r io.Reader) (*Ticket, error) { + var ( + ticket = &Ticket{} + version, state uint8 + offerBytes, recipientBytes []byte + orderBytes, executionBytes []byte + ) + + tlvStream, err := tlv.NewStream( + tlv.MakeStaticRecord(idType, &ticket.ID, 8, EBytes8, DBytes8), + tlv.MakePrimitiveRecord(versionType, &version), + tlv.MakePrimitiveRecord(stateType, &state), + tlv.MakePrimitiveRecord(offerType, &offerBytes), + tlv.MakePrimitiveRecord(recipientType, &recipientBytes), + tlv.MakePrimitiveRecord(orderType, &orderBytes), + tlv.MakePrimitiveRecord(executionType, &executionBytes), + ) + if err != nil { + return nil, err + } + + parsedTypes, err := tlvStream.DecodeWithParsedTypes(r) + if err != nil { + return nil, err + } + + ticket.Version = Version(version) + ticket.State = State(state) + + if t, ok := parsedTypes[offerType]; ok && t == nil { + ticket.Offer, err = deserializeOffer(offerBytes) + if err != nil { + return nil, err + } + } + + if t, ok := parsedTypes[recipientType]; ok && t == nil { + recipient, err := deserializeRecipient(recipientBytes) + if err != nil { + return nil, err + } + ticket.Recipient = &recipient + } + + if t, ok := parsedTypes[orderType]; ok && t == nil { + order, err := deserializeOrder(orderBytes) + if err != nil { + return nil, err + } + ticket.Order = &order + } + + if t, ok := parsedTypes[executionType]; ok && t == nil { + execution, err := deserializeExecution(executionBytes) + if err != nil { + return nil, err + } + ticket.Execution = &execution + } + + return ticket, nil +} + +// serializeOffer serializes the given offer to a byte slice using the tlv +// format. +func serializeOffer(o Offer) ([]byte, error) { + capacity := uint64(o.Capacity) + pushAmt := uint64(o.PushAmt) + + tlvRecords := []tlv.Record{ + tlv.MakePrimitiveRecord(capacityType, &capacity), + tlv.MakePrimitiveRecord(pushAmtType, &pushAmt), + } + + if o.SignPubKey != nil { + tlvRecords = append(tlvRecords, tlv.MakePrimitiveRecord( + signPubKeyType, &o.SignPubKey, + )) + } + + if o.SigOfferDigest != nil { + tlvRecords = append(tlvRecords, tlv.MakeStaticRecord( + sigOfferDigestType, &o.SigOfferDigest, 64, ESig, DSig, + )) + } + + return encodeBytes(tlvRecords...) +} + +// deserializeOffer deserializes an order from the given byte slice, expecting +// the data to be encoded in the tlv format. +func deserializeOffer(offerBytes []byte) (Offer, error) { + var ( + o = Offer{} + capacity, pushAmt uint64 + ) + + if err := decodeBytes( + offerBytes, + tlv.MakePrimitiveRecord(capacityType, &capacity), + tlv.MakePrimitiveRecord(pushAmtType, &pushAmt), + tlv.MakePrimitiveRecord(signPubKeyType, &o.SignPubKey), + tlv.MakeStaticRecord( + sigOfferDigestType, &o.SigOfferDigest, 64, ESig, DSig, + ), + ); err != nil { + return o, err + } + + o.Capacity = btcutil.Amount(capacity) + o.PushAmt = btcutil.Amount(pushAmt) + + return o, nil +} + +// serializeRecipient serializes the given recipient to a byte slice using the +// tlv format. +func serializeRecipient(r Recipient) ([]byte, error) { + var tlvRecords []tlv.Record + if r.NodePubKey != nil { + tlvRecords = append(tlvRecords, tlv.MakePrimitiveRecord( + nodePubKeyType, &r.NodePubKey, + )) + } + + if r.MultiSigPubKey != nil { + tlvRecords = append(tlvRecords, tlv.MakePrimitiveRecord( + multiSigPubKeyType, &r.MultiSigPubKey, + )) + } + + return encodeBytes(tlvRecords...) +} + +// deserializeRecipient deserializes a recipient from the given byte slice, +// expecting the data to be encoded in the tlv format. +func deserializeRecipient(recipientBytes []byte) (Recipient, error) { + r := Recipient{} + return r, decodeBytes( + recipientBytes, + tlv.MakePrimitiveRecord(nodePubKeyType, &r.NodePubKey), + tlv.MakePrimitiveRecord(multiSigPubKeyType, &r.MultiSigPubKey), + ) +} + +// serializeOrder serializes the given order to a byte slice using the tlv +// format. +func serializeOrder(o Order) ([]byte, error) { + tlvRecords := []tlv.Record{ + tlv.MakePrimitiveRecord(bidNonceType, &o.BidNonce), + } + + if o.SigOrderDigest != nil { + tlvRecords = append(tlvRecords, tlv.MakeStaticRecord( + sigOrderDigestType, &o.SigOrderDigest, 64, ESig, DSig, + )) + } + + return encodeBytes(tlvRecords...) +} + +// deserializeOrder deserializes an order from the given byte slice, expecting +// the data to be encoded in the tlv format. +func deserializeOrder(orderBytes []byte) (Order, error) { + o := Order{} + return o, decodeBytes( + orderBytes, + tlv.MakePrimitiveRecord(bidNonceType, &o.BidNonce), + tlv.MakeStaticRecord( + sigOrderDigestType, &o.SigOrderDigest, 64, ESig, DSig, + ), + ) +} + +// serializeExecution serializes the given execution to a byte slice using the +// tlv format. +func serializeExecution(e Execution) ([]byte, error) { + return encodeBytes(tlv.MakePrimitiveRecord( + pendingChannelIDType, &e.PendingChannelID, + )) +} + +// deserializeExecution deserializes an execution from the given byte slice, +// expecting the data to be encoded in the tlv format. +func deserializeExecution(executionBytes []byte) (Execution, error) { + e := Execution{} + return e, decodeBytes(executionBytes, tlv.MakePrimitiveRecord( + pendingChannelIDType, &e.PendingChannelID, + )) +} + +// ESig is an Encoder for the btcec.Signature type. An error is returned if val +// is not a **btcec.Signature. +func ESig(w io.Writer, val interface{}, buf *[8]byte) error { + if v, ok := val.(**btcec.Signature); ok { + var s [64]byte + if *v != nil { + rawSig := (*v).Serialize() + sig, err := lnwire.NewSigFromRawSignature(rawSig) + if err != nil { + return err + } + copy(s[:], sig[:]) + } + + return tlv.EBytes64(w, &s, buf) + } + return tlv.NewTypeForEncodingErr(val, "*btcec.Signature") +} + +// DSig is a Decoder for the btcec.Signature type. An error is returned if val +// is not a **btcec.Signature. +func DSig(r io.Reader, val interface{}, buf *[8]byte, l uint64) error { + if v, ok := val.(**btcec.Signature); ok && l == 64 { + var s [64]byte + if err := tlv.DBytes64(r, &s, buf, 64); err != nil { + return err + } + + if s != ZeroSignature { + wireSig := lnwire.Sig(s) + ecSig, err := wireSig.ToSignature() + if err != nil { + return err + } + + *v = ecSig + } + + return nil + } + return tlv.NewTypeForEncodingErr(val, "*btcec.Signature") +} + +// EBytes8 is an Encoder for 8-byte arrays. An error is returned if val is not +// a *[8]byte. +func EBytes8(w io.Writer, val interface{}, _ *[8]byte) error { + if b, ok := val.(*[8]byte); ok { + _, err := w.Write(b[:]) + return err + } + return tlv.NewTypeForEncodingErr(val, "[8]byte") +} + +// DBytes8 is a Decoder for 8-byte arrays. An error is returned if val is not +// a *[8]byte. +func DBytes8(r io.Reader, val interface{}, _ *[8]byte, l uint64) error { + if b, ok := val.(*[8]byte); ok && l == 8 { + _, err := io.ReadFull(r, b[:]) + return err + } + return tlv.NewTypeForDecodingErr(val, "[8]byte", l, 8) +} + +// encodeBytes encodes the given tlv records into a byte slice. +func encodeBytes(tlvRecords ...tlv.Record) ([]byte, error) { + tlvStream, err := tlv.NewStream(tlvRecords...) + if err != nil { + return nil, err + } + + var buf bytes.Buffer + if err := tlvStream.Encode(&buf); err != nil { + return nil, err + } + + return buf.Bytes(), nil +} + +// decodeBytes decodes the given byte slice interpreting the data as a tlv +// stream containing the given records. +func decodeBytes(tlvBytes []byte, tlvRecords ...tlv.Record) error { + tlvStream, err := tlv.NewStream(tlvRecords...) + if err != nil { + return err + } + + return tlvStream.Decode(bytes.NewReader(tlvBytes)) +} diff --git a/sidecar/tlv_test.go b/sidecar/tlv_test.go new file mode 100644 index 000000000..23e6f93c6 --- /dev/null +++ b/sidecar/tlv_test.go @@ -0,0 +1,104 @@ +package sidecar + +import ( + "bytes" + "encoding/hex" + "math/big" + "testing" + + "github.com/btcsuite/btcd/btcec" + "github.com/stretchr/testify/require" +) + +var ( + testPubKeyRaw, _ = hex.DecodeString( + "02d52e6e60001ed587b784bf3f18dc2905d83b2f36e496263f283b1a7589" + + "2879c7", + ) + testPubKeyRaw2, _ = hex.DecodeString( + "0319d616f6a373b24fe5d45fb2964bea4054196820f41ef1a093a9c8e234" + + "e57cf7", + ) + testPubKey, _ = btcec.ParsePubKey(testPubKeyRaw, btcec.S256()) + testPubKey2, _ = btcec.ParsePubKey(testPubKeyRaw2, btcec.S256()) +) + +func TestSerializeTicket(t *testing.T) { + var buf bytes.Buffer + + // Test serialization of nil struct members. + ticketMinimal := &Ticket{ + ID: [8]byte{7, 6, 5, 4, 3, 2, 1, 0}, + Version: Version(99), + State: StateRegistered, + } + err := SerializeTicket(&buf, ticketMinimal) + require.NoError(t, err) + + deserializedTicket, err := DeserializeTicket( + bytes.NewReader(buf.Bytes()), + ) + require.NoError(t, err) + require.Equal(t, ticketMinimal, deserializedTicket) + + buf.Reset() + + // Test serialization of nil sub struct members. + ticketEmptySubStructs := &Ticket{ + ID: [8]byte{7, 6, 5, 4, 3, 2, 1, 0}, + Version: Version(99), + State: StateRegistered, + Offer: Offer{}, + Recipient: &Recipient{}, + Order: &Order{}, + Execution: &Execution{}, + } + err = SerializeTicket(&buf, ticketEmptySubStructs) + require.NoError(t, err) + + deserializedTicket, err = DeserializeTicket( + bytes.NewReader(buf.Bytes()), + ) + require.NoError(t, err) + require.Equal(t, ticketEmptySubStructs, deserializedTicket) + + buf.Reset() + + // Test with all struct members set. + ticketMaximal := &Ticket{ + ID: [8]byte{7, 6, 5, 4, 3, 2, 1, 0}, + Version: Version(99), + State: StateRegistered, + Offer: Offer{ + Capacity: 777, + PushAmt: 888, + SignPubKey: testPubKey, + SigOfferDigest: &btcec.Signature{ + R: new(big.Int).SetInt64(22), + S: new(big.Int).SetInt64(55), + }, + }, + Recipient: &Recipient{ + NodePubKey: testPubKey, + MultiSigPubKey: testPubKey2, + }, + Order: &Order{ + BidNonce: [32]byte{11, 22, 33, 44}, + SigOrderDigest: &btcec.Signature{ + R: new(big.Int).SetInt64(99), + S: new(big.Int).SetInt64(33), + }, + }, + Execution: &Execution{ + PendingChannelID: [32]byte{99, 88, 77}, + }, + } + err = SerializeTicket(&buf, ticketMaximal) + require.NoError(t, err) + + deserializedTicket, err = DeserializeTicket( + bytes.NewReader(buf.Bytes()), + ) + require.NoError(t, err) + require.Equal(t, ticketMaximal, deserializedTicket) +}