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.
pull/617/head
sputn1ck 9 months ago
parent 92c1f8ce62
commit f25b5e9626
No known key found for this signature in database
GPG Key ID: 671103D881A5F0E4

@ -1,6 +1,7 @@
package loopdb
import (
"context"
"database/sql"
"fmt"
"testing"
@ -104,13 +105,25 @@ func NewPostgresStore(cfg *PostgresConfig,
queries := sqlc.New(rawDb)
baseDB := &BaseDB{
DB: rawDb,
Queries: queries,
network: network,
}
// Fix faulty timestamps in the database.
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
err = baseDB.FixFaultyTimestamps(ctx, parsePostgresTimeStamp)
if err != nil {
log.Errorf("Failed to fix faulty timestamps: %v", err)
return nil, err
}
return &PostgresStore{
cfg: cfg,
BaseDB: &BaseDB{
DB: rawDb,
Queries: queries,
network: network,
},
cfg: cfg,
BaseDB: baseDB,
}, nil
}

@ -318,6 +318,78 @@ func TestSqliteTypeConversion(t *testing.T) {
}
// TestIssue615 tests that on faulty timestamps, the database will be fixed.
// Reference: https://github.com/lightninglabs/lightning-terminal/issues/615
func TestIssue615(t *testing.T) {
ctxb := context.Background()
// Create a new sqlite store for testing.
sqlDB := NewTestDB(t)
// Create a faulty loopout swap.
destAddr := test.GetDestAddr(t, 0)
faultyTime, err := parseSqliteTimeStamp("55563-06-27 02:09:24 +0000 UTC")
require.NoError(t, err)
unrestrictedSwap := LoopOutContract{
SwapContract: SwapContract{
AmountRequested: 100,
Preimage: testPreimage,
CltvExpiry: 144,
HtlcKeys: HtlcKeys{
SenderScriptKey: senderKey,
ReceiverScriptKey: receiverKey,
SenderInternalPubKey: senderInternalKey,
ReceiverInternalPubKey: receiverInternalKey,
ClientScriptKeyLocator: keychain.KeyLocator{
Family: 1,
Index: 2,
},
},
MaxMinerFee: 10,
MaxSwapFee: 20,
InitiationHeight: 99,
InitiationTime: time.Now(),
ProtocolVersion: ProtocolVersionMuSig2,
},
MaxPrepayRoutingFee: 40,
PrepayInvoice: "prepayinvoice",
DestAddr: destAddr,
SwapInvoice: "swapinvoice",
MaxSwapRoutingFee: 30,
SweepConfTarget: 2,
HtlcConfirmations: 2,
SwapPublicationDeadline: faultyTime,
}
err = sqlDB.CreateLoopOut(ctxb, testPreimage.Hash(), &unrestrictedSwap)
require.NoError(t, err)
// This should fail because of the faulty timestamp.
_, err = sqlDB.GetLoopOutSwaps(ctxb)
// If we're using sqlite, we expect an error.
if testDBType == "sqlite" {
require.Error(t, err)
} else {
require.NoError(t, err)
}
parseFunc := parseSqliteTimeStamp
if testDBType == "postgres" {
parseFunc = parsePostgresTimeStamp
}
// Fix the faulty timestamp.
err = sqlDB.FixFaultyTimestamps(ctxb, parseFunc)
require.NoError(t, err)
_, err = sqlDB.GetLoopOutSwaps(ctxb)
require.NoError(t, err)
}
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
func randomString(length int) string {

@ -6,7 +6,10 @@ import (
"fmt"
"net/url"
"path/filepath"
"strconv"
"strings"
"testing"
"time"
"github.com/btcsuite/btcd/chaincfg"
sqlite_migrate "github.com/golang-migrate/migrate/v4/database/sqlite"
@ -109,13 +112,25 @@ func NewSqliteStore(cfg *SqliteConfig, network *chaincfg.Params) (*SqliteSwapSto
queries := sqlc.New(db)
baseDB := &BaseDB{
DB: db,
Queries: queries,
network: network,
}
// Fix faulty timestamps in the database.
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
err = baseDB.FixFaultyTimestamps(ctx, parseSqliteTimeStamp)
if err != nil {
log.Errorf("Failed to fix faulty timestamps: %v", err)
return nil, err
}
return &SqliteSwapStore{
cfg: cfg,
BaseDB: &BaseDB{
DB: db,
Queries: queries,
network: network,
},
cfg: cfg,
BaseDB: baseDB,
}, nil
}
@ -127,6 +142,7 @@ func NewTestSqliteDB(t *testing.T) *SqliteSwapStore {
t.Logf("Creating new SQLite DB for testing")
dbFileName := filepath.Join(t.TempDir(), "tmp.db")
sqlDB, err := NewSqliteStore(&SqliteConfig{
DatabaseFileName: dbFileName,
SkipMigrations: false,
@ -191,6 +207,79 @@ func (db *BaseDB) ExecTx(ctx context.Context, txOptions TxOptions,
return nil
}
// FixFaultyTimestamps fixes faulty timestamps in the database, caused
// by using milliseconds instead of seconds as the publication deadline.
func (b *BaseDB) FixFaultyTimestamps(ctx context.Context,
parseTimeFunc func(string) (time.Time, error)) error {
// Manually fetch all the loop out swaps.
rows, err := b.DB.QueryContext(
ctx, "SELECT swap_hash, publication_deadline FROM loopout_swaps",
)
if err != nil {
return err
}
// Parse the rows into a struct. We need to do this manually because
// the sqlite driver will fail on faulty timestamps.
type LoopOutRow struct {
Hash []byte `json:"swap_hash"`
PublicationDeadline string `json:"publication_deadline"`
}
var loopOutSwaps []LoopOutRow
for rows.Next() {
var swap LoopOutRow
err := rows.Scan(
&swap.Hash, &swap.PublicationDeadline,
)
if err != nil {
return err
}
loopOutSwaps = append(loopOutSwaps, swap)
}
tx, err := b.BeginTx(ctx, &SqliteTxOptions{})
if err != nil {
return err
}
defer tx.Rollback() //nolint: errcheck
for _, swap := range loopOutSwaps {
faultyTime, err := parseTimeFunc(swap.PublicationDeadline)
if err != nil {
return err
}
// Skip if the time is not faulty.
if !isMilisecondsTime(faultyTime.Unix()) {
continue
}
// Update the faulty time to a valid time.
secs := faultyTime.Unix() / 1000
correctTime := time.Unix(secs, 0)
_, err = tx.ExecContext(
ctx, `
UPDATE
loopout_swaps
SET
publication_deadline = $1
WHERE
swap_hash = $2;
`,
correctTime, swap.Hash,
)
if err != nil {
return err
}
}
return tx.Commit()
}
// TxOptions represents a set of options one can use to control what type of
// database transaction is created. Transaction can wither be read or write.
type TxOptions interface {
@ -219,3 +308,85 @@ func NewSqlReadOpts() *SqliteTxOptions {
func (r *SqliteTxOptions) ReadOnly() bool {
return r.readOnly
}
// parseSqliteTimeStamp parses a timestamp string in the format of
// "YYYY-MM-DD HH:MM:SS +0000 UTC" and returns a time.Time value.
// NOTE: we can't use time.Parse() because it doesn't support having years
// with more than 4 digits.
func parseSqliteTimeStamp(dateTimeStr string) (time.Time, error) {
// Split the date and time parts.
parts := strings.Fields(strings.TrimSpace(dateTimeStr))
datePart, timePart := parts[0], parts[1]
return parseTimeParts(datePart, timePart)
}
// parseSqliteTimeStamp parses a timestamp string in the format of
// "YYYY-MM-DDTHH:MM:SSZ" and returns a time.Time value.
// NOTE: we can't use time.Parse() because it doesn't support having years
// with more than 4 digits.
func parsePostgresTimeStamp(dateTimeStr string) (time.Time, error) {
// Split the date and time parts.
parts := strings.Split(dateTimeStr, "T")
datePart, timePart := parts[0], strings.TrimSuffix(parts[1], "Z")
return parseTimeParts(datePart, timePart)
}
// parseTimeParts takes a datePart string in the format of "YYYY-MM-DD" and
// a timePart string in the format of "HH:MM:SS" and returns a time.Time value.
func parseTimeParts(datePart, timePart string) (time.Time, error) {
// Parse the date.
dateParts := strings.Split(datePart, "-")
year, err := strconv.Atoi(dateParts[0])
if err != nil {
return time.Time{}, err
}
month, err := strconv.Atoi(dateParts[1])
if err != nil {
return time.Time{}, err
}
day, err := strconv.Atoi(dateParts[2])
if err != nil {
return time.Time{}, err
}
// Parse the time.
timeParts := strings.Split(timePart, ":")
hour, err := strconv.Atoi(timeParts[0])
if err != nil {
return time.Time{}, err
}
minute, err := strconv.Atoi(timeParts[1])
if err != nil {
return time.Time{}, err
}
second, err := strconv.Atoi(timeParts[2])
if err != nil {
return time.Time{}, err
}
// Construct a time.Time value.
return time.Date(
year, time.Month(month), day, hour, minute, second, 0, time.UTC,
), nil
}
// isMilisecondsTime returns true if the unix timestamp is likely in
// milliseconds.
func isMilisecondsTime(unixTimestamp int64) bool {
length := len(fmt.Sprintf("%d", unixTimestamp))
if length >= 13 {
// Likely a millisecond timestamp
return true
} else {
// Likely a second timestamp
return false
}
}

@ -7,6 +7,10 @@ import (
"testing"
)
var (
testDBType = "postgres"
)
// NewTestDB is a helper function that creates a Postgres database for testing.
func NewTestDB(t *testing.T) *PostgresStore {
return NewTestPostgresDB(t)

@ -7,6 +7,10 @@ import (
"testing"
)
var (
testDBType = "sqlite"
)
// NewTestDB is a helper function that creates an SQLite database for testing.
func NewTestDB(t *testing.T) *SqliteSwapStore {
return NewTestSqliteDB(t)

Loading…
Cancel
Save