• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

lightningnetwork / lnd / 21485572389

29 Jan 2026 04:09PM UTC coverage: 65.247% (+0.2%) from 65.074%
21485572389

Pull #10089

github

web-flow
Merge 22d34d15e into 19b2ad797
Pull Request #10089: Onion message forwarding

1152 of 1448 new or added lines in 23 files covered. (79.56%)

4109 existing lines in 29 files now uncovered.

139515 of 213825 relevant lines covered (65.25%)

20529.09 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

90.78
/onionmessage/endpoint.go
1
package onionmessage
2

3
import (
4
        "context"
5
        "encoding/hex"
6
        "fmt"
7
        "log/slog"
8

9
        "github.com/btcsuite/btcd/btcec/v2"
10
        "github.com/btcsuite/btclog/v2"
11
        sphinx "github.com/lightningnetwork/lightning-onion"
12
        "github.com/lightningnetwork/lnd/actor"
13
        "github.com/lightningnetwork/lnd/fn/v2"
14
        "github.com/lightningnetwork/lnd/lnutils"
15
        "github.com/lightningnetwork/lnd/lnwire"
16
        "github.com/lightningnetwork/lnd/msgmux"
17
        "github.com/lightningnetwork/lnd/record"
18
        "github.com/lightningnetwork/lnd/subscribe"
19
)
20

21
// OnionMessageUpdate is onion message update dispatched to any potential
22
// subscriber.
23
type OnionMessageUpdate struct {
24
        // Peer is the peer pubkey
25
        Peer [33]byte
26

27
        // PathKey is the route blinding ephemeral pubkey to be used for
28
        // the onion message.
29
        PathKey [33]byte
30

31
        // OnionBlob is the raw serialized mix header used to relay messages in
32
        // a privacy-preserving manner. This blob should be handled in the same
33
        // manner as onions used to route HTLCs, with the exception that it uses
34
        // blinded routes by default.
35
        OnionBlob []byte
36

37
        // CustomRecords contains any custom TLV records included in the
38
        // payload.
39
        CustomRecords record.CustomSet
40

41
        // ReplyPath contains the reply path information for the onion message.
42
        ReplyPath *sphinx.BlindedPath
43

44
        // EncryptedRecipientData contains the encrypted recipient data for the
45
        // onion message, created by the creator of the blinded route. This is
46
        // the receiver for the last leg of the route, and the sender for the
47
        // first leg up to the introduction point.
48
        EncryptedRecipientData []byte
49
}
50

51
// OnionEndpointOption defines a function that can be used to configure
52
// an OnionEndpoint. This allows for flexible configuration of the endpoint
53
// when creating a new instance.
54
type OnionEndpointOption func(*OnionEndpoint)
55

56
// WithMessageServer sets the subscribe.Server for the OnionEndpoint.
57
func WithMessageServer(server *subscribe.Server) OnionEndpointOption {
29✔
58
        return func(o *OnionEndpoint) {
58✔
59
                o.onionMessageServer = server
29✔
60
        }
29✔
61
}
62

63
// OnionEndpoint handles incoming onion messages.
64
type OnionEndpoint struct {
65
        // subscribe.Server is used for subscriptions to onion messages.
66
        onionMessageServer *subscribe.Server
67

68
        // router is the sphinx router used to process onion_message_packet
69
        router *sphinx.Router
70

71
        // resolver is used to resolve node public keys from short channel IDs.
72
        resolver NodeIDResolver
73

74
        // receptionist is the actor system receptionist used to look up peer
75
        // onion actors.
76
        receptionist *actor.Receptionist
77
}
78

79
// A compile-time check to ensure OnionEndpoint implements the Endpoint
80
// interface.
81
var _ msgmux.Endpoint = (*OnionEndpoint)(nil)
82

83
// NewOnionEndpoint creates a new OnionEndpoint with the given options.
84
func NewOnionEndpoint(receptionist *actor.Receptionist, router *sphinx.Router,
85
        resolver NodeIDResolver, opts ...OnionEndpointOption) (*OnionEndpoint,
86
        error) {
29✔
87

29✔
88
        if receptionist == nil {
29✔
NEW
89
                return nil, ErrNilReceptionist
×
NEW
90
        }
×
91

92
        if router == nil {
29✔
NEW
93
                return nil, ErrNilRouter
×
NEW
94
        }
×
95

96
        if resolver == nil {
29✔
NEW
97
                return nil, ErrNilResolver
×
NEW
98
        }
×
99

100
        o := &OnionEndpoint{
29✔
101
                receptionist: receptionist,
29✔
102
                router:       router,
29✔
103
                resolver:     resolver,
29✔
104
        }
29✔
105
        for _, opt := range opts {
58✔
106
                opt(o)
29✔
107
        }
29✔
108

109
        log.Info("OnionEndpoint created")
29✔
110

29✔
111
        return o, nil
29✔
112
}
113

114
// Name returns the unique name of the endpoint.
115
func (o *OnionEndpoint) Name() string {
14✔
116
        return "OnionMessageHandler"
14✔
117
}
14✔
118

119
// CanHandle checks if the endpoint can handle the incoming message.
120
// It returns true if the message is an lnwire.OnionMessage.
121
func (o *OnionEndpoint) CanHandle(msg msgmux.PeerMsg) bool {
5✔
122
        _, ok := msg.Message.(*lnwire.OnionMessage)
5✔
123
        return ok
5✔
124
}
5✔
125

126
// SendMessage processes the incoming onion message.
127
// It returns true if the message was successfully processed.
128
func (o *OnionEndpoint) SendMessage(ctx context.Context,
129
        msg msgmux.PeerMsg) bool {
10✔
130

10✔
131
        onionMsg, ok := msg.Message.(*lnwire.OnionMessage)
10✔
132
        if !ok {
10✔
NEW
133
                return false
×
NEW
134
        }
×
135

136
        peer := msg.PeerPub.SerializeCompressed()
10✔
137

10✔
138
        logCtx := btclog.WithCtx(ctx,
10✔
139
                slog.String("peer", hex.EncodeToString(peer)),
10✔
140
                lnutils.LogPubKey("path_key", onionMsg.PathKey),
10✔
141
        )
10✔
142

10✔
143
        log.DebugS(logCtx, "OnionEndpoint received OnionMessage",
10✔
144
                btclog.HexN("onion_blob", onionMsg.OnionBlob, 10),
10✔
145
                slog.Int("blob_length", len(onionMsg.OnionBlob)))
10✔
146

10✔
147
        routingActionResult := processOnionMessage(
10✔
148
                o.router, o.resolver, onionMsg,
10✔
149
        )
10✔
150

10✔
151
        routingAction, err := routingActionResult.Unpack()
10✔
152
        if err != nil {
13✔
153
                log.ErrorS(logCtx, "Failed to handle onion message", err)
3✔
154
                return false
3✔
155
        }
3✔
156

157
        var isProcessedSuccessful bool = false
7✔
158

7✔
159
        // Handle the routing action.
7✔
160
        payload := fn.ElimEither(routingAction,
7✔
161
                func(forwardAction forwardAction) *lnwire.OnionMessagePayload {
13✔
162
                        log.DebugS(logCtx, "Forwarding onion message",
6✔
163
                                lnutils.LogPubKey("next_node_id",
6✔
164
                                        forwardAction.nextNodeID),
6✔
165
                        )
6✔
166

6✔
167
                        err := o.forwardMessage(
6✔
168
                                ctx, forwardAction.nextNodeID,
6✔
169
                                forwardAction.nextPathKey,
6✔
170
                                forwardAction.nextPacket,
6✔
171
                        )
6✔
172

6✔
173
                        if err != nil {
7✔
174
                                log.ErrorS(logCtx, "Failed to forward onion "+
1✔
175
                                        "message", err)
1✔
176
                        }
1✔
177

178
                        isProcessedSuccessful = true
6✔
179

6✔
180
                        return forwardAction.payload
6✔
181
                },
182
                func(deliverAction deliverAction) *lnwire.OnionMessagePayload {
3✔
183
                        log.DebugS(logCtx, "Delivering onion message to self")
3✔
184
                        isProcessedSuccessful = true
3✔
185
                        return deliverAction.payload
3✔
186
                })
3✔
187

188
        // Convert peer []byte to [33]byte.
189
        var peerArr [33]byte
7✔
190
        copy(peerArr[:], peer)
7✔
191

7✔
192
        // Convert path key []byte to [33]byte.
7✔
193
        var pathKeyArr [33]byte
7✔
194
        copy(pathKeyArr[:], onionMsg.PathKey.SerializeCompressed())
7✔
195

7✔
196
        // Create the onion message update to send to subscribers.
7✔
197
        update := &OnionMessageUpdate{
7✔
198
                Peer:      peerArr,
7✔
199
                PathKey:   pathKeyArr,
7✔
200
                OnionBlob: onionMsg.OnionBlob,
7✔
201
        }
7✔
202

7✔
203
        // If we have a payload, add its contents to our update.
7✔
204
        if payload != nil {
14✔
205
                customRecords := make(record.CustomSet)
7✔
206
                for _, v := range payload.FinalHopTLVs {
10✔
207
                        customRecords[uint64(v.TLVType)] = v.Value
3✔
208
                }
3✔
209
                update.CustomRecords = customRecords
7✔
210
                update.ReplyPath = payload.ReplyPath
7✔
211
                update.EncryptedRecipientData = payload.EncryptedData
7✔
212
        }
213

214
        // Send the update to any subscribers.
215
        if sendErr := o.onionMessageServer.SendUpdate(update); sendErr != nil {
7✔
NEW
216
                log.ErrorS(logCtx, "Failed to send onion message update",
×
NEW
217
                        sendErr)
×
NEW
218

×
NEW
219
                return false
×
NEW
220
        }
×
221

222
        return isProcessedSuccessful
7✔
223
}
224

225
// forwardMessage forwards the onion message to the next node using the peer
226
// actor that was spawned for that node.
227
func (o *OnionEndpoint) forwardMessage(ctx context.Context,
228
        nextNodeID *btcec.PublicKey, nextBlindingPoint *btcec.PublicKey,
229
        nextPacket []byte) error {
6✔
230

6✔
231
        var nextNodeIDBytes [33]byte
6✔
232
        copy(nextNodeIDBytes[:], nextNodeID.SerializeCompressed())
6✔
233

6✔
234
        // Find the onion peer actor for the next node ID.
6✔
235
        actorRefOpt := findPeerActor(o.receptionist, nextNodeIDBytes)
6✔
236

6✔
237
        // If no actor found, return an error.
6✔
238
        if actorRefOpt.IsNone() {
7✔
239
                return fmt.Errorf("failed to forward onion message: no peer "+
1✔
240
                        "actor found for node %v (peer may be disconnected)",
1✔
241
                        nextNodeID)
1✔
242
        }
1✔
243

244
        // Send the onion message to the peer actor.
245
        actorRefOpt.WhenSome(func(actorRef actor.ActorRef[*Request,
5✔
246
                *Response]) {
10✔
247

5✔
248
                onionMsg := lnwire.NewOnionMessage(
5✔
249
                        nextBlindingPoint, nextPacket,
5✔
250
                )
5✔
251
                req := &Request{msg: *onionMsg}
5✔
252
                actorRef.Tell(ctx, req)
5✔
253
        })
5✔
254

255
        // Successfully forwarded the message.
256
        return nil
5✔
257
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc