What changed, and why it matters
This commit adds a new message type, NodeAnnouncement2, for a future Lightning protocol feature called Gossip 2 (or 1.75). It is purely a code addition that defines how the message is formatted, encoded, and decoded. There is no indication this fixes a security bug or introduces a vulnerability; it is normal feature development.
No security action required. Treat as routine feature code; review in the context of the broader Gossip 2 implementation when it is integrated.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The commit introduces the lnwire.NodeAnnouncement2 type and its TLV encoding/decoding logic for the Gossip 2 protocol. It adds message type 269, implements the Message/PureTLVMessage/SizeableMessage interfaces, and defines custom TLV records for Color, NodeAlias2, IPV4Addrs, IPV6Addrs, TorV3Addrs, and DNSHostName. It also adds fuzz and unit tests. No security-relevant behavior changes or bug fixes are present in the diff.
Changed components
lnwire packageNodeAnnouncement2 message typeGossip 2 protocol supportInspect captured patch +873 / −0
diff --git a/lnwire/fuzz_test.go b/lnwire/fuzz_test.go
index c3cd819..a143ce6 100644
--- a/lnwire/fuzz_test.go
+++ b/lnwire/fuzz_test.go
@@ -244,6 +244,30 @@ func FuzzNodeAnnouncement(f *testing.F) {
})
}
+func FuzzNodeAnnouncement2(f *testing.F) {
+ f.Fuzz(func(t *testing.T, data []byte) {
+ // We can't use require.Equal for Features, since we consider
+ // the empty map and nil to be equivalent.
+ assertEq := func(t *testing.T, x, y any) {
+ require.IsType(t, &NodeAnnouncement2{}, x)
+ first, _ := x.(*NodeAnnouncement2)
+ require.IsType(t, &NodeAnnouncement2{}, y)
+ second, _ := y.(*NodeAnnouncement2)
+
+ require.True(
+ t,
+ first.Features.Val.Equals(&second.Features.Val),
+ )
+ first.Features.Val = *NewRawFeatureVector()
+ second.Features.Val = *NewRawFeatureVector()
+
+ require.Equal(t, first, second)
+ }
+
+ wireMsgHarnessCustom(t, data, MsgNodeAnnouncement2, assertEq)
+ })
+}
+
func FuzzOpenChannel(f *testing.F) {
f.Fuzz(func(t *testing.T, data []byte) {
// We can't use require.Equal for UpfrontShutdownScript, since
diff --git a/lnwire/message.go b/lnwire/message.go
index 9584bfc..c64b09b 100644
--- a/lnwire/message.go
+++ b/lnwire/message.go
@@ -64,6 +64,7 @@ const (
MsgReplyChannelRange = 264
MsgGossipTimestampRange = 265
MsgChannelAnnouncement2 = 267
+ MsgNodeAnnouncement2 = 269
MsgChannelUpdate2 = 271
MsgKickoffSig = 777
@@ -193,6 +194,8 @@ func (t MessageType) String() string {
return "MsgAnnounceSignatures2"
case MsgChannelAnnouncement2:
return "ChannelAnnouncement2"
+ case MsgNodeAnnouncement2:
+ return "NodeAnnouncement2"
case MsgChannelUpdate2:
return "ChannelUpdate2"
default:
@@ -355,6 +358,8 @@ func makeEmptyMessage(msgType MessageType) (Message, error) {
msg = &AnnounceSignatures2{}
case MsgChannelAnnouncement2:
msg = &ChannelAnnouncement2{}
+ case MsgNodeAnnouncement2:
+ msg = &NodeAnnouncement2{}
case MsgChannelUpdate2:
msg = &ChannelUpdate2{}
default:
diff --git a/lnwire/node_announcement_2.go b/lnwire/node_announcement_2.go
new file mode 100644
index 0000000..23fcc50
--- /dev/null
+++ b/lnwire/node_announcement_2.go
@@ -0,0 +1,581 @@
+package lnwire
+
+import (
+ "bytes"
+ "encoding/binary"
+ "errors"
+ "fmt"
+ "image/color"
+ "io"
+ "net"
+ "unicode/utf8"
+
+ "github.com/lightningnetwork/lnd/tlv"
+ "github.com/lightningnetwork/lnd/tor"
+)
+
+// NodeAnnouncement2 message is used to announce the presence of a Lightning
+// node and also to signal that the node is accepting incoming connections.
+// Each NodeAnnouncement2 authenticating the advertised information within the
+// announcement via a signature using the advertised node pubkey.
+type NodeAnnouncement2 struct {
+ // Features is the feature vector that encodes the features supported
+ // by the target node.
+ Features tlv.RecordT[tlv.TlvType0, RawFeatureVector]
+
+ // Color is an optional field used to customize a node's appearance in
+ // maps and graphs.
+ Color tlv.OptionalRecordT[tlv.TlvType1, Color]
+
+ // BlockHeight allows ordering in the case of multiple announcements. We
+ // should ignore the message if block height is not greater than the
+ // last-received. The block height must always be greater or equal to
+ // the block height that the channel funding transaction was confirmed
+ // in.
+ BlockHeight tlv.RecordT[tlv.TlvType2, uint32]
+
+ // Alias is used to customize their node's appearance in maps and
+ // graphs.
+ Alias tlv.OptionalRecordT[tlv.TlvType3, NodeAlias2]
+
+ // NodeID is the public key of the node creating the announcement.
+ NodeID tlv.RecordT[tlv.TlvType4, [33]byte]
+
+ // IPV4Addrs is an optional list of ipv4 addresses that the node is
+ // reachable at.
+ IPV4Addrs tlv.OptionalRecordT[tlv.TlvType5, IPV4Addrs]
+
+ // IPV6Addrs is an optional list of ipv6 addresses that the node is
+ // reachable at.
+ IPV6Addrs tlv.OptionalRecordT[tlv.TlvType7, IPV6Addrs]
+
+ // TorV3Addrs is an optional list of tor v3 addresses that the node is
+ // reachable at.
+ TorV3Addrs tlv.OptionalRecordT[tlv.TlvType9, TorV3Addrs]
+
+ // DNSHostName is an optional DNS hostname that the node is reachable
+ // at.
+ DNSHostName tlv.OptionalRecordT[tlv.TlvType11, DNSAddress]
+
+ // Signature is used to validate the announced data and prove the
+ // ownership of node id.
+ 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
+}
+
+// SerializedSize returns the serialized size of the message in bytes.
+//
+// This is part of the lnwire.SizeableMessage interface.
+func (n *NodeAnnouncement2) SerializedSize() (uint32, error) {
+ return MessageSerializedSize(n)
+}
+
+// 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.
+//
+// NOTE: this is part of the PureTLVMessage interface.
+func (n *NodeAnnouncement2) AllRecords() []tlv.Record {
+ recordProducers := []tlv.RecordProducer{
+ &n.Features,
+ &n.BlockHeight,
+ &n.NodeID,
+ &n.Signature,
+ }
+
+ n.Color.WhenSome(func(r tlv.RecordT[tlv.TlvType1, Color]) {
+ recordProducers = append(recordProducers, &r)
+ })
+
+ n.Alias.WhenSome(func(a tlv.RecordT[tlv.TlvType3, NodeAlias2]) {
+ recordProducers = append(recordProducers, &a)
+ })
+
+ n.IPV4Addrs.WhenSome(func(r tlv.RecordT[tlv.TlvType5, IPV4Addrs]) {
+ recordProducers = append(recordProducers, &r)
+ })
+
+ n.IPV6Addrs.WhenSome(func(r tlv.RecordT[tlv.TlvType7, IPV6Addrs]) {
+ recordProducers = append(recordProducers, &r)
+ })
+
+ n.TorV3Addrs.WhenSome(func(r tlv.RecordT[tlv.TlvType9, TorV3Addrs]) {
+ recordProducers = append(recordProducers, &r)
+ })
+
+ n.DNSHostName.WhenSome(func(r tlv.RecordT[tlv.TlvType11, DNSAddress]) {
+ recordProducers = append(recordProducers, &r)
+ })
+
+ recordProducers = append(recordProducers, RecordsAsProducers(
+ tlv.MapToRecords(n.ExtraSignedFields),
+ )...)
+
+ return ProduceRecordsSorted(recordProducers...)
+}
+
+// Decode deserializes a serialized NodeAnnouncement2 stored in the passed
+// io.Reader observing the specified protocol version.
+//
+// This is part of the lnwire.Message interface.
+func (n *NodeAnnouncement2) Decode(r io.Reader, _ uint32) error {
+ var (
+ color = tlv.ZeroRecordT[tlv.TlvType1, Color]()
+ alias = tlv.ZeroRecordT[tlv.TlvType3, NodeAlias2]()
+ ipv4 = tlv.ZeroRecordT[tlv.TlvType5, IPV4Addrs]()
+ ipv6 = tlv.ZeroRecordT[tlv.TlvType7, IPV6Addrs]()
+ torV3 = tlv.ZeroRecordT[tlv.TlvType9, TorV3Addrs]()
+ dns = tlv.ZeroRecordT[tlv.TlvType11, DNSAddress]()
+ )
+
+ stream, err := tlv.NewStream(ProduceRecordsSorted(
+ &n.Features,
+ &n.BlockHeight,
+ &n.NodeID,
+ &n.Signature,
+ &alias,
+ &color,
+ &ipv4,
+ &ipv6,
+ &torV3,
+ &dns,
+ )...)
+ if err != nil {
+ return err
+ }
+ n.Signature.Val.ForceSchnorr()
+
+ typeMap, err := stream.DecodeWithParsedTypesP2P(r)
+ if err != nil {
+ return err
+ }
+
+ if _, ok := typeMap[n.Alias.TlvType()]; ok {
+ n.Alias = tlv.SomeRecordT(alias)
+ }
+
+ if _, ok := typeMap[n.Color.TlvType()]; ok {
+ n.Color = tlv.SomeRecordT(color)
+ }
+
+ if _, ok := typeMap[n.IPV4Addrs.TlvType()]; ok {
+ n.IPV4Addrs = tlv.SomeRecordT(ipv4)
+ }
+
+ if _, ok := typeMap[n.IPV6Addrs.TlvType()]; ok {
+ n.IPV6Addrs = tlv.SomeRecordT(ipv6)
+ }
+
+ if _, ok := typeMap[n.TorV3Addrs.TlvType()]; ok {
+ n.TorV3Addrs = tlv.SomeRecordT(torV3)
+ }
+
+ if _, ok := typeMap[n.DNSHostName.TlvType()]; ok {
+ n.DNSHostName = tlv.SomeRecordT(dns)
+ }
+
+ n.ExtraSignedFields = ExtraSignedFieldsFromTypeMap(typeMap)
+
+ return nil
+}
+
+// Encode serializes the target NodeAnnouncement2 into the passed io.Writer
+// observing the protocol version specified.
+//
+// This is part of the lnwire.Message interface.
+func (n *NodeAnnouncement2) Encode(w *bytes.Buffer, _ uint32) error {
+ return EncodePureTLVMessage(n, w)
+}
+
+// MsgType returns the integer uniquely identifying this message type on the
+// wire.
+//
+// This is part of the lnwire.Message interface.
+func (n *NodeAnnouncement2) MsgType() MessageType {
+ return MsgNodeAnnouncement2
+}
+
+// A compile-time check to ensure NodeAnnouncement2 implements the Message
+// interface.
+var _ Message = (*NodeAnnouncement2)(nil)
+
+// A compile-time check to ensure NodeAnnouncement2 implements the
+// PureTLVMessage interface.
+var _ PureTLVMessage = (*NodeAnnouncement2)(nil)
+
+// A compile time check to ensure ChannelAnnouncement2 implements the
+// lnwire.SizeableMessage interface.
+var _ SizeableMessage = (*NodeAnnouncement2)(nil)
+
+// NodeAlias2 represents a UTF-8 encoded node alias with a maximum length of 32
+// bytes.
+type NodeAlias2 []byte
+
+// Record returns a TLV record for encoding/decoding NodeAlias2.
+func (n *NodeAlias2) Record() tlv.Record {
+ sizeFunc := func() uint64 {
+ return uint64(len(*n))
+ }
+
+ return tlv.MakeDynamicRecord(
+ 0, n, sizeFunc, nodeAlias2Encoder, nodeAlias2Decoder,
+ )
+}
+
+// ValidateNodeAlias2 validates that the node alias is valid UTF-8 and within
+// the 32-byte limit.
+func ValidateNodeAlias2(alias []byte) error {
+ if len(alias) == 0 {
+ return errors.New("node alias cannot be empty")
+ }
+
+ if len(alias) > 32 {
+ return fmt.Errorf("node alias exceeds 32 bytes: length %d",
+ len(alias))
+ }
+
+ if !utf8.Valid(alias) {
+ return errors.New("node alias contains invalid UTF-8")
+ }
+
+ return nil
+}
+
+// nodeAlias2Encoder encodes a NodeAlias2 as raw UTF-8 bytes.
+func nodeAlias2Encoder(w io.Writer, val any, _ *[8]byte) error {
+ if v, ok := val.(*NodeAlias2); ok {
+ if err := ValidateNodeAlias2(*v); err != nil {
+ return err
+ }
+
+ _, err := w.Write(*v)
+
+ return err
+ }
+
+ return tlv.NewTypeForEncodingErr(val, "NodeAlias2")
+}
+
+// nodeAlias2Decoder decodes raw bytes into a NodeAlias2.
+func nodeAlias2Decoder(r io.Reader, val any, _ *[8]byte, l uint64) error {
+ if v, ok := val.(*NodeAlias2); ok {
+ if l > 32 {
+ return fmt.Errorf("node alias exceeds 32 bytes: %d", l)
+ }
+
+ aliasBytes := make([]byte, l)
+ if _, err := io.ReadFull(r, aliasBytes); err != nil {
+ return err
+ }
+
+ if err := ValidateNodeAlias2(aliasBytes); err != nil {
+ return err
+ }
+
+ *v = aliasBytes
+
+ return nil
+ }
+
+ return tlv.NewTypeForDecodingErr(val, "NodeAlias2", l, 0)
+}
+
+// Color is a custom type to represent a color.RGBA that can be encoded/decoded
+// as a TLV record.
+type Color color.RGBA
+
+// Record returns a TLV record for encoding/decoding Color.
+func (c *Color) Record() tlv.Record {
+ return tlv.MakeStaticRecord(0, c, 3, rgbEncoder, rgbDecoder)
+}
+
+// rgbEncoder encodes a Color as RGB bytes.
+func rgbEncoder(w io.Writer, val any, _ *[8]byte) error {
+ if v, ok := val.(*Color); ok {
+ buf := bytes.NewBuffer(nil)
+ err := WriteColorRGBA(buf, color.RGBA(*v))
+ if err != nil {
+ return err
+ }
+ _, err = w.Write(buf.Bytes())
+
+ return err
+ }
+
+ return tlv.NewTypeForEncodingErr(val, "Color")
+}
+
+// rgbDecoder decodes RGB bytes into a Color.
+func rgbDecoder(r io.Reader, val any, _ *[8]byte, l uint64) error {
+ if v, ok := val.(*Color); ok {
+ return ReadElements(r, &v.R, &v.G, &v.B)
+ }
+
+ return tlv.NewTypeForDecodingErr(val, "Color", l, 3)
+}
+
+// ipv4AddrEncodedSize is the number of bytes required to encode a single ipv4
+// address. Four bytes are used to encode the IP address and two bytes for the
+// port number.
+const ipv4AddrEncodedSize = 4 + 2
+
+// IPV4Addrs is a list of ipv4 addresses that can be encoded as a TLV record.
+type IPV4Addrs []*net.TCPAddr
+
+// Record returns a Record that can be used to encode/decode a IPV4Addrs
+// to/from a TLV stream.
+func (a *IPV4Addrs) Record() tlv.Record {
+ return tlv.MakeDynamicRecord(
+ 0, a, a.encodedSize, ipv4AddrsEncoder, ipv4AddrsDecoder,
+ )
+}
+
+// encodedSize returns the number of bytes required to encode an IPV4Addrs
+// variable.
+func (a *IPV4Addrs) encodedSize() uint64 {
+ return uint64(len(*a) * ipv4AddrEncodedSize)
+}
+
+// ipv4AddrsEncoder encodes IPv4 addresses as TLV bytes.
+func ipv4AddrsEncoder(w io.Writer, val interface{}, _ *[8]byte) error {
+ if v, ok := val.(*IPV4Addrs); ok {
+ for _, ip := range *v {
+ _, err := w.Write(ip.IP.To4())
+ if err != nil {
+ return err
+ }
+ var port [2]byte
+ binary.BigEndian.PutUint16(port[:], uint16(ip.Port))
+ _, err = w.Write(port[:])
+ if err != nil {
+ return err
+ }
+ }
+
+ return nil
+ }
+
+ return tlv.NewTypeForEncodingErr(val, "lnwire.IPV4Addrs")
+}
+
+// ipv4AddrsDecoder decodes TLV bytes into IPv4 addresses.
+func ipv4AddrsDecoder(r io.Reader, val interface{}, _ *[8]byte,
+ l uint64) error {
+
+ if v, ok := val.(*IPV4Addrs); ok {
+ if l%(ipv4AddrEncodedSize) != 0 {
+ return fmt.Errorf("invalid ipv4 list encoding")
+ }
+ var (
+ numAddrs = int(l / ipv4AddrEncodedSize)
+ addrs = make([]*net.TCPAddr, 0, numAddrs)
+ ip [4]byte
+ port [2]byte
+ )
+ for len(addrs) < numAddrs {
+ _, err := r.Read(ip[:])
+ if err != nil {
+ return err
+ }
+ _, err = r.Read(port[:])
+ if err != nil {
+ return err
+ }
+ addrs = append(addrs, &net.TCPAddr{
+ IP: ip[:],
+ Port: int(binary.BigEndian.Uint16(port[:])),
+ })
+ }
+ *v = addrs
+
+ return nil
+ }
+
+ return tlv.NewTypeForEncodingErr(val, "lnwire.IPV4Addrs")
+}
+
+// IPV6Addrs is a list of ipv6 addresses that can be encoded as a TLV record.
+type IPV6Addrs []*net.TCPAddr
+
+// Record returns a Record that can be used to encode/decode a IPV4Addrs
+// to/from a TLV stream.
+func (a *IPV6Addrs) Record() tlv.Record {
+ return tlv.MakeDynamicRecord(
+ 0, a, a.encodedSize, ipv6AddrsEncoder, ipv6AddrsDecoder,
+ )
+}
+
+// ipv6AddrEncodedSize is the number of bytes required to encode a single ipv6
+// address. Sixteen bytes are used to encode the IP address and two bytes for
+// the port number.
+const ipv6AddrEncodedSize = 16 + 2
+
+// encodedSize returns the number of bytes required to encode an IPV6Addrs
+// variable.
+func (a *IPV6Addrs) encodedSize() uint64 {
+ return uint64(len(*a) * ipv6AddrEncodedSize)
+}
+
+// ipv6AddrsEncoder encodes IPv6 addresses as TLV bytes.
+func ipv6AddrsEncoder(w io.Writer, val interface{}, _ *[8]byte) error {
+ if v, ok := val.(*IPV6Addrs); ok {
+ for _, ip := range *v {
+ _, err := w.Write(ip.IP.To16())
+ if err != nil {
+ return err
+ }
+ var port [2]byte
+ binary.BigEndian.PutUint16(port[:], uint16(ip.Port))
+ _, err = w.Write(port[:])
+ if err != nil {
+ return err
+ }
+ }
+
+ return nil
+ }
+
+ return tlv.NewTypeForEncodingErr(val, "lnwire.IPV6Addrs")
+}
+
+// ipv6AddrsDecoder decodes TLV bytes into IPv6 addresses.
+func ipv6AddrsDecoder(r io.Reader, val interface{}, _ *[8]byte,
+ l uint64) error {
+
+ if v, ok := val.(*IPV6Addrs); ok {
+ if l%(ipv6AddrEncodedSize) != 0 {
+ return fmt.Errorf("invalid ipv6 list encoding")
+ }
+ var (
+ numAddrs = int(l / ipv6AddrEncodedSize)
+ addrs = make([]*net.TCPAddr, 0, numAddrs)
+ ip [16]byte
+ port [2]byte
+ )
+ for len(addrs) < numAddrs {
+ _, err := r.Read(ip[:])
+ if err != nil {
+ return err
+ }
+ _, err = r.Read(port[:])
+ if err != nil {
+ return err
+ }
+ addrs = append(addrs, &net.TCPAddr{
+ IP: ip[:],
+ Port: int(binary.BigEndian.Uint16(port[:])),
+ })
+ }
+ *v = addrs
+
+ return nil
+ }
+
+ return tlv.NewTypeForEncodingErr(val, "lnwire.IPV6Addrs")
+}
+
+// TorV3Addrs is a list of tor v3 addresses that can be encoded as a TLV record.
+type TorV3Addrs []*tor.OnionAddr
+
+// torV3AddrEncodedSize is the number of bytes required to encode a single tor
+// v3 address.
+const torV3AddrEncodedSize = tor.V3DecodedLen + 2
+
+// encodedSize returns the number of bytes required to encode an TorV3Addrs
+// variable.
+func (a *TorV3Addrs) encodedSize() uint64 {
+ return uint64(len(*a) * torV3AddrEncodedSize)
+}
+
+// Record returns a Record that can be used to encode/decode a IPV4Addrs
+// to/from a TLV stream.
+func (a *TorV3Addrs) Record() tlv.Record {
+ return tlv.MakeDynamicRecord(
+ 0, a, a.encodedSize, torV3AddrsEncoder, torV3AddrsDecoder,
+ )
+}
+
+// torV3AddrsEncoder encodes Tor v3 addresses as TLV bytes.
+func torV3AddrsEncoder(w io.Writer, val interface{}, _ *[8]byte) error {
+ if v, ok := val.(*TorV3Addrs); ok {
+ for _, addr := range *v {
+ encodedHostLen := tor.V3Len - tor.OnionSuffixLen
+ host, err := tor.Base32Encoding.DecodeString(
+ addr.OnionService[:encodedHostLen],
+ )
+ if err != nil {
+ return err
+ }
+
+ if len(host) != tor.V3DecodedLen {
+ return fmt.Errorf("expected a tor v3 host "+
+ "length of %d, got: %d",
+ tor.V3DecodedLen, len(host))
+ }
+
+ if _, err = w.Write(host); err != nil {
+ return err
+ }
+
+ var port [2]byte
+ binary.BigEndian.PutUint16(port[:], uint16(addr.Port))
+ if _, err = w.Write(port[:]); err != nil {
+ return err
+ }
+ }
+
+ return nil
+ }
+
+ return tlv.NewTypeForEncodingErr(val, "lnwire.TorV3Addrs")
+}
+
+// torV3AddrsDecoder decodes TLV bytes into Tor v3 addresses.
+func torV3AddrsDecoder(r io.Reader, val interface{}, _ *[8]byte,
+ l uint64) error {
+
+ if v, ok := val.(*TorV3Addrs); ok {
+ if l%torV3AddrEncodedSize != 0 {
+ return fmt.Errorf("invalid tor v3 list encoding")
+ }
+ var (
+ numAddrs = int(l / torV3AddrEncodedSize)
+ addrs = make([]*tor.OnionAddr, 0, numAddrs)
+ ip [tor.V3DecodedLen]byte
+ p [2]byte
+ )
+ for len(addrs) < numAddrs {
+ _, err := r.Read(ip[:])
+ if err != nil {
+ return err
+ }
+ _, err = r.Read(p[:])
+ if err != nil {
+ return err
+ }
+ onionService := tor.Base32Encoding.EncodeToString(ip[:])
+ onionService += tor.OnionSuffix
+
+ if len(onionService) != tor.V3Len {
+ return fmt.Errorf("expected a tor v3 onion "+
+ "service length of %d, got: %d",
+ tor.V3Len, len(onionService))
+ }
+
+ port := int(binary.BigEndian.Uint16(p[:]))
+ addrs = append(addrs, &tor.OnionAddr{
+ OnionService: onionService,
+ Port: port,
+ })
+ }
+ *v = addrs
+
+ return nil
+ }
+
+ return tlv.NewTypeForEncodingErr(val, "lnwire.TorV3Addrs")
+}
diff --git a/lnwire/node_announcement_2_test.go b/lnwire/node_announcement_2_test.go
new file mode 100644
index 0000000..0aea661
--- /dev/null
+++ b/lnwire/node_announcement_2_test.go
@@ -0,0 +1,119 @@
+package lnwire
+
+import (
+ "bytes"
+ "testing"
+
+ "github.com/lightningnetwork/lnd/tlv"
+ "github.com/stretchr/testify/require"
+)
+
+// TestNodeAnn2EncodeDecode tests the encoding and decoding of the
+// NodeAnnouncement2 message using hardcoded byte slices.
+func TestNodeAnn2EncodeDecode(t *testing.T) {
+ t.Parallel()
+
+ // We'll create a raw byte stream that represents a valid
+ // NodeAnnouncement2 message with various known and unknown fields in
+ // the signed TLV ranges along with the signature in the unsigned range.
+ rawBytes := []byte{
+ // Features record.
+ 0x00, // type.
+ 0x02, // length.
+ 0x1, 0x2, // value.
+
+ // Color record (optional).
+ 0x01, // type.
+ 0x03, // length.
+ 0xff, 0x00, 0x80, // value: RGB(255, 0, 128).
+
+ // BlockHeight record.
+ 0x02, // type.
+ 0x04, // length.
+ 0x00, 0x01, 0x86, 0xa0, // value: 100000.
+
+ // Alias record (optional).
+ 0x03, // type.
+ 0x20, // length (32).
+ 0x4c, 0x69, 0x67, 0x68, 0x74, 0x6e, 0x69, 0x6e,
+ 0x67, 0x20, 0x4e, 0x6f, 0x64, 0x65, 0x20, 0x54,
+ 0x65, 0x73, 0x74, 0x20, 0x41, 0x6c, 0x69, 0x61,
+ 0x73, 0x20, 0x4e, 0x61, 0x6d, 0x65, 0x21, 0x00, // value.
+
+ // NodeID record.
+ 0x04, // type.
+ 0x21, // length.
+ 0x02, 0x28, 0xf2, 0xaf, 0xa, 0xbe, 0x32, 0x24, 0x3, 0x48, 0xf,
+ 0xb3, 0xee, 0x17, 0x2f, 0x7f, 0x16, 0x1, 0xe6, 0x7d, 0x1d, 0xa6,
+ 0xca, 0xd4, 0xb, 0x54, 0xc4, 0x46, 0x8d, 0x48, 0x23, 0x6c, 0x39,
+
+ // IPV4Addrs record (optional).
+ 0x05, // type.
+ 0x0c, // length (12 bytes).
+ 0x0a, 0x00, 0x00, 0x01, 0x23, 0x28, // 10.0.0.1:9000
+ 0x0a, 0x00, 0x00, 0x01, 0x1f, 0x90, // 10.0.0.1:8080
+
+ // IPV6Addrs record (optional).
+ 0x07, // type.
+ 0x12, // length.
+ 0x20, 0x01, 0x0d, 0xb8, 0x00, 0x00, 0x00, 0x00, // IPv6 address
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, // 2001:db8::1
+ 0x1f, 0x40, // port 8000
+
+ // TorV3Addrs record (optional).
+ 0x09, // type.
+ 0x25, // length (37 bytes = 1 address).
+ // value: 35-byte tor v3 address + 2-byte port.
+ 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
+ 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10,
+ 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18,
+ 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20,
+ 0x21, 0x22, 0x23,
+ 0x23, 0x28, // port 9000.
+
+ // DNSHostName record (optional).
+ 0x0b, // type (11).
+ 0x17, // length (23).
+ 0x6c, 0x69, 0x67, 0x68, 0x74, 0x6e, 0x69, 0x6e,
+ 0x67, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, // value.
+ 0x65, 0x2e, 0x63, 0x6f, 0x6d,
+ 0x23, 0x28, // port 9000.
+
+ // Signature.
+ 0xa0, // type.
+ 0x40, // length.
+ 0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xa, 0xb,
+ 0xc, 0xd, 0xe, 0xf, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16,
+ 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20,
+ 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a,
+ 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, 0x30, 0x31, 0x32, 0x33, 0x34,
+ 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e,
+ 0x3f, // value.
+ }
+ secondSignedRangeType := new(bytes.Buffer)
+ var buf [8]byte
+ err := tlv.WriteVarInt(
+ secondSignedRangeType, pureTLVSignedSecondRangeStart+1, &buf,
+ )
+ require.NoError(t, err)
+ rawBytes = append(rawBytes, secondSignedRangeType.Bytes()...) // type.
+ rawBytes = append(rawBytes, []byte{
+ 0x02, // length.
+ 0x79, 0x79, // value.
+ }...)
+
+ // Now, create a new empty message and decode the raw bytes into it.
+ msg := &NodeAnnouncement2{}
+ r := bytes.NewReader(rawBytes)
+ err = msg.Decode(r, 0)
+ require.NoError(t, err)
+
+ // Next, encode the message back into a new byte buffer.
+ var b bytes.Buffer
+ err = msg.Encode(&b, 0)
+ require.NoError(t, err)
+
+ // The re-encoded bytes should be exactly the same as the original raw
+ // bytes.
+ require.Equal(t, rawBytes, b.Bytes())
+}
diff --git a/lnwire/test_message.go b/lnwire/test_message.go
index ff7db2d..4e62bee 100644
--- a/lnwire/test_message.go
+++ b/lnwire/test_message.go
@@ -5,12 +5,14 @@ import (
"fmt"
"image/color"
"math"
+ "net"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/lightningnetwork/lnd/fn/v2"
"github.com/lightningnetwork/lnd/tlv"
+ "github.com/lightningnetwork/lnd/tor"
"github.com/stretchr/testify/require"
"pgregory.net/rapid"
)
@@ -1231,6 +1233,10 @@ func (ks *KickoffSig) RandTestMessage(t *rapid.T) Message {
// lnwire.TestMessage interface.
var _ TestMessage = (*NodeAnnouncement1)(nil)
+// A compile time check to ensure NodeAnnouncement2 implements the
+// lnwire.TestMessage interface.
+var _ TestMessage = (*NodeAnnouncement2)(nil)
+
// RandTestMessage populates the message with random data suitable for testing.
// It uses the rapid testing framework to generate random values.
//
@@ -1262,6 +1268,131 @@ func (a *NodeAnnouncement1) RandTestMessage(t *rapid.T) Message {
}
}
+// RandTestMessage populates the message with random data suitable for testing.
+// It uses the rapid testing framework to generate random values.
+//
+// This is part of the TestMessage interface.
+func (n *NodeAnnouncement2) RandTestMessage(t *rapid.T) Message {
+ features := RandFeatureVector(t)
+ blockHeight := uint32(rapid.IntRange(0, 1000000).Draw(t, "blockHeight"))
+
+ // Generate random compressed public key for node ID
+ pubKey := RandPubKey(t)
+ var nodeID [33]byte
+ copy(nodeID[:], pubKey.SerializeCompressed())
+
+ msg := &NodeAnnouncement2{
+ Features: tlv.NewRecordT[tlv.TlvType0](*features),
+ BlockHeight: tlv.NewPrimitiveRecord[tlv.TlvType2](
+ blockHeight,
+ ),
+ NodeID: tlv.NewPrimitiveRecord[tlv.TlvType4](nodeID),
+ ExtraSignedFields: make(map[uint64][]byte),
+ }
+
+ msg.Signature.Val = RandSignature(t)
+ msg.Signature.Val.ForceSchnorr()
+
+ // Test optional fields one by one
+ if rapid.Bool().Draw(t, "includeColor") {
+ rgbColor := Color{
+ R: uint8(rapid.IntRange(0, 255).Draw(t, "rgbR")),
+ G: uint8(rapid.IntRange(0, 255).Draw(t, "rgbG")),
+ B: uint8(rapid.IntRange(0, 255).Draw(t, "rgbB")),
+ }
+ colorRecord := tlv.ZeroRecordT[tlv.TlvType1, Color]()
+ colorRecord.Val = rgbColor
+ msg.Color = tlv.SomeRecordT(colorRecord)
+ }
+
+ if rapid.Bool().Draw(t, "includeAlias") {
+ aliasBytes := RandNodeAlias2(t)
+ aliasRecord := tlv.ZeroRecordT[tlv.TlvType3, NodeAlias2]()
+ aliasRecord.Val = aliasBytes
+ msg.Alias = tlv.SomeRecordT(aliasRecord)
+ }
+
+ if rapid.Bool().Draw(t, "includeIPV4Addrs") {
+ ipv4Addrs := make(IPV4Addrs, 1)
+ ip := make(net.IP, 4)
+ ip[0] = uint8(rapid.IntRange(1, 223).Draw(t, "ip4_0"))
+ ip[1] = uint8(rapid.IntRange(0, 255).Draw(t, "ip4_1"))
+ ip[2] = uint8(rapid.IntRange(0, 255).Draw(t, "ip4_2"))
+ ip[3] = uint8(rapid.IntRange(1, 254).Draw(t, "ip4_3"))
+
+ ipv4Addrs[0] = &net.TCPAddr{
+ IP: ip,
+ Port: rapid.IntRange(1, 65535).Draw(t, "port4"),
+ }
+
+ ipv4Record := tlv.ZeroRecordT[tlv.TlvType5, IPV4Addrs]()
+ ipv4Record.Val = ipv4Addrs
+ msg.IPV4Addrs = tlv.SomeRecordT(ipv4Record)
+ }
+
+ if rapid.Bool().Draw(t, "includeIPV6Addrs") {
+ ipv6Addrs := make(IPV6Addrs, 1)
+ ip := make(net.IP, 16)
+ // Generate random IPv6 address.
+ for j := 0; j < 16; j++ {
+ ip[j] = uint8(rapid.IntRange(0, 255).Draw(
+ t, fmt.Sprintf("ip6_%d", j)),
+ )
+ }
+
+ ipv6Addrs[0] = &net.TCPAddr{
+ IP: ip,
+ Port: rapid.IntRange(1, 65535).Draw(t, "port6"),
+ }
+
+ ipv6Record := tlv.ZeroRecordT[tlv.TlvType7, IPV6Addrs]()
+ ipv6Record.Val = ipv6Addrs
+ msg.IPV6Addrs = tlv.SomeRecordT(ipv6Record)
+ }
+
+ if rapid.Bool().Draw(t, "includeTorV3Addrs") {
+ torV3Addrs := make(TorV3Addrs, 1)
+ onionBytes := rapid.SliceOfN(rapid.Byte(), 35, 35).Draw(
+ t, "onion",
+ )
+ onionService := tor.Base32Encoding.EncodeToString(onionBytes) +
+ tor.OnionSuffix
+
+ torV3Addrs[0] = &tor.OnionAddr{
+ OnionService: onionService,
+ Port: rapid.IntRange(1, 65535).Draw(
+ t, "torPort",
+ ),
+ }
+
+ torV3Record := tlv.ZeroRecordT[tlv.TlvType9, TorV3Addrs]()
+ torV3Record.Val = torV3Addrs
+ msg.TorV3Addrs = tlv.SomeRecordT(torV3Record)
+ }
+
+ if rapid.Bool().Draw(t, "includeDNSHostName") {
+ // Generate a valid DNS hostname.
+ hostname := genValidHostname(t)
+ port := rapid.Uint16Range(1, 65535).Draw(t, "dnsPort")
+
+ dnsAddr := DNSAddress{
+ Hostname: hostname,
+ Port: port,
+ }
+
+ dnsRecord := tlv.ZeroRecordT[tlv.TlvType11, DNSAddress]()
+ dnsRecord.Val = dnsAddr
+ msg.DNSHostName = tlv.SomeRecordT(dnsRecord)
+ }
+
+ randRecs, _ := RandSignedRangeRecords(t)
+ if len(randRecs) > 0 {
+ msg.ExtraSignedFields = ExtraSignedFields(randRecs)
+ }
+
+ return msg
+}
+
// A compile time check to ensure OpenChannel implements the TestMessage
// interface.
var _ TestMessage = (*OpenChannel)(nil)
diff --git a/lnwire/test_utils.go b/lnwire/test_utils.go
index 4c88687..ed86230 100644
--- a/lnwire/test_utils.go
+++ b/lnwire/test_utils.go
@@ -152,6 +152,19 @@ func RandNodeAlias(t *rapid.T) NodeAlias {
return alias
}
+// RandNodeAlias2 generates a random node alias that is compatible with
+// NodeAlias2 validation (non-empty, max 32 bytes, valid UTF-8).
+func RandNodeAlias2(t *rapid.T) NodeAlias2 {
+ // Ensure length is at least 1 to satisfy NodeAlias2 validation
+ aliasLength := rapid.IntRange(1, 32).Draw(t, "aliasLength")
+
+ aliasBytes := rapid.SliceOfN(
+ rapid.SampledFrom([]rune(charset)), aliasLength, aliasLength,
+ ).Draw(t, "alias")
+
+ return []byte(string(aliasBytes))
+}
+
// RandNetAddrs generates random network addresses.
func RandNetAddrs(t *rapid.T) []net.Addr {
numAddresses := rapid.IntRange(0, 5).Draw(t, "numAddresses")
Why this scored 15/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.