lnwire: introduce PureTLVMessage
PureTLVMessage describes an LN message that is a pure TLV stream. If the message includes a signature, it will sign all the TLV records in the inclusive ranges: 0 to 159 and 1000000000 to 2999999999. A comprehensive test is added that shows how two versions of the same message remain forward compatible.
This commit is contained in:
2 files changed
+494
No files matched your search
@@ -0,0 +1,105 @@
|
||||
package lnwire
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
||||
"github.com/lightningnetwork/lnd/tlv"
|
||||
)
|
||||
|
||||
const (
|
||||
// pureTLVUnsignedRangeOneStart defines the start of the first unsigned
|
||||
// TLV range used for pure TLV messages. The range is inclusive of this
|
||||
// number.
|
||||
pureTLVUnsignedRangeOneStart = 160
|
||||
|
||||
// pureTLVSignedSecondRangeStart defines the start of the second signed
|
||||
// TLV range used for pure TLV messages. The range is inclusive of this
|
||||
// number. Note that the first range is the inclusive range of 0-159.
|
||||
pureTLVSignedSecondRangeStart = 1000000000
|
||||
|
||||
// pureTLVUnsignedRangeTwoStart defines the start of the second unsigned
|
||||
// TLV range used for pure TLV message.
|
||||
pureTLVUnsignedRangeTwoStart = 3000000000
|
||||
)
|
||||
|
||||
// PureTLVMessage describes an LN message that is a pure TLV stream. If the
|
||||
// message includes a signature, it will sign all the TLV records in the
|
||||
// inclusive ranges: 0 to 159 and 1000000000 to 2999999999.
|
||||
type PureTLVMessage interface {
|
||||
// AllRecords returns all the TLV records for the message. This will
|
||||
// include all the records we know about along with any that we don't
|
||||
// know about but that fall in the signed TLV range.
|
||||
AllRecords() []tlv.Record
|
||||
}
|
||||
|
||||
// EncodePureTLVMessage encodes the given PureTLVMessage to the given buffer.
|
||||
func EncodePureTLVMessage(msg PureTLVMessage, buf *bytes.Buffer) error {
|
||||
return EncodeRecordsTo(buf, msg.AllRecords())
|
||||
}
|
||||
|
||||
// SerialiseFieldsToSign serialises all the records from the given
|
||||
// PureTLVMessage that fall within the signed TLV range.
|
||||
func SerialiseFieldsToSign(msg PureTLVMessage) ([]byte, error) {
|
||||
// Filter out all the fields not in the signed ranges.
|
||||
var signedRecords []tlv.Record
|
||||
for _, record := range msg.AllRecords() {
|
||||
if InUnsignedRange(record.Type()) {
|
||||
continue
|
||||
}
|
||||
|
||||
signedRecords = append(signedRecords, record)
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := EncodeRecordsTo(&buf, signedRecords); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
// InUnsignedRange returns true if the given TLV type falls outside the TLV
|
||||
// ranges that the signature of a pure TLV message will cover.
|
||||
func InUnsignedRange(t tlv.Type) bool {
|
||||
return (t >= pureTLVUnsignedRangeOneStart &&
|
||||
t < pureTLVSignedSecondRangeStart) ||
|
||||
t >= pureTLVUnsignedRangeTwoStart
|
||||
}
|
||||
|
||||
// ExtraSignedFields is a type that stores a map from TLV types in the signed
|
||||
// range (for PureMessages) to their corresponding serialised values. This type
|
||||
// can be used to keep around data that we don't yet understand but that we need
|
||||
// for re-composing the wire message since the signature covers these fields.
|
||||
type ExtraSignedFields map[uint64][]byte
|
||||
|
||||
// ExtraSignedFieldsFromTypeMap is a helper that can be used alongside calls to
|
||||
// the tlv.Stream DecodeWithParsedTypesP2P or DecodeWithParsedTypes methods to
|
||||
// extract the tlv type and value pairs in the defined PureTLVMessage signed
|
||||
// range which we have not handled with any of our defined Records. These
|
||||
// methods will return a tlv.TypeMap containing the records that were extracted
|
||||
// from an io.Reader. If the record was know and handled by a defined record,
|
||||
// then the value accompanying the record's type in the map will be nil.
|
||||
// Otherwise, if the record was unhandled, it will be non-nil.
|
||||
func ExtraSignedFieldsFromTypeMap(m tlv.TypeMap) ExtraSignedFields {
|
||||
extraFields := make(ExtraSignedFields)
|
||||
for t, v := range m {
|
||||
// If the value in the type map is nil, then it indicates that
|
||||
// we know this type, and it was handled by one of the records
|
||||
// we passed to the decode function vai the TLV stream.
|
||||
if v == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// No need to keep this field if it is unknown to us and is not
|
||||
// in the sign range.
|
||||
if InUnsignedRange(t) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Otherwise, this is an un-handled type, so we keep track of
|
||||
// it for signature validation and re-encoding later on.
|
||||
extraFields[uint64(t)] = v
|
||||
}
|
||||
|
||||
return extraFields
|
||||
}
|
||||
@@ -0,0 +1,389 @@
|
||||
package lnwire
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/btcsuite/btcd/btcec/v2"
|
||||
"github.com/lightningnetwork/lnd/tlv"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestPureTLVMessages tests the forwards compatibility of two versions of the
|
||||
// same Lightning Network message that uses the Pure TLV format. This in essence
|
||||
// tests that and older client is able to verify the signature over relevant
|
||||
// data in a newer client's message.
|
||||
func TestPureTLVMessage(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var (
|
||||
_, pkA = btcec.PrivKeyFromBytes([]byte{1})
|
||||
_, pkB = btcec.PrivKeyFromBytes([]byte{2})
|
||||
capacity = MilliSatoshi(100)
|
||||
)
|
||||
|
||||
// Test encode and decode of MsgV1 as is.
|
||||
t.Run("Encode and Decode of MsgV1", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
msgOld := newMsgV1(pkA, &capacity)
|
||||
|
||||
buf := bytes.NewBuffer(nil)
|
||||
require.NoError(t, msgOld.Encode(buf, 0))
|
||||
|
||||
var msgOld2 MsgV1
|
||||
require.NoError(t, msgOld2.Decode(buf, 0))
|
||||
|
||||
require.Equal(t, msgOld, &msgOld2)
|
||||
})
|
||||
|
||||
// Test encode and decode of MsgV2 as is.
|
||||
t.Run("Encode and Decode of MsgV2", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
msgNew := newMsgV2(
|
||||
pkA, &capacity, pkB, []byte{1, 2, 3, 4}, 90, 100, true,
|
||||
)
|
||||
|
||||
buf := bytes.NewBuffer(nil)
|
||||
require.NoError(t, msgNew.Encode(buf, 0))
|
||||
|
||||
var msgNew2 MsgV2
|
||||
require.NoError(t, msgNew2.Decode(buf, 0))
|
||||
|
||||
require.Equal(t, msgNew, &msgNew2)
|
||||
})
|
||||
|
||||
// Create a MsgV2 and decode it into a MsgV1. Both the new client
|
||||
// (MsgV2) and old client (MsgV1) should be able to generate the same
|
||||
// digest that will be used to create and validate the signture.
|
||||
t.Run("Encode MsgV2 and decode via MsgV1", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var (
|
||||
buf = bytes.NewBuffer(nil)
|
||||
msgV2 = newMsgV2(
|
||||
pkA, &capacity, pkB, []byte{1, 2, 3, 4}, 100,
|
||||
90, true,
|
||||
)
|
||||
)
|
||||
require.NoError(t, msgV2.Encode(buf, 0))
|
||||
|
||||
// Get the serialised bytes that would be signed for msgV2.
|
||||
signData1, err := SerialiseFieldsToSign(msgV2)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Decoding via the old message should store some of the extra
|
||||
// fields.
|
||||
var msgV1 MsgV1
|
||||
require.NoError(t, msgV1.Decode(buf, 0))
|
||||
require.NotEmpty(t, msgV1.ExtraSignedFields)
|
||||
|
||||
// Show that the extra fields map contains unknown fields in the
|
||||
// signed range but not unknown fields in the unsigned range.
|
||||
_, ok := msgV1.ExtraSignedFields[uint64(msgV2.Num.TlvType())] //nolint:ll
|
||||
require.True(t, ok)
|
||||
_, ok = msgV1.ExtraSignedFields[uint64(msgV2.Other.TlvType())] //nolint:ll
|
||||
require.False(t, ok)
|
||||
|
||||
// The serialised bytes to verify the signature against should
|
||||
// be the same though.
|
||||
signData2, err := SerialiseFieldsToSign(&msgV1)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, signData1, signData2)
|
||||
|
||||
// Re-encoding via the old message should keep the extra fields.
|
||||
buf = bytes.NewBuffer(nil)
|
||||
require.NoError(t, msgV1.Encode(buf, 0))
|
||||
|
||||
var msgV1ReEncoded MsgV1
|
||||
require.NoError(t, msgV1ReEncoded.Decode(buf, 0))
|
||||
|
||||
require.Equal(t, &msgV1, &msgV1ReEncoded)
|
||||
})
|
||||
}
|
||||
|
||||
// MsgV1 represents a more minimal, first version of a Lightning Network
|
||||
// message.
|
||||
type MsgV1 struct {
|
||||
// Two known fields in the signed range.
|
||||
NodeKey tlv.RecordT[tlv.TlvType0, *btcec.PublicKey]
|
||||
Capacity tlv.OptionalRecordT[tlv.TlvType1, MilliSatoshi]
|
||||
|
||||
// Signature in the unsigned range.
|
||||
Signature tlv.RecordT[tlv.TlvType160, Sig]
|
||||
|
||||
// Any extra fields in the signed range that we do not yet know about,
|
||||
// but we need to keep them for signature validation and to produce a
|
||||
// valid message.
|
||||
ExtraSignedFields
|
||||
}
|
||||
|
||||
var _ Message = (*MsgV1)(nil)
|
||||
var _ PureTLVMessage = (*MsgV1)(nil)
|
||||
|
||||
// newMsgV1 is a constructor for MsgV1.
|
||||
func newMsgV1(nodeKey *btcec.PublicKey, capacity *MilliSatoshi) *MsgV1 {
|
||||
newMsg := &MsgV1{
|
||||
NodeKey: tlv.NewPrimitiveRecord[tlv.TlvType0](
|
||||
nodeKey,
|
||||
),
|
||||
Signature: tlv.NewRecordT[tlv.TlvType160](
|
||||
testSchnorrSig,
|
||||
),
|
||||
ExtraSignedFields: make(ExtraSignedFields),
|
||||
}
|
||||
|
||||
if capacity != nil {
|
||||
newMsg.Capacity = tlv.SomeRecordT(
|
||||
tlv.NewPrimitiveRecord[tlv.TlvType1](*capacity),
|
||||
)
|
||||
}
|
||||
|
||||
return newMsg
|
||||
}
|
||||
|
||||
// Decode deserializes a serialized MsgV1 in the passed io.Reader.
|
||||
//
|
||||
// This is part of the lnwire.Message interface.
|
||||
func (g *MsgV1) Decode(r io.Reader, _ uint32) error {
|
||||
var capacity = tlv.ZeroRecordT[tlv.TlvType1, MilliSatoshi]()
|
||||
stream, err := tlv.NewStream(
|
||||
ProduceRecordsSorted(
|
||||
&g.NodeKey,
|
||||
&capacity,
|
||||
&g.Signature,
|
||||
)...,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
g.Signature.Val.ForceSchnorr()
|
||||
|
||||
typeMap, err := stream.DecodeWithParsedTypesP2P(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, ok := typeMap[g.Capacity.TlvType()]; ok {
|
||||
g.Capacity = tlv.SomeRecordT(capacity)
|
||||
}
|
||||
|
||||
g.ExtraSignedFields = ExtraSignedFieldsFromTypeMap(typeMap)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Encode serializes the target MsgV1 into the passed buffer.
|
||||
//
|
||||
// This is part of the lnwire.Message interface.
|
||||
func (g *MsgV1) Encode(buf *bytes.Buffer, _ uint32) error {
|
||||
return EncodePureTLVMessage(g, buf)
|
||||
}
|
||||
|
||||
// MsgType returns the integer uniquely identifying this message type on the
|
||||
// wire.
|
||||
//
|
||||
// This is part of the lnwire.Message interface.
|
||||
func (g *MsgV1) MsgType() MessageType {
|
||||
return 7777
|
||||
}
|
||||
|
||||
// AllRecords returns all the TLV records for the message. This will
|
||||
// include all the records we know about along with any that we don't
|
||||
// know about but that fall in the signed TLV range.
|
||||
//
|
||||
// This is part of the PureTLVMessage interface.
|
||||
func (g *MsgV1) AllRecords() []tlv.Record {
|
||||
recordProducers := []tlv.RecordProducer{
|
||||
&g.NodeKey,
|
||||
&g.Signature,
|
||||
}
|
||||
recordProducers = append(
|
||||
recordProducers,
|
||||
RecordsAsProducers(
|
||||
tlv.MapToRecords(g.ExtraSignedFields),
|
||||
)...,
|
||||
)
|
||||
|
||||
g.Capacity.WhenSome(
|
||||
func(capacity tlv.RecordT[tlv.TlvType1, MilliSatoshi]) {
|
||||
recordProducers = append(recordProducers, &capacity)
|
||||
},
|
||||
)
|
||||
|
||||
return ProduceRecordsSorted(recordProducers...)
|
||||
}
|
||||
|
||||
// MsgV2 represents a newer version of MsgV1 which contains more fields both in
|
||||
// the unsigned and signed TLV ranges.
|
||||
type MsgV2 struct {
|
||||
NodeKey tlv.RecordT[tlv.TlvType0, *btcec.PublicKey]
|
||||
Capacity tlv.OptionalRecordT[tlv.TlvType1, MilliSatoshi]
|
||||
|
||||
// An additional fields (optional) in the signed range.
|
||||
BitcoinKey tlv.OptionalRecordT[tlv.TlvType3, *btcec.PublicKey]
|
||||
|
||||
// A zero length TLV in the signed range.
|
||||
SecondPeer tlv.OptionalRecordT[tlv.TlvType5, TrueBoolean]
|
||||
|
||||
// Signature in the unsigned range.
|
||||
Signature tlv.RecordT[tlv.TlvType160, Sig]
|
||||
|
||||
// Another field in the unsigned range. An older node can throw this
|
||||
// away.
|
||||
SPVProof tlv.RecordT[tlv.TlvType161, []byte]
|
||||
|
||||
// A new field in the second signed range. An older node should keep
|
||||
// this since it is part of the serialised message that is signed.
|
||||
Num tlv.RecordT[tlv.TlvType1000000000, uint8]
|
||||
|
||||
// Another field in the second unsigned-range. Older nodes may throw
|
||||
// this away and it won't affect the digest used for signature creation
|
||||
// and validation.
|
||||
Other tlv.RecordT[tlv.TlvType3000000000, uint8]
|
||||
|
||||
// Any extra fields in the signed range that we do not yet know about,
|
||||
// but we need to keep them for signature validation and to produce a
|
||||
// valid message.
|
||||
ExtraSignedFields
|
||||
}
|
||||
|
||||
// newMsgV2 is a constructor for MsgV2.
|
||||
func newMsgV2(nodeKey *btcec.PublicKey, capacity *MilliSatoshi,
|
||||
btcKey *btcec.PublicKey, spvProof []byte, num, other uint8,
|
||||
secondPeer bool) *MsgV2 {
|
||||
|
||||
newMsg := &MsgV2{
|
||||
NodeKey: tlv.NewPrimitiveRecord[tlv.TlvType0](nodeKey),
|
||||
SPVProof: tlv.NewPrimitiveRecord[tlv.TlvType161](spvProof),
|
||||
Num: tlv.NewPrimitiveRecord[tlv.TlvType1000000000](num),
|
||||
Other: tlv.NewPrimitiveRecord[tlv.TlvType3000000000](num),
|
||||
Signature: tlv.NewRecordT[tlv.TlvType160](
|
||||
testSchnorrSig,
|
||||
),
|
||||
ExtraSignedFields: make(ExtraSignedFields),
|
||||
}
|
||||
|
||||
if secondPeer {
|
||||
newMsg.SecondPeer = tlv.SomeRecordT(
|
||||
tlv.NewRecordT[tlv.TlvType5](TrueBoolean{}),
|
||||
)
|
||||
}
|
||||
|
||||
if capacity != nil {
|
||||
newMsg.Capacity = tlv.SomeRecordT(
|
||||
tlv.NewPrimitiveRecord[tlv.TlvType1](*capacity),
|
||||
)
|
||||
}
|
||||
|
||||
if btcKey != nil {
|
||||
newMsg.BitcoinKey = tlv.SomeRecordT(
|
||||
tlv.NewPrimitiveRecord[tlv.TlvType3](btcKey),
|
||||
)
|
||||
}
|
||||
|
||||
return newMsg
|
||||
}
|
||||
|
||||
// Decode deserializes a serialized MsgV2 in the passed io.Reader.
|
||||
//
|
||||
// This is part of the lnwire.Message interface.
|
||||
func (g *MsgV2) Decode(r io.Reader, _ uint32) error {
|
||||
var (
|
||||
capacity = tlv.ZeroRecordT[tlv.TlvType1, MilliSatoshi]()
|
||||
btcKey = tlv.ZeroRecordT[tlv.TlvType3, *btcec.PublicKey]()
|
||||
secondPeer = tlv.ZeroRecordT[tlv.TlvType5, TrueBoolean]()
|
||||
)
|
||||
|
||||
stream, err := tlv.NewStream(
|
||||
ProduceRecordsSorted(
|
||||
&g.NodeKey,
|
||||
&capacity,
|
||||
&btcKey,
|
||||
&secondPeer,
|
||||
&g.Signature,
|
||||
&g.SPVProof,
|
||||
&g.Num,
|
||||
&g.Other,
|
||||
)...,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
g.Signature.Val.ForceSchnorr()
|
||||
|
||||
typeMap, err := stream.DecodeWithParsedTypesP2P(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, ok := typeMap[g.Capacity.TlvType()]; ok {
|
||||
g.Capacity = tlv.SomeRecordT(capacity)
|
||||
}
|
||||
|
||||
if _, ok := typeMap[g.SecondPeer.TlvType()]; ok {
|
||||
g.SecondPeer = tlv.SomeRecordT(secondPeer)
|
||||
}
|
||||
|
||||
if _, ok := typeMap[g.BitcoinKey.TlvType()]; ok {
|
||||
g.BitcoinKey = tlv.SomeRecordT(btcKey)
|
||||
}
|
||||
|
||||
g.ExtraSignedFields = ExtraSignedFieldsFromTypeMap(typeMap)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Encode serializes the target MsgV2 into the passed buffer.
|
||||
//
|
||||
// This is part of the lnwire.Message interface.
|
||||
func (g *MsgV2) Encode(buf *bytes.Buffer, _ uint32) error {
|
||||
return EncodePureTLVMessage(g, buf)
|
||||
}
|
||||
|
||||
// MsgType returns the integer uniquely identifying this message type on the
|
||||
// wire.
|
||||
//
|
||||
// This is part of the lnwire.Message interface.
|
||||
func (g *MsgV2) MsgType() MessageType {
|
||||
return 7779
|
||||
}
|
||||
|
||||
// AllRecords returns all the TLV records for the message. This will
|
||||
// include all the records we know about along with any that we don't
|
||||
// know about but that fall in the signed TLV range.
|
||||
//
|
||||
// This is part of the PureTLVMessage interface.
|
||||
func (g *MsgV2) AllRecords() []tlv.Record {
|
||||
recordProducers := []tlv.RecordProducer{
|
||||
&g.NodeKey,
|
||||
&g.Signature,
|
||||
&g.SPVProof,
|
||||
&g.Num,
|
||||
&g.Other,
|
||||
}
|
||||
recordProducers = append(recordProducers, RecordsAsProducers(
|
||||
tlv.MapToRecords(g.ExtraSignedFields),
|
||||
)...)
|
||||
|
||||
g.Capacity.WhenSome(
|
||||
func(c tlv.RecordT[tlv.TlvType1, MilliSatoshi]) {
|
||||
recordProducers = append(recordProducers, &c)
|
||||
},
|
||||
)
|
||||
g.BitcoinKey.WhenSome(
|
||||
func(key tlv.RecordT[tlv.TlvType3, *btcec.PublicKey]) {
|
||||
recordProducers = append(recordProducers, &key)
|
||||
},
|
||||
)
|
||||
g.SecondPeer.WhenSome(
|
||||
func(second tlv.RecordT[tlv.TlvType5, TrueBoolean]) {
|
||||
recordProducers = append(recordProducers, &second)
|
||||
},
|
||||
)
|
||||
|
||||
return ProduceRecordsSorted(recordProducers...)
|
||||
}
|
||||
Reference in new issue
Block a user