diff --git a/config.go b/config.go index 4a7123bf2..39818397c 100644 --- a/config.go +++ b/config.go @@ -39,6 +39,7 @@ import ( "github.com/lightningnetwork/lnd/lnrpc/routerrpc" "github.com/lightningnetwork/lnd/lnrpc/signrpc" "github.com/lightningnetwork/lnd/lnwallet" + "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/routing" "github.com/lightningnetwork/lnd/signal" "github.com/lightningnetwork/lnd/sweep" @@ -1656,6 +1657,17 @@ func ValidateConfig(cfg Config, interceptor signal.Interceptor, fileParser, lncfg.DefaultIncomingBroadcastDelta) } + // If the experimental protocol options specify any protocol messages + // that we want to handle as custom messages, set them now. + //nolint:lll + customMsg := cfg.ProtocolOptions.ExperimentalProtocol.CustomMessageOverrides() + + // We can safely set our custom override values during startup because + // startup is blocked on config parsing. + if err := lnwire.SetCustomOverrides(customMsg); err != nil { + return nil, mkErr("custom-message: %v", err) + } + // Validate the subconfigs for workers, caches, and the tower client. err = lncfg.Validate( cfg.Workers, diff --git a/docs/release-notes/release-notes-0.16.0.md b/docs/release-notes/release-notes-0.16.0.md index d6c889a90..977a69a26 100644 --- a/docs/release-notes/release-notes-0.16.0.md +++ b/docs/release-notes/release-notes-0.16.0.md @@ -92,6 +92,11 @@ current gossip sync query status. * [Ensure that closing addresses match the node network for `OpenChannel` requests](https://github.com/lightningnetwork/lnd/pull/7272) +* The `SendCustomMessage` and `SubscribeCustomMessage` APIs can now be used to + send and receive custom messages below the custom range if lnd is built with + the `dev` tag, and configured to [opt into overriding a specific message + type](https://github.com/lightningnetwork/lnd/pull/7153) + ## Wallet * [Allows Taproot public keys and tap scripts to be imported as watch-only diff --git a/lncfg/protocol_experimental_off.go b/lncfg/protocol_experimental_off.go index 34143a224..e88b4b52b 100644 --- a/lncfg/protocol_experimental_off.go +++ b/lncfg/protocol_experimental_off.go @@ -7,3 +7,9 @@ package lncfg // features that also require a build-tag to activate. type ExperimentalProtocol struct { } + +// CustomMessageOverrides returns the set of protocol messages that we override +// to allow custom handling. +func (p ExperimentalProtocol) CustomMessageOverrides() []uint16 { + return nil +} diff --git a/lncfg/protocol_experimental_on.go b/lncfg/protocol_experimental_on.go index b7d74acfe..cf49890ae 100644 --- a/lncfg/protocol_experimental_on.go +++ b/lncfg/protocol_experimental_on.go @@ -5,5 +5,14 @@ package lncfg // ExperimentalProtocol is a sub-config that houses any experimental protocol // features that also require a build-tag to activate. +// +//nolint:lll type ExperimentalProtocol struct { + CustomMessage []uint16 `long:"custom-message" description:"allows the custom message apis to send and report messages with the protocol number provided that fall outside of the custom message number range."` +} + +// CustomMessageOverrides returns the set of protocol messages that we override +// to allow custom handling. +func (p ExperimentalProtocol) CustomMessageOverrides() []uint16 { + return p.CustomMessage } diff --git a/lnrpc/lightning.pb.go b/lnrpc/lightning.pb.go index 4c0727b09..45e68d161 100644 --- a/lnrpc/lightning.pb.go +++ b/lnrpc/lightning.pb.go @@ -1517,6 +1517,9 @@ type SendCustomMessageRequest struct { // Peer to send the message to Peer []byte `protobuf:"bytes,1,opt,name=peer,proto3" json:"peer,omitempty"` // Message type. This value needs to be in the custom range (>= 32768). + // To send a type < custom range, lnd needs to be compiled with the `dev` + // build tag, and the message type to override should be specified in lnd's + // experimental protocol configuration. Type uint32 `protobuf:"varint,2,opt,name=type,proto3" json:"type,omitempty"` // Raw message data. Data []byte `protobuf:"bytes,3,opt,name=data,proto3" json:"data,omitempty"` diff --git a/lnrpc/lightning.proto b/lnrpc/lightning.proto index 5fa726ac9..3b4294a17 100644 --- a/lnrpc/lightning.proto +++ b/lnrpc/lightning.proto @@ -567,6 +567,10 @@ service Lightning { /* lncli: `subscribecustom` SubscribeCustomMessages subscribes to a stream of incoming custom peer messages. + + To include messages with type outside of the custom range (>= 32768) lnd + needs to be compiled with the `dev` build tag, and the message type to + override should be specified in lnd's experimental protocol configuration. */ rpc SubscribeCustomMessages (SubscribeCustomMessagesRequest) returns (stream CustomMessage); @@ -612,6 +616,9 @@ message SendCustomMessageRequest { bytes peer = 1; // Message type. This value needs to be in the custom range (>= 32768). + // To send a type < custom range, lnd needs to be compiled with the `dev` + // build tag, and the message type to override should be specified in lnd's + // experimental protocol configuration. uint32 type = 2; // Raw message data. diff --git a/lnrpc/lightning.swagger.json b/lnrpc/lightning.swagger.json index aa6112f42..85b94f6fc 100644 --- a/lnrpc/lightning.swagger.json +++ b/lnrpc/lightning.swagger.json @@ -918,6 +918,7 @@ "/v1/custommessage/subscribe": { "get": { "summary": "lncli: `subscribecustom`\nSubscribeCustomMessages subscribes to a stream of incoming custom peer\nmessages.", + "description": "To include messages with type outside of the custom range (\u003e= 32768) lnd\nneeds to be compiled with the `dev` build tag, and the message type to\noverride should be specified in lnd's experimental protocol configuration.", "operationId": "Lightning_SubscribeCustomMessages", "responses": { "200": { @@ -6616,7 +6617,7 @@ "type": { "type": "integer", "format": "int64", - "description": "Message type. This value needs to be in the custom range (\u003e= 32768)." + "description": "Message type. This value needs to be in the custom range (\u003e= 32768).\nTo send a type \u003c custom range, lnd needs to be compiled with the `dev`\nbuild tag, and the message type to override should be specified in lnd's\nexperimental protocol configuration." }, "data": { "type": "string", diff --git a/lnrpc/lightning_grpc.pb.go b/lnrpc/lightning_grpc.pb.go index 9306f849c..a384a77f2 100644 --- a/lnrpc/lightning_grpc.pb.go +++ b/lnrpc/lightning_grpc.pb.go @@ -392,6 +392,10 @@ type LightningClient interface { // lncli: `subscribecustom` // SubscribeCustomMessages subscribes to a stream of incoming custom peer // messages. + // + // To include messages with type outside of the custom range (>= 32768) lnd + // needs to be compiled with the `dev` build tag, and the message type to + // override should be specified in lnd's experimental protocol configuration. SubscribeCustomMessages(ctx context.Context, in *SubscribeCustomMessagesRequest, opts ...grpc.CallOption) (Lightning_SubscribeCustomMessagesClient, error) // lncli: `listaliases` // ListAliases returns the set of all aliases that have ever existed with @@ -1687,6 +1691,10 @@ type LightningServer interface { // lncli: `subscribecustom` // SubscribeCustomMessages subscribes to a stream of incoming custom peer // messages. + // + // To include messages with type outside of the custom range (>= 32768) lnd + // needs to be compiled with the `dev` build tag, and the message type to + // override should be specified in lnd's experimental protocol configuration. SubscribeCustomMessages(*SubscribeCustomMessagesRequest, Lightning_SubscribeCustomMessagesServer) error // lncli: `listaliases` // ListAliases returns the set of all aliases that have ever existed with diff --git a/lntest/itest/lnd_custom_message.go b/lntest/itest/lnd_custom_message.go new file mode 100644 index 000000000..1d9c36d45 --- /dev/null +++ b/lntest/itest/lnd_custom_message.go @@ -0,0 +1,188 @@ +package itest + +import ( + "context" + "fmt" + "sync" + "time" + + "github.com/lightningnetwork/lnd/lnrpc" + "github.com/lightningnetwork/lnd/lntest" + "github.com/lightningnetwork/lnd/lnwire" + "github.com/stretchr/testify/require" +) + +// testCustomMessage tests sending and receiving of overridden custom message +// types (within the message type range usually reserved for protocol messages) +// via the send and subscribe custom message APIs. +func testCustomMessage(net *lntest.NetworkHarness, t *harnessTest) { + ctx, cancel := context.WithCancel(context.Background()) + var wg sync.WaitGroup + + // At the end of our test, cancel our context and wait for all + // goroutines to exit. + defer func() { + cancel() + wg.Wait() + }() + + var ( + overrideType1 uint32 = 554 + overrideType2 uint32 = 555 + msgOverrideArg = "--protocol.custom-message=%v" + ) + + // Update Alice to accept custom protocol messages with type 1 but do + // not allow Bob to handle them yet. + net.Alice.Cfg.ExtraArgs = append( + net.Alice.Cfg.ExtraArgs, + fmt.Sprintf(msgOverrideArg, overrideType1), + ) + require.NoError(t.t, net.RestartNode(net.Alice, nil, nil)) + + // Wait for Alice's server to be active after the restart before we + // try to subscribe to our message stream. + require.NoError(t.t, net.Alice.WaitUntilServerActive()) + + // Subscribe Alice to custom messages before we send any, so that we + // don't miss any. + msgClient, err := net.Alice.LightningClient.SubscribeCustomMessages( + ctx, &lnrpc.SubscribeCustomMessagesRequest{}, + ) + require.NoError(t.t, err, "alice could not subscribe") + + // Create a channel to receive custom messages on. + messages := make(chan *lnrpc.CustomMessage) + wg.Add(1) + go func() { + defer wg.Done() + for { + // If we fail to receive, just exit. The test should + // fail elsewhere if it doesn't get a message that it + // was expecting. + msg, err := msgClient.Recv() + if err != nil { + return + } + + // Deliver the message into our channel or exit if the + // test is shutting down. + select { + case messages <- msg: + case <-ctx.Done(): + return + } + } + }() + + // Connect alice and bob so that they can exchange messages. + net.EnsureConnected(t.t, net.Alice, net.Bob) + + // Create a custom message that is within our allowed range. + msgType := uint32(lnwire.CustomTypeStart + 1) + msgData := []byte{1, 2, 3} + + // Send it from Bob to Alice. + ctxt, _ := context.WithTimeout(ctx, defaultTimeout) + _, err = net.Bob.LightningClient.SendCustomMessage( + ctxt, &lnrpc.SendCustomMessageRequest{ + Peer: net.Alice.PubKey[:], + Type: msgType, + Data: msgData, + }, + ) + require.NoError(t.t, err, "bob could not send") + + // Wait for Alice to receive the message. It should come through because + // it is within our allowed range. + select { + case msg := <-messages: + // Check our type and data and (sanity) check the peer we got it + // from. + require.Equal(t.t, msgType, msg.Type, "first msg type wrong") + require.Equal(t.t, msgData, msg.Data, "first msg data wrong") + require.Equal(t.t, net.Bob.PubKey[:], msg.Peer, "first msg "+ + "peer wrong") + + case <-time.After(defaultTimeout): + t.t.Fatalf("alice did not receive first custom message: %v", + msgType) + } + + // Try to send a message from Bob to Alice which has a message type + // outside of the custom type range and assert that it fails. + ctxt, _ = context.WithTimeout(ctx, defaultTimeout) + _, err = net.Bob.LightningClient.SendCustomMessage( + ctxt, &lnrpc.SendCustomMessageRequest{ + Peer: net.Alice.PubKey[:], + Type: overrideType1, + Data: msgData, + }, + ) + require.Error(t.t, err, "bob should not be able to send type 1") + + // Now, restart Bob with the ability to send two different custom + // protocol messages. + net.Bob.Cfg.ExtraArgs = append( + net.Bob.Cfg.ExtraArgs, + fmt.Sprintf(msgOverrideArg, overrideType1), + fmt.Sprintf(msgOverrideArg, overrideType2), + ) + require.NoError(t.t, net.RestartNode(net.Bob, nil, nil)) + + // Make sure Bob and Alice are connected after his restart. + net.EnsureConnected(t.t, net.Alice, net.Bob) + + // Send a message from Bob to Alice with a type that Bob is allowed to + // send, but Alice will not handle as a custom message. + ctxt, _ = context.WithTimeout(ctx, defaultTimeout) + _, err = net.Bob.LightningClient.SendCustomMessage( + ctxt, &lnrpc.SendCustomMessageRequest{ + Peer: net.Alice.PubKey[:], + Type: overrideType2, + Data: msgData, + }, + ) + require.NoError(t.t, err, "bob should be able to send type 2") + + // Do a quick check that Alice did not receive this message in her + // stream. Note that this is an instant check, so could miss the message + // being received. We'll also check below that she didn't get it, this + // is just a sanity check. + select { + case msg := <-messages: + t.t.Fatalf("unexpected message: %v", msg) + default: + } + + // Finally, send a custom message with a type that Bob is allowed to + // send and Alice is configured to receive. + ctxt, _ = context.WithTimeout(ctx, defaultTimeout) + _, err = net.Bob.LightningClient.SendCustomMessage( + ctxt, &lnrpc.SendCustomMessageRequest{ + Peer: net.Alice.PubKey[:], + Type: overrideType1, + Data: msgData, + }, + ) + require.NoError(t.t, err, "bob should be able to send type 1") + + // Wait to receive a message from Bob. This check serves to ensure that + // our message type 1 was delivered, and assert that the preceding one + // was not (we could have missed it in our check above). When we receive + // the second message, we know that the first one did not go through, + // because we expect our messages to deliver in order. + select { + case msg := <-messages: + // Check our type and data and (sanity) check the peer we got it + // from. + require.Equal(t.t, overrideType1, msg.Type, "second message "+ + "type") + require.Equal(t.t, msgData, msg.Data, "second message data") + require.Equal(t.t, net.Bob.PubKey[:], msg.Peer, "second "+ + "message peer") + + case <-time.After(defaultTimeout): + t.t.Fatalf("alice did not receive second custom message") + } +} diff --git a/lntest/itest/lnd_test_list_on_test.go b/lntest/itest/lnd_test_list_on_test.go index c2d0ace9f..fe56fcf0d 100644 --- a/lntest/itest/lnd_test_list_on_test.go +++ b/lntest/itest/lnd_test_list_on_test.go @@ -274,4 +274,8 @@ var allTestCases = []*testCase{ name: "open channel fee policy", test: testOpenChannelUpdateFeePolicy, }, + { + name: "custom messaging", + test: testCustomMessage, + }, } diff --git a/lnwire/custom.go b/lnwire/custom.go index f012b8aec..232a8be52 100644 --- a/lnwire/custom.go +++ b/lnwire/custom.go @@ -2,13 +2,66 @@ package lnwire import ( "bytes" - "errors" + "fmt" "io" + "sync" ) // CustomTypeStart is the start of the custom type range for peer messages as // defined in BOLT 01. -var CustomTypeStart MessageType = 32768 +const CustomTypeStart MessageType = 32768 + +var ( + // customTypeOverride contains a set of message types < CustomTypeStart + // that lnd allows to be treated as custom messages. This allows us to + // override messages reserved for the protocol level and treat them as + // custom messages. This set of message types is stored as a global so + // that we do not need to pass around state when accounting for this + // set of messages in message creation. + // + // Note: This global is protected by the customTypeOverride mutex. + customTypeOverride map[MessageType]struct{} + + // customTypeOverrideMtx manages concurrent access to + // customTypeOverride. + customTypeOverrideMtx sync.RWMutex +) + +// SetCustomOverrides validates that the set of override types are outside of +// the custom message range (there's no reason to override messages that are +// already within the range), and updates the customTypeOverride global to hold +// this set of message types. Note that this function will completely overwrite +// the set of overrides, so should be called with the full set of types. +func SetCustomOverrides(overrideTypes []uint16) error { + customTypeOverrideMtx.Lock() + defer customTypeOverrideMtx.Unlock() + + customTypeOverride = make(map[MessageType]struct{}, len(overrideTypes)) + + for _, t := range overrideTypes { + msgType := MessageType(t) + + if msgType >= CustomTypeStart { + return fmt.Errorf("can't override type: %v, already "+ + "in custom range", t) + } + + customTypeOverride[msgType] = struct{}{} + } + + return nil +} + +// IsCustomOverride returns a bool indicating whether the message type is one +// of the protocol messages that we override for custom use. +func IsCustomOverride(t MessageType) bool { + customTypeOverrideMtx.RLock() + defer customTypeOverrideMtx.RUnlock() + + _, ok := customTypeOverride[t] + + return ok +} // Custom represents an application-defined wire message. type Custom struct { @@ -20,10 +73,11 @@ type Custom struct { // interface. var _ Message = (*Custom)(nil) -// NewCustom instanties a new custom message. +// NewCustom instantiates a new custom message. func NewCustom(msgType MessageType, data []byte) (*Custom, error) { - if msgType < CustomTypeStart { - return nil, errors.New("msg type not in custom range") + if msgType < CustomTypeStart && !IsCustomOverride(msgType) { + return nil, fmt.Errorf("msg type: %d not in custom range: %v "+ + "and not overridden", msgType, CustomTypeStart) } return &Custom{ diff --git a/lnwire/message.go b/lnwire/message.go index ac57c8a29..2e71173e5 100644 --- a/lnwire/message.go +++ b/lnwire/message.go @@ -237,9 +237,16 @@ func makeEmptyMessage(msgType MessageType) (Message, error) { case MsgGossipTimestampRange: msg = &GossipTimestampRange{} default: - if msgType < CustomTypeStart { + // If the message is not within our custom range and has not + // specifically been overridden, return an unknown message. + // + // Note that we do not allow custom message overrides to replace + // known message types, only protocol messages that are not yet + // known to lnd. + if msgType < CustomTypeStart && !IsCustomOverride(msgType) { return nil, &UnknownMessage{msgType} } + msg = &Custom{ Type: msgType, }