Merge pull request #617 from sputn1ck/fix_faulty_timestamps

loopdb: fix faulty timestamps on startup
pull/618/head
Konstantin Nick 9 months ago committed by GitHub
commit 824f543c0f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -156,21 +156,22 @@ func (s *swapClientServer) LoopOut(ctx context.Context,
return nil, err
}
// Infer if the publication deadline is set in milliseconds.
publicationDeadline := getPublicationDeadline(in.SwapPublicationDeadline)
req := &loop.OutRequest{
Amount: btcutil.Amount(in.Amt),
DestAddr: sweepAddr,
MaxMinerFee: btcutil.Amount(in.MaxMinerFee),
MaxPrepayAmount: btcutil.Amount(in.MaxPrepayAmt),
MaxPrepayRoutingFee: btcutil.Amount(in.MaxPrepayRoutingFee),
MaxSwapRoutingFee: btcutil.Amount(in.MaxSwapRoutingFee),
MaxSwapFee: btcutil.Amount(in.MaxSwapFee),
SweepConfTarget: sweepConfTarget,
HtlcConfirmations: in.HtlcConfirmations,
SwapPublicationDeadline: time.Unix(
int64(in.SwapPublicationDeadline), 0,
),
Label: in.Label,
Initiator: in.Initiator,
Amount: btcutil.Amount(in.Amt),
DestAddr: sweepAddr,
MaxMinerFee: btcutil.Amount(in.MaxMinerFee),
MaxPrepayAmount: btcutil.Amount(in.MaxPrepayAmt),
MaxPrepayRoutingFee: btcutil.Amount(in.MaxPrepayRoutingFee),
MaxSwapRoutingFee: btcutil.Amount(in.MaxSwapRoutingFee),
MaxSwapFee: btcutil.Amount(in.MaxSwapFee),
SweepConfTarget: sweepConfTarget,
HtlcConfirmations: in.HtlcConfirmations,
SwapPublicationDeadline: publicationDeadline,
Label: in.Label,
Initiator: in.Initiator,
}
switch {
@ -538,13 +539,14 @@ func (s *swapClientServer) LoopOutQuote(ctx context.Context,
if err != nil {
return nil, err
}
publicactionDeadline := getPublicationDeadline(req.SwapPublicationDeadline)
quote, err := s.impl.LoopOutQuote(ctx, &loop.LoopOutQuoteRequest{
Amount: btcutil.Amount(req.Amt),
SweepConfTarget: confTarget,
SwapPublicationDeadline: time.Unix(
int64(req.SwapPublicationDeadline), 0,
),
Initiator: defaultLoopdInitiator,
Amount: btcutil.Amount(req.Amt),
SweepConfTarget: confTarget,
SwapPublicationDeadline: publicactionDeadline,
Initiator: defaultLoopdInitiator,
})
if err != nil {
return nil, err
@ -1249,3 +1251,19 @@ func hasBandwidth(channels []lndclient.ChannelInfo, amt btcutil.Amount,
return false, 0
}
// getPublicationDeadline returns the publication deadline for a swap given the
// unix timestamp. If the timestamp is believed to be in milliseconds, then it
// is converted to seconds.
func getPublicationDeadline(unixTimestamp uint64) time.Time {
length := len(fmt.Sprintf("%d", unixTimestamp))
if length >= 13 {
// Likely a millisecond timestamp
secs := unixTimestamp / 1000
nsecs := (unixTimestamp % 1000) * 1e6
return time.Unix(int64(secs), int64(nsecs))
} else {
// Likely a second timestamp
return time.Unix(int64(unixTimestamp), 0)
}
}

@ -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,76 @@ 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,103 @@ 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))
if len(parts) <= 2 {
return time.Time{}, fmt.Errorf("invalid timestamp format: %v",
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")
if len(parts) != 2 {
return time.Time{}, fmt.Errorf("invalid timestamp format: %v",
dateTimeStr)
}
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, "-")
if len(dateParts) != 3 {
return time.Time{}, fmt.Errorf("invalid date format: %v",
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, ":")
if len(timeParts) != 3 {
return time.Time{}, fmt.Errorf("invalid time format: %v",
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