Skip to content

Commit f25b5e9

Browse files
committed
loopdb: fix faulty timestamps on startup
This commit fixes faulty timestamps caused by using unix milliseconds as unix seconds on startup. This commit also adds a test for the lightning-terminal issue that first reported the bug.
1 parent 92c1f8c commit f25b5e9

File tree

5 files changed

+276
-12
lines changed

5 files changed

+276
-12
lines changed

loopdb/postgres.go

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package loopdb
22

33
import (
4+
"context"
45
"database/sql"
56
"fmt"
67
"testing"
@@ -104,13 +105,25 @@ func NewPostgresStore(cfg *PostgresConfig,
104105

105106
queries := sqlc.New(rawDb)
106107

108+
baseDB := &BaseDB{
109+
DB: rawDb,
110+
Queries: queries,
111+
network: network,
112+
}
113+
114+
// Fix faulty timestamps in the database.
115+
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
116+
defer cancel()
117+
118+
err = baseDB.FixFaultyTimestamps(ctx, parsePostgresTimeStamp)
119+
if err != nil {
120+
log.Errorf("Failed to fix faulty timestamps: %v", err)
121+
return nil, err
122+
}
123+
107124
return &PostgresStore{
108-
cfg: cfg,
109-
BaseDB: &BaseDB{
110-
DB: rawDb,
111-
Queries: queries,
112-
network: network,
113-
},
125+
cfg: cfg,
126+
BaseDB: baseDB,
114127
}, nil
115128
}
116129

loopdb/sql_test.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,78 @@ func TestSqliteTypeConversion(t *testing.T) {
318318

319319
}
320320

321+
// TestIssue615 tests that on faulty timestamps, the database will be fixed.
322+
// Reference: https://github.com/lightninglabs/lightning-terminal/issues/615
323+
func TestIssue615(t *testing.T) {
324+
ctxb := context.Background()
325+
326+
// Create a new sqlite store for testing.
327+
sqlDB := NewTestDB(t)
328+
329+
// Create a faulty loopout swap.
330+
destAddr := test.GetDestAddr(t, 0)
331+
faultyTime, err := parseSqliteTimeStamp("55563-06-27 02:09:24 +0000 UTC")
332+
require.NoError(t, err)
333+
334+
unrestrictedSwap := LoopOutContract{
335+
SwapContract: SwapContract{
336+
AmountRequested: 100,
337+
Preimage: testPreimage,
338+
CltvExpiry: 144,
339+
HtlcKeys: HtlcKeys{
340+
SenderScriptKey: senderKey,
341+
ReceiverScriptKey: receiverKey,
342+
SenderInternalPubKey: senderInternalKey,
343+
ReceiverInternalPubKey: receiverInternalKey,
344+
ClientScriptKeyLocator: keychain.KeyLocator{
345+
Family: 1,
346+
Index: 2,
347+
},
348+
},
349+
MaxMinerFee: 10,
350+
MaxSwapFee: 20,
351+
352+
InitiationHeight: 99,
353+
354+
InitiationTime: time.Now(),
355+
ProtocolVersion: ProtocolVersionMuSig2,
356+
},
357+
MaxPrepayRoutingFee: 40,
358+
PrepayInvoice: "prepayinvoice",
359+
DestAddr: destAddr,
360+
SwapInvoice: "swapinvoice",
361+
MaxSwapRoutingFee: 30,
362+
SweepConfTarget: 2,
363+
HtlcConfirmations: 2,
364+
SwapPublicationDeadline: faultyTime,
365+
}
366+
367+
err = sqlDB.CreateLoopOut(ctxb, testPreimage.Hash(), &unrestrictedSwap)
368+
require.NoError(t, err)
369+
370+
// This should fail because of the faulty timestamp.
371+
_, err = sqlDB.GetLoopOutSwaps(ctxb)
372+
373+
// If we're using sqlite, we expect an error.
374+
if testDBType == "sqlite" {
375+
require.Error(t, err)
376+
} else {
377+
require.NoError(t, err)
378+
}
379+
380+
parseFunc := parseSqliteTimeStamp
381+
if testDBType == "postgres" {
382+
parseFunc = parsePostgresTimeStamp
383+
}
384+
385+
// Fix the faulty timestamp.
386+
err = sqlDB.FixFaultyTimestamps(ctxb, parseFunc)
387+
require.NoError(t, err)
388+
389+
_, err = sqlDB.GetLoopOutSwaps(ctxb)
390+
require.NoError(t, err)
391+
}
392+
321393
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
322394

323395
func randomString(length int) string {

loopdb/sqlite.go

Lines changed: 177 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@ import (
66
"fmt"
77
"net/url"
88
"path/filepath"
9+
"strconv"
10+
"strings"
911
"testing"
12+
"time"
1013

1114
"github.com/btcsuite/btcd/chaincfg"
1215
sqlite_migrate "github.com/golang-migrate/migrate/v4/database/sqlite"
@@ -109,13 +112,25 @@ func NewSqliteStore(cfg *SqliteConfig, network *chaincfg.Params) (*SqliteSwapSto
109112

110113
queries := sqlc.New(db)
111114

115+
baseDB := &BaseDB{
116+
DB: db,
117+
Queries: queries,
118+
network: network,
119+
}
120+
121+
// Fix faulty timestamps in the database.
122+
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
123+
defer cancel()
124+
125+
err = baseDB.FixFaultyTimestamps(ctx, parseSqliteTimeStamp)
126+
if err != nil {
127+
log.Errorf("Failed to fix faulty timestamps: %v", err)
128+
return nil, err
129+
}
130+
112131
return &SqliteSwapStore{
113-
cfg: cfg,
114-
BaseDB: &BaseDB{
115-
DB: db,
116-
Queries: queries,
117-
network: network,
118-
},
132+
cfg: cfg,
133+
BaseDB: baseDB,
119134
}, nil
120135
}
121136

@@ -127,6 +142,7 @@ func NewTestSqliteDB(t *testing.T) *SqliteSwapStore {
127142
t.Logf("Creating new SQLite DB for testing")
128143

129144
dbFileName := filepath.Join(t.TempDir(), "tmp.db")
145+
130146
sqlDB, err := NewSqliteStore(&SqliteConfig{
131147
DatabaseFileName: dbFileName,
132148
SkipMigrations: false,
@@ -191,6 +207,79 @@ func (db *BaseDB) ExecTx(ctx context.Context, txOptions TxOptions,
191207
return nil
192208
}
193209

210+
// FixFaultyTimestamps fixes faulty timestamps in the database, caused
211+
// by using milliseconds instead of seconds as the publication deadline.
212+
func (b *BaseDB) FixFaultyTimestamps(ctx context.Context,
213+
parseTimeFunc func(string) (time.Time, error)) error {
214+
215+
// Manually fetch all the loop out swaps.
216+
rows, err := b.DB.QueryContext(
217+
ctx, "SELECT swap_hash, publication_deadline FROM loopout_swaps",
218+
)
219+
if err != nil {
220+
return err
221+
}
222+
223+
// Parse the rows into a struct. We need to do this manually because
224+
// the sqlite driver will fail on faulty timestamps.
225+
type LoopOutRow struct {
226+
Hash []byte `json:"swap_hash"`
227+
PublicationDeadline string `json:"publication_deadline"`
228+
}
229+
230+
var loopOutSwaps []LoopOutRow
231+
232+
for rows.Next() {
233+
var swap LoopOutRow
234+
err := rows.Scan(
235+
&swap.Hash, &swap.PublicationDeadline,
236+
)
237+
if err != nil {
238+
return err
239+
}
240+
241+
loopOutSwaps = append(loopOutSwaps, swap)
242+
}
243+
244+
tx, err := b.BeginTx(ctx, &SqliteTxOptions{})
245+
if err != nil {
246+
return err
247+
}
248+
defer tx.Rollback() //nolint: errcheck
249+
250+
for _, swap := range loopOutSwaps {
251+
faultyTime, err := parseTimeFunc(swap.PublicationDeadline)
252+
if err != nil {
253+
return err
254+
}
255+
256+
// Skip if the time is not faulty.
257+
if !isMilisecondsTime(faultyTime.Unix()) {
258+
continue
259+
}
260+
261+
// Update the faulty time to a valid time.
262+
secs := faultyTime.Unix() / 1000
263+
correctTime := time.Unix(secs, 0)
264+
_, err = tx.ExecContext(
265+
ctx, `
266+
UPDATE
267+
loopout_swaps
268+
SET
269+
publication_deadline = $1
270+
WHERE
271+
swap_hash = $2;
272+
`,
273+
correctTime, swap.Hash,
274+
)
275+
if err != nil {
276+
return err
277+
}
278+
}
279+
280+
return tx.Commit()
281+
}
282+
194283
// TxOptions represents a set of options one can use to control what type of
195284
// database transaction is created. Transaction can wither be read or write.
196285
type TxOptions interface {
@@ -219,3 +308,85 @@ func NewSqlReadOpts() *SqliteTxOptions {
219308
func (r *SqliteTxOptions) ReadOnly() bool {
220309
return r.readOnly
221310
}
311+
312+
// parseSqliteTimeStamp parses a timestamp string in the format of
313+
// "YYYY-MM-DD HH:MM:SS +0000 UTC" and returns a time.Time value.
314+
// NOTE: we can't use time.Parse() because it doesn't support having years
315+
// with more than 4 digits.
316+
func parseSqliteTimeStamp(dateTimeStr string) (time.Time, error) {
317+
// Split the date and time parts.
318+
parts := strings.Fields(strings.TrimSpace(dateTimeStr))
319+
datePart, timePart := parts[0], parts[1]
320+
321+
return parseTimeParts(datePart, timePart)
322+
}
323+
324+
// parseSqliteTimeStamp parses a timestamp string in the format of
325+
// "YYYY-MM-DDTHH:MM:SSZ" and returns a time.Time value.
326+
// NOTE: we can't use time.Parse() because it doesn't support having years
327+
// with more than 4 digits.
328+
func parsePostgresTimeStamp(dateTimeStr string) (time.Time, error) {
329+
// Split the date and time parts.
330+
parts := strings.Split(dateTimeStr, "T")
331+
datePart, timePart := parts[0], strings.TrimSuffix(parts[1], "Z")
332+
333+
return parseTimeParts(datePart, timePart)
334+
}
335+
336+
// parseTimeParts takes a datePart string in the format of "YYYY-MM-DD" and
337+
// a timePart string in the format of "HH:MM:SS" and returns a time.Time value.
338+
func parseTimeParts(datePart, timePart string) (time.Time, error) {
339+
// Parse the date.
340+
dateParts := strings.Split(datePart, "-")
341+
342+
year, err := strconv.Atoi(dateParts[0])
343+
if err != nil {
344+
return time.Time{}, err
345+
}
346+
347+
month, err := strconv.Atoi(dateParts[1])
348+
if err != nil {
349+
return time.Time{}, err
350+
}
351+
352+
day, err := strconv.Atoi(dateParts[2])
353+
if err != nil {
354+
return time.Time{}, err
355+
}
356+
357+
// Parse the time.
358+
timeParts := strings.Split(timePart, ":")
359+
360+
hour, err := strconv.Atoi(timeParts[0])
361+
if err != nil {
362+
return time.Time{}, err
363+
}
364+
365+
minute, err := strconv.Atoi(timeParts[1])
366+
if err != nil {
367+
return time.Time{}, err
368+
}
369+
370+
second, err := strconv.Atoi(timeParts[2])
371+
if err != nil {
372+
return time.Time{}, err
373+
}
374+
375+
// Construct a time.Time value.
376+
return time.Date(
377+
year, time.Month(month), day, hour, minute, second, 0, time.UTC,
378+
), nil
379+
}
380+
381+
// isMilisecondsTime returns true if the unix timestamp is likely in
382+
// milliseconds.
383+
func isMilisecondsTime(unixTimestamp int64) bool {
384+
length := len(fmt.Sprintf("%d", unixTimestamp))
385+
if length >= 13 {
386+
// Likely a millisecond timestamp
387+
return true
388+
} else {
389+
// Likely a second timestamp
390+
return false
391+
}
392+
}

loopdb/test_postgres.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ import (
77
"testing"
88
)
99

10+
var (
11+
testDBType = "postgres"
12+
)
13+
1014
// NewTestDB is a helper function that creates a Postgres database for testing.
1115
func NewTestDB(t *testing.T) *PostgresStore {
1216
return NewTestPostgresDB(t)

loopdb/test_sqlite.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ import (
77
"testing"
88
)
99

10+
var (
11+
testDBType = "sqlite"
12+
)
13+
1014
// NewTestDB is a helper function that creates an SQLite database for testing.
1115
func NewTestDB(t *testing.T) *SqliteSwapStore {
1216
return NewTestSqliteDB(t)

0 commit comments

Comments
 (0)