Merge pull request #764 from bhandras/costs-cleanup-migration

loop: add migration to fix stored loop out costs
This commit is contained in:
András Bánki-Horváth 2024-06-03 16:04:30 +02:00 committed by GitHub
commit 55241ffe04
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 805 additions and 8 deletions

View file

@ -65,6 +65,18 @@ type SwapStore interface {
// it's decoding using the proto package's `Unmarshal` method.
FetchLiquidityParams(ctx context.Context) ([]byte, error)
// BatchUpdateLoopOutSwapCosts updates the swap costs for a batch of
// loop out swaps.
BatchUpdateLoopOutSwapCosts(ctx context.Context,
swaps map[lntypes.Hash]SwapCost) error
// HasMigration returns true if the migration with the given ID has
// been done.
HasMigration(ctx context.Context, migrationID string) (bool, error)
// SetMigration marks the migration with the given ID as done.
SetMigration(ctx context.Context, migrationID string) error
// Close closes the underlying database.
Close() error
}

View file

@ -407,6 +407,61 @@ func (s *BaseDB) BatchInsertUpdate(ctx context.Context,
})
}
// BatchUpdateLoopOutSwapCosts updates the swap costs for a batch of loop out
// swaps.
func (b *BaseDB) BatchUpdateLoopOutSwapCosts(ctx context.Context,
costs map[lntypes.Hash]SwapCost) error {
writeOpts := &SqliteTxOptions{}
return b.ExecTx(ctx, writeOpts, func(tx *sqlc.Queries) error {
for swapHash, cost := range costs {
lastUpdateID, err := tx.GetLastUpdateID(
ctx, swapHash[:],
)
if err != nil {
return err
}
err = tx.OverrideSwapCosts(
ctx, sqlc.OverrideSwapCostsParams{
ID: lastUpdateID,
ServerCost: int64(cost.Server),
OnchainCost: int64(cost.Onchain),
OffchainCost: int64(cost.Offchain),
},
)
if err != nil {
return err
}
}
return nil
})
}
// HasMigration returns true if the migration with the given ID has been done.
func (b *BaseDB) HasMigration(ctx context.Context, migrationID string) (
bool, error) {
migration, err := b.GetMigration(ctx, migrationID)
if err != nil && !errors.Is(err, sql.ErrNoRows) {
return false, err
}
return migration.MigrationTs.Valid, nil
}
// SetMigration marks the migration with the given ID as done.
func (b *BaseDB) SetMigration(ctx context.Context, migrationID string) error {
return b.InsertMigration(ctx, sqlc.InsertMigrationParams{
MigrationID: migrationID,
MigrationTs: sql.NullTime{
Time: time.Now().UTC(),
Valid: true,
},
})
}
// loopToInsertArgs converts a SwapContract struct to the arguments needed to
// insert it into the database.
func loopToInsertArgs(hash lntypes.Hash,

View file

@ -13,6 +13,7 @@ import (
"github.com/lightninglabs/loop/loopdb/sqlc"
"github.com/lightninglabs/loop/test"
"github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/routing/route"
"github.com/stretchr/testify/require"
)
@ -396,6 +397,140 @@ func TestIssue615(t *testing.T) {
require.NoError(t, err)
}
// TestBatchUpdateCost tests that we can batch update the cost of multiple swaps
// at once.
func TestBatchUpdateCost(t *testing.T) {
// Create a new sqlite store for testing.
store := NewTestDB(t)
destAddr := test.GetDestAddr(t, 0)
initiationTime := time.Date(2018, 11, 1, 0, 0, 0, 0, time.UTC)
testContract := LoopOutContract{
SwapContract: SwapContract{
AmountRequested: 100,
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: initiationTime,
ProtocolVersion: ProtocolVersionMuSig2,
},
MaxPrepayRoutingFee: 40,
PrepayInvoice: "prepayinvoice",
DestAddr: destAddr,
SwapInvoice: "swapinvoice",
MaxSwapRoutingFee: 30,
SweepConfTarget: 2,
HtlcConfirmations: 2,
SwapPublicationDeadline: initiationTime,
PaymentTimeout: time.Second * 11,
}
makeSwap := func(preimage lntypes.Preimage) *LoopOutContract {
contract := testContract
contract.Preimage = preimage
return &contract
}
// Next, we'll add two swaps to the database.
preimage1 := testPreimage
preimage2 := lntypes.Preimage{4, 4, 4}
ctxb := context.Background()
swap1 := makeSwap(preimage1)
swap2 := makeSwap(preimage2)
hash1 := swap1.Preimage.Hash()
err := store.CreateLoopOut(ctxb, hash1, swap1)
require.NoError(t, err)
hash2 := swap2.Preimage.Hash()
err = store.CreateLoopOut(ctxb, hash2, swap2)
require.NoError(t, err)
// Add an update to both swaps containing the cost.
err = store.UpdateLoopOut(
ctxb, hash1, testTime,
SwapStateData{
State: StateSuccess,
Cost: SwapCost{
Server: 1,
Onchain: 2,
Offchain: 3,
},
},
)
require.NoError(t, err)
err = store.UpdateLoopOut(
ctxb, hash2, testTime,
SwapStateData{
State: StateSuccess,
Cost: SwapCost{
Server: 4,
Onchain: 5,
Offchain: 6,
},
},
)
require.NoError(t, err)
updateMap := map[lntypes.Hash]SwapCost{
hash1: {
Server: 2,
Onchain: 3,
Offchain: 4,
},
hash2: {
Server: 6,
Onchain: 7,
Offchain: 8,
},
}
require.NoError(t, store.BatchUpdateLoopOutSwapCosts(ctxb, updateMap))
swaps, err := store.FetchLoopOutSwaps(ctxb)
require.NoError(t, err)
require.Len(t, swaps, 2)
swapsMap := make(map[lntypes.Hash]*LoopOut)
swapsMap[swaps[0].Hash] = swaps[0]
swapsMap[swaps[1].Hash] = swaps[1]
require.Equal(t, updateMap[hash1], swapsMap[hash1].State().Cost)
require.Equal(t, updateMap[hash2], swapsMap[hash2].State().Cost)
}
// TestMigrationTracker tests the migration tracker functionality.
func TestMigrationTracker(t *testing.T) {
ctxb := context.Background()
// Create a new sqlite store for testing.
sqlDB := NewTestDB(t)
hasMigration, err := sqlDB.HasMigration(ctxb, "test")
require.NoError(t, err)
require.False(t, hasMigration)
require.NoError(t, sqlDB.SetMigration(ctxb, "test"))
hasMigration, err = sqlDB.HasMigration(ctxb, "test")
require.NoError(t, err)
require.True(t, hasMigration)
}
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
func randomString(length int) string {

View file

@ -0,0 +1,45 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.25.0
// source: migration_tracker.sql
package sqlc
import (
"context"
"database/sql"
)
const getMigration = `-- name: GetMigration :one
SELECT
migration_id,
migration_ts
FROM
migration_tracker
WHERE
migration_id = $1
`
func (q *Queries) GetMigration(ctx context.Context, migrationID string) (MigrationTracker, error) {
row := q.db.QueryRowContext(ctx, getMigration, migrationID)
var i MigrationTracker
err := row.Scan(&i.MigrationID, &i.MigrationTs)
return i, err
}
const insertMigration = `-- name: InsertMigration :exec
INSERT INTO migration_tracker (
migration_id,
migration_ts
) VALUES ($1, $2)
`
type InsertMigrationParams struct {
MigrationID string
MigrationTs sql.NullTime
}
func (q *Queries) InsertMigration(ctx context.Context, arg InsertMigrationParams) error {
_, err := q.db.ExecContext(ctx, insertMigration, arg.MigrationID, arg.MigrationTs)
return err
}

View file

@ -0,0 +1,2 @@
DROP TABLE migration_tracker;

View file

@ -0,0 +1,9 @@
CREATE TABLE migration_tracker (
-- migration_id is the id of the migration.
migration_id TEXT NOT NULL,
-- migration_ts is the timestamp at which the migration was run.
migration_ts TIMESTAMP,
PRIMARY KEY (migration_id)
);

View file

@ -67,6 +67,11 @@ type LoopoutSwap struct {
PaymentTimeout int32
}
type MigrationTracker struct {
MigrationID string
MigrationTs sql.NullTime
}
type Reservation struct {
ID int32
ReservationID []byte

View file

@ -18,10 +18,12 @@ type Querier interface {
GetInstantOutSwap(ctx context.Context, swapHash []byte) (GetInstantOutSwapRow, error)
GetInstantOutSwapUpdates(ctx context.Context, swapHash []byte) ([]InstantoutUpdate, error)
GetInstantOutSwaps(ctx context.Context) ([]GetInstantOutSwapsRow, error)
GetLastUpdateID(ctx context.Context, swapHash []byte) (int32, error)
GetLoopInSwap(ctx context.Context, swapHash []byte) (GetLoopInSwapRow, error)
GetLoopInSwaps(ctx context.Context) ([]GetLoopInSwapsRow, error)
GetLoopOutSwap(ctx context.Context, swapHash []byte) (GetLoopOutSwapRow, error)
GetLoopOutSwaps(ctx context.Context) ([]GetLoopOutSwapsRow, error)
GetMigration(ctx context.Context, migrationID string) (MigrationTracker, error)
GetParentBatch(ctx context.Context, swapHash []byte) (SweepBatch, error)
GetReservation(ctx context.Context, reservationID []byte) (Reservation, error)
GetReservationUpdates(ctx context.Context, reservationID []byte) ([]ReservationUpdate, error)
@ -35,9 +37,11 @@ type Querier interface {
InsertInstantOutUpdate(ctx context.Context, arg InsertInstantOutUpdateParams) error
InsertLoopIn(ctx context.Context, arg InsertLoopInParams) error
InsertLoopOut(ctx context.Context, arg InsertLoopOutParams) error
InsertMigration(ctx context.Context, arg InsertMigrationParams) error
InsertReservationUpdate(ctx context.Context, arg InsertReservationUpdateParams) error
InsertSwap(ctx context.Context, arg InsertSwapParams) error
InsertSwapUpdate(ctx context.Context, arg InsertSwapUpdateParams) error
OverrideSwapCosts(ctx context.Context, arg OverrideSwapCostsParams) error
UpdateBatch(ctx context.Context, arg UpdateBatchParams) error
UpdateInstantOut(ctx context.Context, arg UpdateInstantOutParams) error
UpdateReservation(ctx context.Context, arg UpdateReservationParams) error

View file

@ -0,0 +1,14 @@
-- name: InsertMigration :exec
INSERT INTO migration_tracker (
migration_id,
migration_ts
) VALUES ($1, $2);
-- name: GetMigration :one
SELECT
migration_id,
migration_ts
FROM
migration_tracker
WHERE
migration_id = $1;

View file

@ -133,3 +133,19 @@ INSERT INTO htlc_keys(
) VALUES (
$1, $2, $3, $4, $5, $6, $7
);
-- name: GetLastUpdateID :one
SELECT id
FROM swap_updates
WHERE swap_hash = $1
ORDER BY update_timestamp DESC
LIMIT 1;
-- name: OverrideSwapCosts :exec
UPDATE swap_updates
SET
server_cost = $2,
onchain_cost = $3,
offchain_cost = $4
WHERE id = $1;

View file

@ -10,6 +10,21 @@ import (
"time"
)
const getLastUpdateID = `-- name: GetLastUpdateID :one
SELECT id
FROM swap_updates
WHERE swap_hash = $1
ORDER BY update_timestamp DESC
LIMIT 1
`
func (q *Queries) GetLastUpdateID(ctx context.Context, swapHash []byte) (int32, error) {
row := q.db.QueryRowContext(ctx, getLastUpdateID, swapHash)
var id int32
err := row.Scan(&id)
return id, err
}
const getLoopInSwap = `-- name: GetLoopInSwap :one
SELECT
swaps.id, swaps.swap_hash, swaps.preimage, swaps.initiation_time, swaps.amount_requested, swaps.cltv_expiry, swaps.max_miner_fee, swaps.max_swap_fee, swaps.initiation_height, swaps.protocol_version, swaps.label,
@ -596,3 +611,29 @@ func (q *Queries) InsertSwapUpdate(ctx context.Context, arg InsertSwapUpdatePara
)
return err
}
const overrideSwapCosts = `-- name: OverrideSwapCosts :exec
UPDATE swap_updates
SET
server_cost = $2,
onchain_cost = $3,
offchain_cost = $4
WHERE id = $1
`
type OverrideSwapCostsParams struct {
ID int32
ServerCost int64
OnchainCost int64
OffchainCost int64
}
func (q *Queries) OverrideSwapCosts(ctx context.Context, arg OverrideSwapCostsParams) error {
_, err := q.db.ExecContext(ctx, overrideSwapCosts,
arg.ID,
arg.ServerCost,
arg.OnchainCost,
arg.OffchainCost,
)
return err
}

View file

@ -1009,3 +1009,25 @@ func (b *boltSwapStore) BatchInsertUpdate(ctx context.Context,
return errUnimplemented
}
// BatchUpdateLoopOutSwapCosts updates the swap costs for a batch of loop out
// swaps.
func (b *boltSwapStore) BatchUpdateLoopOutSwapCosts(ctx context.Context,
costs map[lntypes.Hash]SwapCost) error {
return errUnimplemented
}
// HasMigration returns true if the migration with the given ID has been done.
func (b *boltSwapStore) HasMigration(ctx context.Context, migrationID string) (
bool, error) {
return false, errUnimplemented
}
// SetMigration marks the migration with the given ID as done.
func (b *boltSwapStore) SetMigration(ctx context.Context,
migrationID string) error {
return errUnimplemented
}

View file

@ -3,6 +3,7 @@ package loopdb
import (
"context"
"errors"
"fmt"
"testing"
"time"
@ -23,6 +24,8 @@ type StoreMock struct {
loopInStoreChan chan LoopInContract
loopInUpdateChan chan SwapStateData
migrations map[string]struct{}
t *testing.T
}
@ -38,6 +41,7 @@ func NewStoreMock(t *testing.T) *StoreMock {
loopInUpdateChan: make(chan SwapStateData, 1),
LoopInSwaps: make(map[lntypes.Hash]*LoopInContract),
LoopInUpdates: make(map[lntypes.Hash][]SwapStateData),
migrations: make(map[string]struct{}),
t: t,
}
}
@ -337,3 +341,46 @@ func (b *StoreMock) BatchInsertUpdate(ctx context.Context,
return errors.New("not implemented")
}
// BatchUpdateLoopOutSwapCosts updates the swap costs for a batch of loop out
// swaps.
func (s *StoreMock) BatchUpdateLoopOutSwapCosts(ctx context.Context,
costs map[lntypes.Hash]SwapCost) error {
for hash, cost := range costs {
if _, ok := s.LoopOutUpdates[hash]; !ok {
return fmt.Errorf("swap has no updates: %v", hash)
}
updates, ok := s.LoopOutUpdates[hash]
if !ok {
return fmt.Errorf("swap has no updates: %v", hash)
}
updates[len(updates)-1].Cost = cost
}
return nil
}
// HasMigration returns true if the migration with the given ID has been done.
func (s *StoreMock) HasMigration(ctx context.Context, migrationID string) (
bool, error) {
_, ok := s.migrations[migrationID]
return ok, nil
}
// SetMigration marks the migration with the given ID as done.
func (s *StoreMock) SetMigration(ctx context.Context,
migrationID string) error {
if _, ok := s.migrations[migrationID]; ok {
return errors.New("migration already done")
}
s.migrations[migrationID] = struct{}{}
return nil
}