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

lightningnetwork / lnd / 17236841262

26 Aug 2025 11:33AM UTC coverage: 66.228% (+8.9%) from 57.321%
17236841262

Pull #9147

github

web-flow
Merge 39fde1801 into 0c2f045f5
Pull Request #9147: [Part 1|3] Introduce SQL Payment schema into LND

129 of 1847 new or added lines in 13 files covered. (6.98%)

20 existing lines in 7 files now uncovered.

136069 of 205456 relevant lines covered (66.23%)

21357.68 hits per line

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

0.0
/payments/db/sql_convert.go
1
package paymentsdb
2

3
import (
4
        "bytes"
5
        "fmt"
6
        "sort"
7
        "strconv"
8
        "time"
9

10
        "github.com/btcsuite/btcd/btcec/v2"
11
        "github.com/lightningnetwork/lnd/lntypes"
12
        "github.com/lightningnetwork/lnd/lnwire"
13
        "github.com/lightningnetwork/lnd/record"
14
        "github.com/lightningnetwork/lnd/routing/route"
15
        "github.com/lightningnetwork/lnd/sqldb/sqlc"
16
        "github.com/lightningnetwork/lnd/tlv"
17
)
18

19
// unmarshalHtlcAttempt converts a sqlc.PaymentHtlcAttempt and its hops to
20
// an HTLCAttempt.
21
func unmarshalHtlcAttempt(dbAttempt sqlc.PaymentHtlcAttempt,
22
        hops []sqlc.PaymentRouteHop,
23
        dbRouteCustomRecords []sqlc.PaymentHtlcAttemptCustomRecord) (
NEW
24
        *HTLCAttempt, error) {
×
NEW
25

×
NEW
26
        if len(dbAttempt.SessionKey) != btcec.PrivKeyBytesLen {
×
NEW
27
                return nil, fmt.Errorf("invalid session key length: %d",
×
NEW
28
                        len(dbAttempt.SessionKey))
×
NEW
29
        }
×
30

31
        // Convert session key of the sphinx packet.
NEW
32
        var sessionKey [btcec.PrivKeyBytesLen]byte
×
NEW
33
        copy(sessionKey[:], dbAttempt.SessionKey)
×
NEW
34

×
NEW
35
        // Reconstruct the route from hops
×
NEW
36
        route, err := unmarshalRoute(dbAttempt, hops, dbRouteCustomRecords)
×
NEW
37
        if err != nil {
×
NEW
38
                return nil, fmt.Errorf("unable to reconstruct route: %w", err)
×
NEW
39
        }
×
40

NEW
41
        var hash lntypes.Hash
×
NEW
42
        copy(hash[:], dbAttempt.PaymentHash)
×
NEW
43

×
NEW
44
        // Create HTLCAttemptInfo
×
NEW
45
        htlcInfo := HTLCAttemptInfo{
×
NEW
46
                AttemptID:   uint64(dbAttempt.AttemptIndex),
×
NEW
47
                sessionKey:  sessionKey,
×
NEW
48
                AttemptTime: dbAttempt.AttemptTime.Local(),
×
NEW
49
                Route:       *route,
×
NEW
50
                Hash:        &hash,
×
NEW
51
        }
×
NEW
52

×
NEW
53
        // Create the HTLCAttempt
×
NEW
54
        htlcAttempt := &HTLCAttempt{
×
NEW
55
                HTLCAttemptInfo: htlcInfo,
×
NEW
56
        }
×
NEW
57

×
NEW
58
        // Handle settle info.
×
NEW
59
        if dbAttempt.SettlePreimage != nil {
×
NEW
60
                var preimage lntypes.Preimage
×
NEW
61
                if len(dbAttempt.SettlePreimage) != 32 {
×
NEW
62
                        return nil, fmt.Errorf("invalid preimage length: %d",
×
NEW
63
                                len(dbAttempt.SettlePreimage))
×
NEW
64
                }
×
NEW
65
                copy(preimage[:], dbAttempt.SettlePreimage)
×
NEW
66

×
NEW
67
                var settleTime time.Time
×
NEW
68
                if dbAttempt.SettleTime.Valid {
×
NEW
69
                        settleTime = dbAttempt.SettleTime.Time.Local()
×
NEW
70
                }
×
71

NEW
72
                htlcAttempt.Settle = &HTLCSettleInfo{
×
NEW
73
                        Preimage:   preimage,
×
NEW
74
                        SettleTime: settleTime,
×
NEW
75
                }
×
76
        }
77

78
        // Handle failure info.
NEW
79
        if dbAttempt.HtlcFailReason.Valid {
×
NEW
80
                htlcAttempt.Failure = &HTLCFailInfo{
×
NEW
81
                        FailTime: dbAttempt.FailTime.Time.Local(),
×
NEW
82
                        Reason: HTLCFailReason(
×
NEW
83
                                dbAttempt.HtlcFailReason.Int32,
×
NEW
84
                        ),
×
NEW
85
                        FailureSourceIndex: uint32(
×
NEW
86
                                dbAttempt.FailureSourceIndex.Int32),
×
NEW
87
                }
×
NEW
88

×
NEW
89
                // If we have a failure message, we could parse it here
×
NEW
90
                if dbAttempt.FailureMsg != nil {
×
NEW
91
                        failureMsg, err := lnwire.DecodeFailureMessage(
×
NEW
92
                                bytes.NewReader(dbAttempt.FailureMsg), 0)
×
NEW
93
                        if err != nil {
×
NEW
94
                                return nil, fmt.Errorf(
×
NEW
95
                                        "unable to decode failure message: %w",
×
NEW
96
                                        err)
×
NEW
97
                        }
×
98

NEW
99
                        htlcAttempt.Failure.Message = failureMsg
×
100
                }
101
        }
102

NEW
103
        return htlcAttempt, nil
×
104
}
105

106
// unmarshalRoute converts a sqlc.PaymentHtlcAttempt and its hops to a
107
// route.Route.
108
func unmarshalRoute(dbAttempt sqlc.PaymentHtlcAttempt,
109
        hops []sqlc.PaymentRouteHop,
110
        dbRouteCustomRecords []sqlc.PaymentHtlcAttemptCustomRecord) (
NEW
111
        *route.Route, error) {
×
NEW
112

×
NEW
113
        // Unmarshal the hops.
×
NEW
114
        routeHops, err := unmarshalRouteHops(hops)
×
NEW
115
        if err != nil {
×
NEW
116
                return nil, fmt.Errorf("unable to unmarshal route hops: %w",
×
NEW
117
                        err)
×
NEW
118
        }
×
119

120
        // Add the additional data to the route.
NEW
121
        sourcePubKey, err := route.NewVertexFromBytes(dbAttempt.RouteSourceKey)
×
NEW
122
        if err != nil {
×
NEW
123
                return nil, fmt.Errorf("unable to convert source "+
×
NEW
124
                        "pubkey: %w", err)
×
NEW
125
        }
×
126

NEW
127
        route := &route.Route{
×
NEW
128
                TotalTimeLock: uint32(dbAttempt.RouteTotalTimeLock),
×
NEW
129
                TotalAmount:   lnwire.MilliSatoshi(dbAttempt.RouteTotalAmount),
×
NEW
130
                SourcePubKey:  sourcePubKey,
×
NEW
131
                Hops:          routeHops,
×
NEW
132
                FirstHopAmount: tlv.NewRecordT[tlv.TlvType0](
×
NEW
133
                        tlv.NewBigSizeT(
×
NEW
134
                                lnwire.MilliSatoshi(
×
NEW
135
                                        dbAttempt.FirstHopAmountMsat,
×
NEW
136
                                ),
×
NEW
137
                        ),
×
NEW
138
                ),
×
NEW
139
        }
×
NEW
140

×
NEW
141
        // Attach the custom records to the route.
×
NEW
142
        customRecords, err := unmarshalRouteCustomRecords(
×
NEW
143
                dbRouteCustomRecords,
×
NEW
144
        )
×
NEW
145
        if err != nil {
×
NEW
146
                return nil, fmt.Errorf("unable to unmarshal route "+
×
NEW
147
                        "custom records: %w", err)
×
NEW
148
        }
×
149

NEW
150
        route.FirstHopWireCustomRecords = customRecords
×
NEW
151

×
NEW
152
        return route, nil
×
153
}
154

155
// unmarshalHops converts a slice of sqlc.PaymentRouteHop to a slice of
156
// *route.Hop.
NEW
157
func unmarshalRouteHops(dbHops []sqlc.PaymentRouteHop) ([]*route.Hop, error) {
×
NEW
158
        if len(dbHops) == 0 {
×
NEW
159
                return nil, fmt.Errorf("no hops provided")
×
NEW
160
        }
×
161

NEW
162
        routeHops := make([]*route.Hop, len(dbHops))
×
NEW
163
        for i, dbHop := range dbHops {
×
NEW
164
                routeHop, err := unmarshalHop(dbHop)
×
NEW
165
                if err != nil {
×
NEW
166
                        return nil, fmt.Errorf("unable to unmarshal "+
×
NEW
167
                                "hop %d: %w", i, err)
×
NEW
168
                }
×
NEW
169
                routeHops[i] = routeHop
×
170
        }
171

NEW
172
        return routeHops, nil
×
173
}
174

175
// unmarshalHop converts a sqlc.PaymentRouteHop to a route.Hop.
NEW
176
func unmarshalHop(dbHop sqlc.PaymentRouteHop) (*route.Hop, error) {
×
NEW
177
        // Parse channel ID which is represented as a string in the sql db and
×
NEW
178
        // uses the integer representation of the channel id.
×
NEW
179
        chanID, err := strconv.ParseUint(dbHop.Scid, 10, 64)
×
NEW
180
        if err != nil {
×
NEW
181
                return nil, fmt.Errorf("invalid channel ID: %w", err)
×
NEW
182
        }
×
183

NEW
184
        var pubKey [33]byte
×
NEW
185
        if len(dbHop.PubKey) != 33 {
×
NEW
186
                return nil, fmt.Errorf("invalid public key length: %d",
×
NEW
187
                        len(dbHop.PubKey))
×
NEW
188
        }
×
NEW
189
        copy(pubKey[:], dbHop.PubKey)
×
NEW
190

×
NEW
191
        hop := &route.Hop{
×
NEW
192
                ChannelID:        chanID,
×
NEW
193
                PubKeyBytes:      pubKey,
×
NEW
194
                OutgoingTimeLock: uint32(dbHop.OutgoingTimeLock),
×
NEW
195
                AmtToForward:     lnwire.MilliSatoshi(dbHop.AmtToForward),
×
NEW
196
                Metadata:         dbHop.MetaData,
×
NEW
197
                LegacyPayload:    dbHop.LegacyPayload,
×
NEW
198
        }
×
NEW
199

×
NEW
200
        // Handle MPP fields if available. This should only be present for
×
NEW
201
        // the last hop of the route in MPP payments.
×
NEW
202
        if len(dbHop.MppPaymentAddr) > 0 {
×
NEW
203
                var paymentAddr [32]byte
×
NEW
204
                copy(paymentAddr[:], dbHop.MppPaymentAddr)
×
NEW
205
                totalMsat := lnwire.MilliSatoshi(0)
×
NEW
206
                if dbHop.MppTotalMsat.Valid {
×
NEW
207
                        totalMsat = lnwire.MilliSatoshi(
×
NEW
208
                                dbHop.MppTotalMsat.Int64,
×
NEW
209
                        )
×
NEW
210
                }
×
NEW
211
                hop.MPP = record.NewMPP(totalMsat, paymentAddr)
×
212
        }
213

214
        // Handle AMP fields if available. This should only be present for
215
        // the last hop of the route in AMP payments.
NEW
216
        if len(dbHop.AmpRootShare) > 0 && len(dbHop.AmpSetID) > 0 {
×
NEW
217
                var rootShare, setID [32]byte
×
NEW
218
                copy(rootShare[:], dbHop.AmpRootShare)
×
NEW
219
                copy(setID[:], dbHop.AmpSetID)
×
NEW
220

×
NEW
221
                childIndex := uint32(0)
×
NEW
222
                if dbHop.AmpChildIndex.Valid {
×
NEW
223
                        childIndex = uint32(dbHop.AmpChildIndex.Int32)
×
NEW
224
                }
×
225

NEW
226
                hop.AMP = record.NewAMP(rootShare, setID, childIndex)
×
227
        }
228

229
        // Handle blinded path fields
NEW
230
        if len(dbHop.EncryptedData) > 0 {
×
NEW
231
                hop.EncryptedData = dbHop.EncryptedData
×
NEW
232
        }
×
233

NEW
234
        if len(dbHop.BlindingPoint) > 0 {
×
NEW
235
                pubKey, err := btcec.ParsePubKey(dbHop.BlindingPoint)
×
NEW
236
                if err != nil {
×
NEW
237
                        return nil, fmt.Errorf("invalid blinding "+
×
NEW
238
                                "point: %w", err)
×
NEW
239
                }
×
NEW
240
                hop.BlindingPoint = pubKey
×
241
        }
242

NEW
243
        if dbHop.BlindedPathTotalAmt.Valid {
×
NEW
244
                hop.TotalAmtMsat = lnwire.MilliSatoshi(
×
NEW
245
                        dbHop.BlindedPathTotalAmt.Int64)
×
NEW
246
        }
×
247

NEW
248
        return hop, nil
×
249
}
250

251
// unmarshalFirstHopCustomRecords converts a slice of
252
// sqlc.PaymentFirstHopCustomRecord to a lnwire.CustomRecords.
253
func unmarshalFirstHopCustomRecords(
254
        dbFirstHopCustomRecords []sqlc.PaymentFirstHopCustomRecord) (
NEW
255
        lnwire.CustomRecords, error) {
×
NEW
256

×
NEW
257
        tlvMap := make(tlv.TypeMap)
×
NEW
258
        for _, dbRecord := range dbFirstHopCustomRecords {
×
NEW
259
                // We need to make a copy to prevent nil/empty value comparison
×
NEW
260
                // issues for empty values.
×
NEW
261
                value := make([]byte, len(dbRecord.Value))
×
NEW
262
                copy(value, dbRecord.Value)
×
NEW
263
                tlvMap[tlv.Type(dbRecord.Key)] = value
×
NEW
264
        }
×
NEW
265
        firstHopCustomRecordsMap, err := lnwire.NewCustomRecords(tlvMap)
×
NEW
266
        if err != nil {
×
NEW
267
                return nil, fmt.Errorf("unable to convert first "+
×
NEW
268
                        "hop custom records to tlv map: %w", err)
×
NEW
269
        }
×
270

NEW
271
        return firstHopCustomRecordsMap, nil
×
272
}
273

274
// unmarshalRouteCustomRecords converts a slice of
275
// sqlc.PaymentHtlcAttemptCustomRecord to a lnwire.CustomRecords.
276
func unmarshalRouteCustomRecords(
277
        dbRouteCustomRecords []sqlc.PaymentHtlcAttemptCustomRecord) (
NEW
278
        lnwire.CustomRecords, error) {
×
NEW
279

×
NEW
280
        tlvMap := make(tlv.TypeMap)
×
NEW
281
        for _, dbRecord := range dbRouteCustomRecords {
×
NEW
282
                // We need to make a copy to prevent nil/empty value comparison
×
NEW
283
                // issues for empty values.
×
NEW
284
                value := make([]byte, len(dbRecord.Value))
×
NEW
285
                copy(value, dbRecord.Value)
×
NEW
286
                tlvMap[tlv.Type(dbRecord.Key)] = value
×
NEW
287
        }
×
NEW
288
        routeCustomRecordsMap, err := lnwire.NewCustomRecords(tlvMap)
×
NEW
289
        if err != nil {
×
NEW
290
                return nil, fmt.Errorf("unable to convert route "+
×
NEW
291
                        "custom records to tlv map: %w", err)
×
NEW
292
        }
×
293

NEW
294
        return routeCustomRecordsMap, nil
×
295
}
296

297
func unmarshalHopCustomRecords(
298
        dbHopCustomRecords []sqlc.PaymentRouteHopCustomRecord) (
NEW
299
        record.CustomSet, error) {
×
NEW
300

×
NEW
301
        tlvMap := make(tlv.TypeMap)
×
NEW
302
        for _, dbRecord := range dbHopCustomRecords {
×
NEW
303
                // We need to make a copy to prevent nil/empty value comparison
×
NEW
304
                // issues for empty values.
×
NEW
305
                value := make([]byte, len(dbRecord.Value))
×
NEW
306
                copy(value, dbRecord.Value)
×
NEW
307
                tlvMap[tlv.Type(dbRecord.Key)] = value
×
NEW
308
        }
×
NEW
309
        hopCustomRecordsMap, err := lnwire.NewCustomRecords(tlvMap)
×
NEW
310
        if err != nil {
×
NEW
311
                return nil, fmt.Errorf("unable to convert hop "+
×
NEW
312
                        "custom records to tlv map: %w", err)
×
NEW
313
        }
×
314

NEW
315
        return record.CustomSet(hopCustomRecordsMap), nil
×
316
}
317

318
// unmarshalPaymentData unmarshals the payment data into the MPPayment object.
319
func unmarshalPaymentData(dbPayment sqlc.Payment,
320
        dbHtlcAttempts []sqlc.PaymentHtlcAttempt, dbHops []sqlc.PaymentRouteHop,
321
        dbRouteCustomRecords []sqlc.PaymentHtlcAttemptCustomRecord,
322
        dbHopCustomRecords []sqlc.PaymentRouteHopCustomRecord,
323
        dbFirstHopCustomRecords []sqlc.PaymentFirstHopCustomRecord) (*MPPayment,
NEW
324
        error) {
×
NEW
325

×
NEW
326
        // Create maps for efficient lookups
×
NEW
327
        hopsByAttemptIndex := make(map[int64][]sqlc.PaymentRouteHop)
×
NEW
328
        for _, hop := range dbHops {
×
NEW
329
                hopsByAttemptIndex[hop.HtlcAttemptIndex] = append(
×
NEW
330
                        hopsByAttemptIndex[hop.HtlcAttemptIndex], hop,
×
NEW
331
                )
×
NEW
332
        }
×
333

334
        // Sort hops by hop_index for each attempt to ensure correct order.
NEW
335
        for attemptIndex, hops := range hopsByAttemptIndex {
×
NEW
336
                sort.Slice(hops, func(i, j int) bool {
×
NEW
337
                        return hops[i].HopIndex < hops[j].HopIndex
×
NEW
338
                })
×
NEW
339
                hopsByAttemptIndex[attemptIndex] = hops
×
340
        }
341

NEW
342
        customRecordsByHopID := make(
×
NEW
343
                map[int64][]sqlc.PaymentRouteHopCustomRecord,
×
NEW
344
        )
×
NEW
345
        for _, record := range dbHopCustomRecords {
×
NEW
346
                customRecordsByHopID[record.HopID] = append(
×
NEW
347
                        customRecordsByHopID[record.HopID], record,
×
NEW
348
                )
×
NEW
349
        }
×
350

351
        // Convert the db htlc attempts to our internal htlc attempts data
352
        // structure.
NEW
353
        var htlcAttempts []HTLCAttempt
×
NEW
354
        for _, dbAttempt := range dbHtlcAttempts {
×
NEW
355
                // Get hops for this attempt from our pre-fetched data
×
NEW
356
                dbHops := hopsByAttemptIndex[dbAttempt.AttemptIndex]
×
NEW
357

×
NEW
358
                attempt, err := unmarshalHtlcAttempt(
×
NEW
359
                        dbAttempt, dbHops, dbRouteCustomRecords,
×
NEW
360
                )
×
NEW
361
                if err != nil {
×
NEW
362
                        return nil, fmt.Errorf("unable to convert htlc "+
×
NEW
363
                                "attempt(id=%d): %w", dbAttempt.AttemptIndex,
×
NEW
364
                                err)
×
NEW
365
                }
×
366

NEW
367
                hops := attempt.Route.Hops
×
NEW
368

×
NEW
369
                // Attach the custom records to the hop which are in a separate
×
NEW
370
                // table.
×
NEW
371
                for i, dbHop := range dbHops {
×
NEW
372
                        // Get custom records for this hop from our pre-fetched
×
NEW
373
                        // data.
×
NEW
374
                        dbHopCustomRecords := customRecordsByHopID[dbHop.ID]
×
NEW
375

×
NEW
376
                        hops[i].CustomRecords, err = unmarshalHopCustomRecords(
×
NEW
377
                                dbHopCustomRecords,
×
NEW
378
                        )
×
NEW
379
                        if err != nil {
×
NEW
380
                                return nil, fmt.Errorf("unable to unmarshal "+
×
NEW
381
                                        "hop custom records: %w", err)
×
NEW
382
                        }
×
383
                }
384

NEW
385
                htlcAttempts = append(htlcAttempts, *attempt)
×
386
        }
387

388
        // Convert first hop custom records
NEW
389
        firstHopCustomRecords, err := unmarshalFirstHopCustomRecords(
×
NEW
390
                dbFirstHopCustomRecords,
×
NEW
391
        )
×
NEW
392
        if err != nil {
×
NEW
393
                return nil, fmt.Errorf("unable to unmarshal first hop "+
×
NEW
394
                        "custom records: %w", err)
×
NEW
395
        }
×
396

397
        // Convert payment hash from bytes to lntypes.Hash.
NEW
398
        var paymentHash lntypes.Hash
×
NEW
399
        copy(paymentHash[:], dbPayment.PaymentHash)
×
NEW
400

×
NEW
401
        // Create PaymentCreationInfo from the payment data.
×
NEW
402
        creationInfo := &PaymentCreationInfo{
×
NEW
403
                PaymentIdentifier: paymentHash,
×
NEW
404
                Value: lnwire.MilliSatoshi(
×
NEW
405
                        dbPayment.AmountMsat,
×
NEW
406
                ),
×
NEW
407
                CreationTime:          dbPayment.CreatedAt.Local(),
×
NEW
408
                PaymentRequest:        dbPayment.PaymentRequest,
×
NEW
409
                FirstHopCustomRecords: firstHopCustomRecords,
×
NEW
410
        }
×
NEW
411

×
NEW
412
        var failureReason *FailureReason
×
NEW
413
        if dbPayment.FailReason.Valid {
×
NEW
414
                reason := FailureReason(dbPayment.FailReason.Int32)
×
NEW
415
                failureReason = &reason
×
NEW
416
        }
×
417

418
        // Create MPPayment in memory object.
NEW
419
        payment := &MPPayment{
×
NEW
420
                SequenceNum:   uint64(dbPayment.ID),
×
NEW
421
                FailureReason: failureReason,
×
NEW
422
                Info:          creationInfo,
×
NEW
423
                HTLCs:         htlcAttempts,
×
NEW
424
        }
×
NEW
425

×
NEW
426
        return payment, nil
×
427
}
428

429
// unmarshalPaymentWithoutHTLCs unmarshals the payment data into the MPPayment
430
// object without HTLCs.
NEW
431
func unmarshalPaymentWithoutHTLCs(dbPayment sqlc.Payment) (*MPPayment, error) {
×
NEW
432
        // Convert payment hash from bytes to lntypes.Hash
×
NEW
433
        var paymentHash lntypes.Hash
×
NEW
434
        copy(paymentHash[:], dbPayment.PaymentHash)
×
NEW
435

×
NEW
436
        // Create PaymentCreationInfo from the payment data
×
NEW
437
        creationInfo := &PaymentCreationInfo{
×
NEW
438
                PaymentIdentifier: paymentHash,
×
NEW
439
                Value:             lnwire.MilliSatoshi(dbPayment.AmountMsat),
×
NEW
440
                CreationTime:      dbPayment.CreatedAt.Local(),
×
NEW
441
                PaymentRequest:    dbPayment.PaymentRequest,
×
NEW
442
                // No first hop custom records for payments without HTLCs
×
NEW
443
        }
×
NEW
444

×
NEW
445
        var failureReason *FailureReason
×
NEW
446
        if dbPayment.FailReason.Valid {
×
NEW
447
                reason := FailureReason(dbPayment.FailReason.Int32)
×
NEW
448
                failureReason = &reason
×
NEW
449
        }
×
450

451
        // Create MPPayment object
NEW
452
        payment := &MPPayment{
×
NEW
453
                SequenceNum:   uint64(dbPayment.ID),
×
NEW
454
                FailureReason: failureReason,
×
NEW
455
                Info:          creationInfo,
×
NEW
456
                HTLCs:         []HTLCAttempt{},
×
NEW
457
        }
×
NEW
458

×
NEW
459
        return payment, nil
×
460
}
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

© 2025 Coveralls, Inc