• 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

98.22
/onionmessage/test_utils.go
1
package onionmessage
2

3
import (
4
        "bytes"
5
        "fmt"
6
        "testing"
7

8
        "github.com/btcsuite/btcd/btcec/v2"
9
        sphinx "github.com/lightningnetwork/lightning-onion"
10
        "github.com/lightningnetwork/lnd/lnwire"
11
        "github.com/lightningnetwork/lnd/record"
12
        "github.com/lightningnetwork/lnd/routing/route"
13
        "github.com/lightningnetwork/lnd/tlv"
14
        "github.com/stretchr/testify/require"
15
)
16

17
// mockNodeIDResolver implements NodeIDResolver for tests.
18
type mockNodeIDResolver struct {
19
        peers map[lnwire.ShortChannelID]*btcec.PublicKey
20
}
21

22
// addPeer registers a single SCID to pubkey mapping for tests.
23
func (m *mockNodeIDResolver) addPeer(scid lnwire.ShortChannelID,
24
        pubKey *btcec.PublicKey) {
3✔
25

3✔
26
        m.peers[scid] = pubKey
3✔
27
}
3✔
28

29
// newMockNodeIDResolver creates a new instance of mockNodeIDResolver.
30
func newMockNodeIDResolver() *mockNodeIDResolver {
11✔
31
        return &mockNodeIDResolver{
11✔
32
                peers: make(map[lnwire.ShortChannelID]*btcec.PublicKey),
11✔
33
        }
11✔
34
}
11✔
35

36
// RemotePubFromSCID resolves a node public key from a short channel ID.
37
func (m *mockNodeIDResolver) RemotePubFromSCID(
38
        scid lnwire.ShortChannelID) (*btcec.PublicKey, error) {
5✔
39

5✔
40
        if pk, ok := m.peers[scid]; ok {
8✔
41
                return pk, nil
3✔
42
        }
3✔
43

44
        return nil, fmt.Errorf("unknown scid: %v", scid)
2✔
45
}
46

47
// EncodeBlindedRouteData encodes BlindedRouteData to bytes for use in test
48
// hop payloads.
49
func EncodeBlindedRouteData(t *testing.T,
50
        data *record.BlindedRouteData) []byte {
12✔
51

12✔
52
        t.Helper()
12✔
53

12✔
54
        buf, err := record.EncodeBlindedRouteData(data)
12✔
55
        require.NoError(t, err)
12✔
56

12✔
57
        return buf
12✔
58
}
12✔
59

60
// BuildBlindedPath creates a BlindedPathInfo from a list of HopInfo. This is a
61
// test helper that wraps sphinx.BuildBlindedPath with a fresh session key.
62
func BuildBlindedPath(t *testing.T,
63
        hops []*sphinx.HopInfo) *sphinx.BlindedPathInfo {
12✔
64

12✔
65
        t.Helper()
12✔
66

12✔
67
        sessionKey, err := btcec.NewPrivateKey()
12✔
68
        require.NoError(t, err)
12✔
69

12✔
70
        blindedPath, err := sphinx.BuildBlindedPath(sessionKey, hops)
12✔
71
        require.NoError(t, err)
12✔
72

12✔
73
        return blindedPath
12✔
74
}
12✔
75

76
// ConcatBlindedPaths concatenates two blinded paths. The sender's path points
77
// TO the introduction node (with NextBlindingOverride), and the receiver's
78
// path starts AT the introduction node. The concatenated path includes all
79
// hops from both paths - the sender's last hop instructs forwarding to the
80
// intro node, and all receiver hops follow.
81
func ConcatBlindedPaths(t *testing.T, senderPath,
82
        receiverPath *sphinx.BlindedPathInfo) *sphinx.BlindedPathInfo {
1✔
83

1✔
84
        t.Helper()
1✔
85

1✔
86
        // The resulting path uses the sender's session key and introduction
1✔
87
        // point but concatenates all blinded hops.
1✔
88
        concatenated := &sphinx.BlindedPath{
1✔
89
                IntroductionPoint: senderPath.Path.IntroductionPoint,
1✔
90
                BlindingPoint:     senderPath.Path.BlindingPoint,
1✔
91
                BlindedHops: append(
1✔
92
                        senderPath.Path.BlindedHops,
1✔
93
                        receiverPath.Path.BlindedHops...,
1✔
94
                ),
1✔
95
        }
1✔
96

1✔
97
        return &sphinx.BlindedPathInfo{
1✔
98
                Path:             concatenated,
1✔
99
                SessionKey:       senderPath.SessionKey,
1✔
100
                LastEphemeralKey: receiverPath.LastEphemeralKey,
1✔
101
        }
1✔
102
}
1✔
103

104
// BuildOnionMessage builds an onion message from a BlindedPathInfo and returns
105
// the message along with the ciphertexts for each blinded hop (in hop order).
106
// If finalPayloads is nil or empty, no final hop payload data is included.
107
func BuildOnionMessage(t *testing.T, blindedPath *sphinx.BlindedPathInfo,
108
        finalHopTLVs []*lnwire.FinalHopTLV) (*lnwire.OnionMessage,
109
        [][]byte) {
11✔
110

11✔
111
        t.Helper()
11✔
112

11✔
113
        // Convert the blinded path to a sphinx path and add final payloads.
11✔
114
        sphinxPath, err := route.OnionMessageBlindedPathToSphinxPath(
11✔
115
                blindedPath.Path, nil, finalHopTLVs,
11✔
116
        )
11✔
117
        require.NoError(t, err)
11✔
118

11✔
119
        onionSessionKey, err := btcec.NewPrivateKey()
11✔
120
        require.NoError(t, err)
11✔
121

11✔
122
        // Create an onion packet with no associated data.
11✔
123
        onionPkt, err := sphinx.NewOnionPacket(
11✔
124
                sphinxPath, onionSessionKey, nil,
11✔
125
                sphinx.DeterministicPacketFiller,
11✔
126
                sphinx.WithMaxPayloadSize(sphinx.MaxRoutingPayloadSize),
11✔
127
        )
11✔
128
        require.NoError(t, err)
11✔
129

11✔
130
        // Encode the onion message packet.
11✔
131
        var buf bytes.Buffer
11✔
132
        require.NoError(t, onionPkt.Encode(&buf))
11✔
133

11✔
134
        onionMsg := &lnwire.OnionMessage{
11✔
135
                PathKey:   blindedPath.SessionKey.PubKey(),
11✔
136
                OnionBlob: buf.Bytes(),
11✔
137
        }
11✔
138

11✔
139
        var ctexts [][]byte
11✔
140
        for _, bh := range blindedPath.Path.BlindedHops {
31✔
141
                ctexts = append(ctexts, bh.CipherText)
20✔
142
        }
20✔
143

144
        return onionMsg, ctexts
11✔
145
}
146

147
// PeeledHop captures decrypted state for a single hop when peeling an onion.
148
type PeeledHop struct {
149
        EncryptedData []byte
150
        Payload       *lnwire.OnionMessagePayload
151
        IsFinal       bool
152
}
153

154
// PeelOnionLayers sequentially processes an onion message, creating a fresh
155
// router for each hop using the provided private keys (one per hop), returning
156
// the encrypted data and decoded payload for each hop until the final hop.
157
func PeelOnionLayers(t *testing.T, privKeys []*btcec.PrivateKey,
158
        msg *lnwire.OnionMessage) []PeeledHop {
5✔
159

5✔
160
        t.Helper()
5✔
161

5✔
162
        var onionPkt sphinx.OnionPacket
5✔
163
        require.NoError(t, onionPkt.Decode(bytes.NewReader(msg.OnionBlob)))
5✔
164

5✔
165
        currentPathKey := msg.PathKey
5✔
166
        var hops []PeeledHop
5✔
167

5✔
168
        for i := 0; ; i++ {
15✔
169
                require.Less(t, i, len(privKeys), "more hops than privKeys")
10✔
170

10✔
171
                router := sphinx.NewRouter(
10✔
172
                        &sphinx.PrivKeyECDH{PrivKey: privKeys[i]},
10✔
173
                        sphinx.NewMemoryReplayLog(),
10✔
174
                )
10✔
175
                require.NoError(t, router.Start())
10✔
176

10✔
177
                processedPkt, err := router.ProcessOnionPacket(
10✔
178
                        &onionPkt, nil, 10,
10✔
179
                        sphinx.WithBlindingPoint(currentPathKey),
10✔
180
                )
10✔
181
                require.NoError(t, err)
10✔
182

10✔
183
                payload := lnwire.NewOnionMessagePayload()
10✔
184
                _, err = payload.Decode(
10✔
185
                        bytes.NewReader(processedPkt.Payload.Payload),
10✔
186
                )
10✔
187
                require.NoError(t, err)
10✔
188

10✔
189
                origPayload := *payload
10✔
190
                origPayload.EncryptedData = bytes.Clone(payload.EncryptedData)
10✔
191

10✔
192
                isFinal := processedPkt.Action == sphinx.ExitNode
10✔
193
                hops = append(hops, PeeledHop{
10✔
194
                        EncryptedData: origPayload.EncryptedData,
10✔
195
                        Payload:       &origPayload,
10✔
196
                        IsFinal:       isFinal,
10✔
197
                })
10✔
198

10✔
199
                if isFinal {
15✔
200
                        router.Stop()
5✔
201
                        break
5✔
202
                }
203

204
                decrypted, err := router.DecryptBlindedHopData(
5✔
205
                        currentPathKey, payload.EncryptedData,
5✔
206
                )
5✔
207
                require.NoError(t, err)
5✔
208

5✔
209
                routeData, err := record.DecodeBlindedRouteData(
5✔
210
                        bytes.NewReader(decrypted),
5✔
211
                )
5✔
212
                require.NoError(t, err)
5✔
213

5✔
214
                nextPathKey := deriveNextPathKeyForTest(
5✔
215
                        router, currentPathKey, routeData.NextBlindingOverride,
5✔
216
                )
5✔
217
                require.NotNil(t, nextPathKey)
5✔
218

5✔
219
                router.Stop()
5✔
220

5✔
221
                onionPkt = *processedPkt.NextPacket
5✔
222
                currentPathKey = nextPathKey
5✔
223
        }
224

225
        return hops
5✔
226
}
227

228
// deriveNextPathKeyForTest derives the next path key using the router and
229
// current path key. If an override is provided, it is used instead.
230
func deriveNextPathKeyForTest(router *sphinx.Router,
231
        currentPathKey *btcec.PublicKey,
232
        override tlv.OptionalRecordT[tlv.TlvType8,
233
                *btcec.PublicKey]) *btcec.PublicKey {
5✔
234

5✔
235
        // If an override is provided, use it.
5✔
236
        return override.UnwrapOrFunc(func() tlv.RecordT[tlv.TlvType8,
5✔
237
                *btcec.PublicKey] {
9✔
238

4✔
239
                // Otherwise, derive the next path key using the router.
4✔
240
                nextKey, err := router.NextEphemeral(currentPathKey)
4✔
241
                if err != nil {
4✔
NEW
242
                        // If the derivation fails, return a zero key.
×
NEW
243
                        return override.Zero()
×
NEW
244
                }
×
245

246
                return tlv.NewPrimitiveRecord[tlv.TlvType8](nextKey)
4✔
247
        }).Val
248
}
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