mirror of
https://github.com/lightninglabs/loop.git
synced 2026-08-15 12:50:28 +02:00
Merge pull request #617 from sputn1ck/fix_faulty_timestamps
loopdb: fix faulty timestamps on startup
This commit is contained in:
commit
824f543c0f
6 changed files with 330 additions and 32 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
201
loopdb/sqlite.go
201
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,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…
Add table
Add a link
Reference in a new issue