mirror of
https://github.com/lightninglabs/loop.git
synced 2026-08-13 12:33:03 +02:00
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.
This commit is contained in:
parent
92c1f8ce62
commit
f25b5e9626
5 changed files with 276 additions and 12 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
183
loopdb/sqlite.go
183
loopdb/sqlite.go
|
|
@ -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…
Add table
Add a link
Reference in a new issue