From 2ee772b251aac82c8990817dde471d32447ce967 Mon Sep 17 00:00:00 2001 From: Slyghtning Date: Wed, 11 Jun 2025 12:02:25 +0200 Subject: [PATCH] staticaddr: selected swap amount migration The selected_amount column of all previous swaps is filled with the total value of deposits that partook in these swaps. --- loopd/daemon.go | 10 ++ loopdb/sqlc/querier.go | 1 + loopdb/sqlc/queries/static_address_loopin.sql | 6 + loopdb/sqlc/static_address_loopin.sql.go | 17 +++ .../loopin/deposit_swaphash_migration_test.go | 4 +- .../loopin/selected_amount_migration.go | 84 ++++++++++++++ .../loopin/selected_amount_migration_test.go | 107 ++++++++++++++++++ staticaddr/loopin/sql_store.go | 43 ++++++- 8 files changed, 265 insertions(+), 7 deletions(-) create mode 100644 staticaddr/loopin/selected_amount_migration.go create mode 100644 staticaddr/loopin/selected_amount_migration_test.go diff --git a/loopd/daemon.go b/loopd/daemon.go index 5c4201dd..c66196bf 100644 --- a/loopd/daemon.go +++ b/loopd/daemon.go @@ -635,6 +635,16 @@ func (d *Daemon) initialize(withMacaroonService bool) error { return err } + // Run the selected amount migration. + err = loopin.MigrateSelectedSwapAmount( + d.mainCtx, swapDb, depositStore, staticAddressLoopInStore, + ) + if err != nil { + errorf("Selected amount migration failed: %v", err) + + return err + } + staticLoopInManager = loopin.NewManager(&loopin.Config{ Server: staticAddressClient, QuoteGetter: swapClient.Server, diff --git a/loopdb/sqlc/querier.go b/loopdb/sqlc/querier.go index 0a380e42..2d8dc137 100644 --- a/loopdb/sqlc/querier.go +++ b/loopdb/sqlc/querier.go @@ -65,6 +65,7 @@ type Querier interface { InsertSwapUpdate(ctx context.Context, arg InsertSwapUpdateParams) error IsStored(ctx context.Context, swapHash []byte) (bool, error) MapDepositToSwap(ctx context.Context, arg MapDepositToSwapParams) error + OverrideSelectedSwapAmount(ctx context.Context, arg OverrideSelectedSwapAmountParams) error OverrideSwapCosts(ctx context.Context, arg OverrideSwapCostsParams) error SwapHashForDepositID(ctx context.Context, depositID []byte) ([]byte, error) UpdateBatch(ctx context.Context, arg UpdateBatchParams) error diff --git a/loopdb/sqlc/queries/static_address_loopin.sql b/loopdb/sqlc/queries/static_address_loopin.sql index 2d35b906..9ad51d88 100644 --- a/loopdb/sqlc/queries/static_address_loopin.sql +++ b/loopdb/sqlc/queries/static_address_loopin.sql @@ -96,6 +96,12 @@ SELECT EXISTS ( WHERE swap_hash = $1 ); +-- name: OverrideSelectedSwapAmount :exec +UPDATE static_address_swaps +SET + selected_amount = $2 +WHERE swap_hash = $1; + -- name: MapDepositToSwap :exec UPDATE deposits diff --git a/loopdb/sqlc/static_address_loopin.sql.go b/loopdb/sqlc/static_address_loopin.sql.go index 69e868ae..3dbcab46 100644 --- a/loopdb/sqlc/static_address_loopin.sql.go +++ b/loopdb/sqlc/static_address_loopin.sql.go @@ -457,6 +457,23 @@ func (q *Queries) MapDepositToSwap(ctx context.Context, arg MapDepositToSwapPara return err } +const overrideSelectedSwapAmount = `-- name: OverrideSelectedSwapAmount :exec +UPDATE static_address_swaps +SET + selected_amount = $2 +WHERE swap_hash = $1 +` + +type OverrideSelectedSwapAmountParams struct { + SwapHash []byte + SelectedAmount int64 +} + +func (q *Queries) OverrideSelectedSwapAmount(ctx context.Context, arg OverrideSelectedSwapAmountParams) error { + _, err := q.db.ExecContext(ctx, overrideSelectedSwapAmount, arg.SwapHash, arg.SelectedAmount) + return err +} + const swapHashForDepositID = `-- name: SwapHashForDepositID :one SELECT swap_hash diff --git a/staticaddr/loopin/deposit_swaphash_migration_test.go b/staticaddr/loopin/deposit_swaphash_migration_test.go index 1db77fe9..ab93a10d 100644 --- a/staticaddr/loopin/deposit_swaphash_migration_test.go +++ b/staticaddr/loopin/deposit_swaphash_migration_test.go @@ -119,13 +119,13 @@ func TestDepositSwapHashMigration(t *testing.T) { // If so, we reject the loop-in to prevent potential issues with // parsing. for _, outpoint := range loopIn.DepositOutpoints { - if strings.Contains(outpoint, outpointSeparator) { + if strings.Contains(outpoint, OutpointSeparator) { return ErrInvalidOutpoint } } joinedOutpoints := strings.Join( - loopIn.DepositOutpoints, outpointSeparator, + loopIn.DepositOutpoints, OutpointSeparator, ) staticAddressLoopInParams := sqlc.InsertStaticAddressLoopInParams{ SwapHash: loopIn.SwapHash[:], diff --git a/staticaddr/loopin/selected_amount_migration.go b/staticaddr/loopin/selected_amount_migration.go new file mode 100644 index 00000000..168efe93 --- /dev/null +++ b/staticaddr/loopin/selected_amount_migration.go @@ -0,0 +1,84 @@ +package loopin + +import ( + "context" + "fmt" + "time" + + "github.com/btcsuite/btcd/btcutil" + "github.com/lightninglabs/loop/loopdb" + "github.com/lightninglabs/loop/staticaddr/deposit" + "github.com/lightningnetwork/lnd/lntypes" +) + +const ( + // selectedAmountMigrationID is the identifier for the selected swap + // amount migration. + selectedAmountMigrationID = "selected_amount" +) + +// MigrateSelectedSwapAmount will update the selected swap amount of past swaps +// with the sum of the values of the deposits they swapped. +func MigrateSelectedSwapAmount(ctx context.Context, db loopdb.SwapStore, + depositStore *deposit.SqlStore, swapStore *SqlStore) error { + + migrationDone, err := db.HasMigration(ctx, selectedAmountMigrationID) + if err != nil { + return fmt.Errorf("unable to check migration status: %w", err) + } + if migrationDone { + log.Infof("Selected swap amount migration already done, " + + "skipping") + + return nil + } + + log.Infof("Starting swap amount migration") + startTs := time.Now() + defer func() { + log.Infof("Finished swap amount migration in %v", + time.Since(startTs)) + }() + + // First we'll fetch all loop out swaps from the database. + swaps, err := swapStore.GetStaticAddressLoopInSwapsByStates( + ctx, FinalStates, + ) + if err != nil { + return err + } + + // Now we'll calculate the cost for each swap and finally update the + // costs in the database. + // TODO(hieblmi): normalize swap hash and deposit ids. + updateAmounts := make(map[lntypes.Hash]btcutil.Amount) + for _, swap := range swaps { + for _, outpoint := range swap.DepositOutpoints { + deposit, err := depositStore.DepositForOutpoint( + ctx, outpoint, + ) + if err != nil { + return fmt.Errorf("unable to fetch deposit "+ + "for outpoint %s: %w", outpoint, err) + } + if deposit == nil { + return fmt.Errorf("deposit for outpoint %s "+ + "not found", outpoint) + } + + // Set the selected amount to the value of the deposit. + updateAmounts[swap.SwapHash] += deposit.Value + } + } + + log.Infof("Updating selected swap amounts for %d loop in swaps", + len(updateAmounts)) + + err = swapStore.BatchUpdateSelectedSwapAmounts(ctx, updateAmounts) + if err != nil { + return err + } + + // Finally mark the migration as done. + return db.SetMigration(ctx, selectedAmountMigrationID) +} diff --git a/staticaddr/loopin/selected_amount_migration_test.go b/staticaddr/loopin/selected_amount_migration_test.go new file mode 100644 index 00000000..b68269fc --- /dev/null +++ b/staticaddr/loopin/selected_amount_migration_test.go @@ -0,0 +1,107 @@ +package loopin + +import ( + "context" + "testing" + "time" + + "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/wire" + "github.com/lightninglabs/loop/loopdb" + "github.com/lightninglabs/loop/staticaddr/deposit" + "github.com/lightninglabs/loop/test" + "github.com/lightningnetwork/lnd/clock" + "github.com/lightningnetwork/lnd/lntypes" + "github.com/stretchr/testify/require" +) + +// TestMigrateSelectedSwapAmount tests the selected amount migration. +func TestMigrateSelectedSwapAmount(t *testing.T) { + // Set up test context objects. + ctxb := context.Background() + testDb := loopdb.NewTestDB(t) + testClock := clock.NewTestClock(time.Now()) + defer testDb.Close() + + db := loopdb.NewStoreMock(t) + depositStore := deposit.NewSqlStore(testDb.BaseDB) + swapStore := NewSqlStore( + loopdb.NewTypedStore[Querier](testDb), testClock, + &chaincfg.MainNetParams, + ) + + newID := func() deposit.ID { + did, err := deposit.GetRandomDepositID() + require.NoError(t, err) + + return did + } + + d1, d2 := &deposit.Deposit{ + ID: newID(), + OutPoint: wire.OutPoint{ + Hash: chainhash.Hash{0x1a, 0x2b, 0x3c, 0x4d}, + Index: 0, + }, + Value: btcutil.Amount(100_000), + TimeOutSweepPkScript: []byte{ + 0x00, 0x14, 0x1a, 0x2b, 0x3c, 0x41, + }, + }, + &deposit.Deposit{ + ID: newID(), + OutPoint: wire.OutPoint{ + Hash: chainhash.Hash{0x2a, 0x2b, 0x3c, 0x4e}, + Index: 1, + }, + Value: btcutil.Amount(200_000), + TimeOutSweepPkScript: []byte{ + 0x00, 0x14, 0x1a, 0x2b, 0x3c, 0x4d, + }, + } + + err := depositStore.CreateDeposit(ctxb, d1) + require.NoError(t, err) + err = depositStore.CreateDeposit(ctxb, d2) + require.NoError(t, err) + + outpoints := []string{ + d1.OutPoint.String(), + d2.OutPoint.String(), + } + _, clientPubKey := test.CreateKey(1) + _, serverPubKey := test.CreateKey(2) + p2wkhAddr := "bcrt1qq68r6ff4k4pjx39efs44gcyccf7unqnu5qtjjz" + addr, err := btcutil.DecodeAddress(p2wkhAddr, nil) + require.NoError(t, err) + + swap := StaticAddressLoopIn{ + SwapHash: lntypes.Hash{0x1, 0x2, 0x3, 0x4}, + DepositOutpoints: outpoints, + ClientPubkey: clientPubKey, + ServerPubkey: serverPubKey, + HtlcTimeoutSweepAddress: addr, + Deposits: []*deposit.Deposit{d1, d2}, + } + swap.SetState(Succeeded) + + err = swapStore.CreateLoopIn(ctxb, &swap) + require.NoError(t, err) + + storedSwaps, err := swapStore.GetStaticAddressLoopInSwapsByStates( + ctxb, FinalStates, + ) + require.NoError(t, err) + require.EqualValues(t, 0, storedSwaps[0].SelectedAmount) + + err = MigrateSelectedSwapAmount(ctxb, db, depositStore, swapStore) + require.NoError(t, err) + + storedSwaps, err = swapStore.GetStaticAddressLoopInSwapsByStates( + ctxb, FinalStates, + ) + require.NoError(t, err) + require.EqualValues(t, d1.Value+d2.Value, storedSwaps[0].SelectedAmount) +} diff --git a/staticaddr/loopin/sql_store.go b/staticaddr/loopin/sql_store.go index 78a32312..e291680d 100644 --- a/staticaddr/loopin/sql_store.go +++ b/staticaddr/loopin/sql_store.go @@ -22,7 +22,7 @@ import ( "github.com/lightningnetwork/lnd/lnwallet/chainfee" ) -const outpointSeparator = ";" +const OutpointSeparator = ";" var ( // ErrInvalidOutpoint is returned when an outpoint contains the outpoint @@ -88,6 +88,11 @@ type Querier interface { // DepositsForSwapHash retrieves all deposits for a given swap hash. DepositsForSwapHash(ctx context.Context, swapHash []byte) ([]sqlc.DepositsForSwapHashRow, error) + + // OverrideSelectedSwapAmount updates the selected swap amount for + // a given swap hash. + OverrideSelectedSwapAmount(ctx context.Context, + params sqlc.OverrideSelectedSwapAmountParams) error } // BaseDB is the interface that contains all the queries generated by sqlc for @@ -226,11 +231,16 @@ func (s *SqlStore) CreateLoopIn(ctx context.Context, return errors.New("loop-in must have at least one deposit") } + amountRequested := int64(loopIn.TotalDepositAmount()) + if loopIn.SelectedAmount > 0 { + amountRequested = int64(loopIn.SelectedAmount) + } + swapArgs := sqlc.InsertSwapParams{ SwapHash: loopIn.SwapHash[:], Preimage: loopIn.SwapPreimage[:], InitiationTime: loopIn.InitiationTime, - AmountRequested: int64(loopIn.TotalDepositAmount()), + AmountRequested: amountRequested, CltvExpiry: loopIn.HtlcCltvExpiry, MaxSwapFee: int64(loopIn.MaxSwapFee), InitiationHeight: int32(loopIn.InitiationHeight), @@ -250,13 +260,13 @@ func (s *SqlStore) CreateLoopIn(ctx context.Context, // If so, we reject the loop-in to prevent potential issues with // parsing. for _, outpoint := range loopIn.DepositOutpoints { - if strings.Contains(outpoint, outpointSeparator) { + if strings.Contains(outpoint, OutpointSeparator) { return ErrInvalidOutpoint } } joinedOutpoints := strings.Join( - loopIn.DepositOutpoints, outpointSeparator, + loopIn.DepositOutpoints, OutpointSeparator, ) staticAddressLoopInParams := sqlc.InsertStaticAddressLoopInParams{ SwapHash: loopIn.SwapHash[:], @@ -266,6 +276,7 @@ func (s *SqlStore) CreateLoopIn(ctx context.Context, HtlcTimeoutSweepAddress: loopIn.HtlcTimeoutSweepAddress.String(), HtlcTxFeeRateSatKw: int64(loopIn.HtlcTxFeeRate), DepositOutpoints: joinedOutpoints, + SelectedAmount: int64(loopIn.SelectedAmount), PaymentTimeoutSeconds: int32(loopIn.PaymentTimeoutSeconds), } @@ -350,6 +361,27 @@ func (s *SqlStore) UpdateLoopIn(ctx context.Context, ) } +func (s *SqlStore) BatchUpdateSelectedSwapAmounts(ctx context.Context, + updateAmounts map[lntypes.Hash]btcutil.Amount) error { + + return s.baseDB.ExecTx(ctx, loopdb.NewSqlWriteOpts(), + func(q Querier) error { + for swapHash, amount := range updateAmounts { + err := q.OverrideSelectedSwapAmount( + ctx, sqlc.OverrideSelectedSwapAmountParams{ + SwapHash: swapHash[:], + SelectedAmount: int64(amount), + }, + ) + if err != nil { + return err + } + } + + return nil + }) +} + // IsStored returns true if a swap with the given hash is stored in the // database, false otherwise. func (s *SqlStore) IsStored(ctx context.Context, swapHash lntypes.Hash) (bool, @@ -478,7 +510,7 @@ func toStaticAddressLoopIn(_ context.Context, network *chaincfg.Params, } depositOutpoints := strings.Split( - swap.DepositOutpoints, outpointSeparator, + swap.DepositOutpoints, OutpointSeparator, ) timeoutAddressString := swap.HtlcTimeoutSweepAddress @@ -548,6 +580,7 @@ func toStaticAddressLoopIn(_ context.Context, network *chaincfg.Params, LastHop: swap.LastHop, QuotedSwapFee: btcutil.Amount(swap.QuotedSwapFeeSatoshis), DepositOutpoints: depositOutpoints, + SelectedAmount: btcutil.Amount(swap.SelectedAmount), HtlcTxFeeRate: chainfee.SatPerKWeight( swap.HtlcTxFeeRateSatKw, ),