Merge pull request #988 from hieblmi/normalize-deposit-swap

staticaddr: migrate deposit swap hashes
This commit is contained in:
Slyghtning 2025-08-01 23:07:04 +02:00 committed by GitHub
commit 2f21a488d8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 976 additions and 44 deletions

2
go.mod
View file

@ -16,6 +16,7 @@ require (
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0
github.com/jackc/pgconn v1.14.3
github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438
github.com/jackc/pgx/v5 v5.6.0
github.com/jessevdk/go-flags v1.4.0
github.com/lib/pq v1.10.9
github.com/lightninglabs/aperture v0.3.13-beta
@ -104,7 +105,6 @@ require (
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/pgtype v1.14.0 // indirect
github.com/jackc/pgx/v4 v4.18.2 // indirect
github.com/jackc/pgx/v5 v5.6.0 // indirect
github.com/jackc/puddle v1.3.0 // indirect
github.com/jackc/puddle/v2 v2.2.1 // indirect
github.com/jackpal/gateway v1.0.5 // indirect

View file

@ -626,6 +626,16 @@ func (d *Daemon) initialize(withMacaroonService bool) error {
clock.NewDefaultClock(), d.lnd.ChainParams,
)
// Run the deposit swap hash migration.
err = loopin.MigrateDepositSwapHash(
d.mainCtx, swapDb, depositStore, staticAddressLoopInStore,
)
if err != nil {
errorf("Deposit swap hash migration failed: %v", err)
return err
}
staticLoopInManager = loopin.NewManager(&loopin.Config{
Server: staticAddressClient,
QuoteGetter: swapClient.Server,

View file

@ -45,4 +45,4 @@ CREATE TABLE IF NOT EXISTS deposit_updates (
-- update_timestamp is the timestamp of the update.
update_timestamp TIMESTAMP NOT NULL
);
);

View file

@ -0,0 +1 @@
ALTER TABLE deposits DROP COLUMN swap_hash;

View file

@ -0,0 +1 @@
ALTER TABLE deposits ADD swap_hash BLOB;

View file

@ -19,6 +19,7 @@ type Deposit struct {
TimeoutSweepPkScript []byte
ExpirySweepTxid []byte
FinalizedWithdrawalTx sql.NullString
SwapHash []byte
}
type DepositUpdate struct {

View file

@ -19,6 +19,8 @@ type Querier interface {
CreateWithdrawal(ctx context.Context, arg CreateWithdrawalParams) error
CreateWithdrawalDeposit(ctx context.Context, arg CreateWithdrawalDepositParams) error
DepositForOutpoint(ctx context.Context, arg DepositForOutpointParams) (Deposit, error)
DepositIDsForSwapHash(ctx context.Context, swapHash []byte) ([][]byte, error)
DepositsForSwapHash(ctx context.Context, swapHash []byte) ([]DepositsForSwapHashRow, error)
FetchLiquidityParams(ctx context.Context) ([]byte, error)
GetAllWithdrawals(ctx context.Context) ([]Withdrawal, error)
GetBatchSweeps(ctx context.Context, batchID int32) ([]Sweep, error)
@ -62,7 +64,9 @@ type Querier interface {
InsertSwap(ctx context.Context, arg InsertSwapParams) error
InsertSwapUpdate(ctx context.Context, arg InsertSwapUpdateParams) error
IsStored(ctx context.Context, swapHash []byte) (bool, error)
MapDepositToSwap(ctx context.Context, arg MapDepositToSwapParams) error
OverrideSwapCosts(ctx context.Context, arg OverrideSwapCostsParams) error
SwapHashForDepositID(ctx context.Context, depositID []byte) ([]byte, error)
UpdateBatch(ctx context.Context, arg UpdateBatchParams) error
UpdateDeposit(ctx context.Context, arg UpdateDepositParams) error
UpdateInstantOut(ctx context.Context, arg UpdateInstantOutParams) error

View file

@ -93,3 +93,51 @@ SELECT EXISTS (
FROM static_address_swaps
WHERE swap_hash = $1
);
-- name: MapDepositToSwap :exec
UPDATE
deposits
SET
swap_hash = $2
WHERE
deposit_id = $1;
-- name: SwapHashForDepositID :one
SELECT
swap_hash
FROM
deposits
WHERE
deposit_id = $1;
-- name: DepositIDsForSwapHash :many
SELECT
deposit_id
FROM
deposits
WHERE
swap_hash = $1;
-- name: DepositsForSwapHash :many
SELECT
d.*,
u.update_state,
u.update_timestamp
FROM
deposits d
LEFT JOIN
deposit_updates u ON u.id = (
SELECT id
FROM deposit_updates
WHERE deposit_id = d.deposit_id
ORDER BY update_timestamp DESC
LIMIT 1
)
WHERE
d.swap_hash = $1;

View file

@ -13,7 +13,7 @@ import (
const allDeposits = `-- name: AllDeposits :many
SELECT
id, deposit_id, tx_hash, out_index, amount, confirmation_height, timeout_sweep_pk_script, expiry_sweep_txid, finalized_withdrawal_tx
id, deposit_id, tx_hash, out_index, amount, confirmation_height, timeout_sweep_pk_script, expiry_sweep_txid, finalized_withdrawal_tx, swap_hash
FROM
deposits
ORDER BY
@ -39,6 +39,7 @@ func (q *Queries) AllDeposits(ctx context.Context) ([]Deposit, error) {
&i.TimeoutSweepPkScript,
&i.ExpirySweepTxid,
&i.FinalizedWithdrawalTx,
&i.SwapHash,
); err != nil {
return nil, err
}
@ -102,7 +103,7 @@ func (q *Queries) CreateDeposit(ctx context.Context, arg CreateDepositParams) er
const depositForOutpoint = `-- name: DepositForOutpoint :one
SELECT
id, deposit_id, tx_hash, out_index, amount, confirmation_height, timeout_sweep_pk_script, expiry_sweep_txid, finalized_withdrawal_tx
id, deposit_id, tx_hash, out_index, amount, confirmation_height, timeout_sweep_pk_script, expiry_sweep_txid, finalized_withdrawal_tx, swap_hash
FROM
deposits
WHERE
@ -129,13 +130,14 @@ func (q *Queries) DepositForOutpoint(ctx context.Context, arg DepositForOutpoint
&i.TimeoutSweepPkScript,
&i.ExpirySweepTxid,
&i.FinalizedWithdrawalTx,
&i.SwapHash,
)
return i, err
}
const getDeposit = `-- name: GetDeposit :one
SELECT
id, deposit_id, tx_hash, out_index, amount, confirmation_height, timeout_sweep_pk_script, expiry_sweep_txid, finalized_withdrawal_tx
id, deposit_id, tx_hash, out_index, amount, confirmation_height, timeout_sweep_pk_script, expiry_sweep_txid, finalized_withdrawal_tx, swap_hash
FROM
deposits
WHERE
@ -155,6 +157,7 @@ func (q *Queries) GetDeposit(ctx context.Context, depositID []byte) (Deposit, er
&i.TimeoutSweepPkScript,
&i.ExpirySweepTxid,
&i.FinalizedWithdrawalTx,
&i.SwapHash,
)
return i, err
}

View file

@ -11,6 +11,108 @@ import (
"time"
)
const depositIDsForSwapHash = `-- name: DepositIDsForSwapHash :many
SELECT
deposit_id
FROM
deposits
WHERE
swap_hash = $1
`
func (q *Queries) DepositIDsForSwapHash(ctx context.Context, swapHash []byte) ([][]byte, error) {
rows, err := q.db.QueryContext(ctx, depositIDsForSwapHash, swapHash)
if err != nil {
return nil, err
}
defer rows.Close()
var items [][]byte
for rows.Next() {
var deposit_id []byte
if err := rows.Scan(&deposit_id); err != nil {
return nil, err
}
items = append(items, deposit_id)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const depositsForSwapHash = `-- name: DepositsForSwapHash :many
SELECT
d.id, d.deposit_id, d.tx_hash, d.out_index, d.amount, d.confirmation_height, d.timeout_sweep_pk_script, d.expiry_sweep_txid, d.finalized_withdrawal_tx, d.swap_hash,
u.update_state,
u.update_timestamp
FROM
deposits d
LEFT JOIN
deposit_updates u ON u.id = (
SELECT id
FROM deposit_updates
WHERE deposit_id = d.deposit_id
ORDER BY update_timestamp DESC
LIMIT 1
)
WHERE
d.swap_hash = $1
`
type DepositsForSwapHashRow struct {
ID int32
DepositID []byte
TxHash []byte
OutIndex int32
Amount int64
ConfirmationHeight int64
TimeoutSweepPkScript []byte
ExpirySweepTxid []byte
FinalizedWithdrawalTx sql.NullString
SwapHash []byte
UpdateState sql.NullString
UpdateTimestamp sql.NullTime
}
func (q *Queries) DepositsForSwapHash(ctx context.Context, swapHash []byte) ([]DepositsForSwapHashRow, error) {
rows, err := q.db.QueryContext(ctx, depositsForSwapHash, swapHash)
if err != nil {
return nil, err
}
defer rows.Close()
var items []DepositsForSwapHashRow
for rows.Next() {
var i DepositsForSwapHashRow
if err := rows.Scan(
&i.ID,
&i.DepositID,
&i.TxHash,
&i.OutIndex,
&i.Amount,
&i.ConfirmationHeight,
&i.TimeoutSweepPkScript,
&i.ExpirySweepTxid,
&i.FinalizedWithdrawalTx,
&i.SwapHash,
&i.UpdateState,
&i.UpdateTimestamp,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getLoopInSwapUpdates = `-- name: GetLoopInSwapUpdates :many
SELECT
static_address_swap_updates.id, static_address_swap_updates.swap_hash, static_address_swap_updates.update_state, static_address_swap_updates.update_timestamp
@ -328,6 +430,41 @@ func (q *Queries) IsStored(ctx context.Context, swapHash []byte) (bool, error) {
return exists, err
}
const mapDepositToSwap = `-- name: MapDepositToSwap :exec
UPDATE
deposits
SET
swap_hash = $2
WHERE
deposit_id = $1
`
type MapDepositToSwapParams struct {
DepositID []byte
SwapHash []byte
}
func (q *Queries) MapDepositToSwap(ctx context.Context, arg MapDepositToSwapParams) error {
_, err := q.db.ExecContext(ctx, mapDepositToSwap, arg.DepositID, arg.SwapHash)
return err
}
const swapHashForDepositID = `-- name: SwapHashForDepositID :one
SELECT
swap_hash
FROM
deposits
WHERE
deposit_id = $1
`
func (q *Queries) SwapHashForDepositID(ctx context.Context, depositID []byte) ([]byte, error) {
row := q.db.QueryRowContext(ctx, swapHashForDepositID, depositID)
var swap_hash []byte
err := row.Scan(&swap_hash)
return swap_hash, err
}
const updateStaticAddressLoopIn = `-- name: UpdateStaticAddressLoopIn :exec
UPDATE static_address_swaps
SET

View file

@ -135,7 +135,7 @@ func (s *SqlStore) GetDeposit(ctx context.Context, id ID) (*Deposit, error) {
return err
}
deposit, err = s.toDeposit(row, latestUpdate)
deposit, err = ToDeposit(row, latestUpdate)
if err != nil {
return err
}
@ -177,7 +177,7 @@ func (s *SqlStore) DepositForOutpoint(ctx context.Context,
return err
}
deposit, err = s.toDeposit(row, latestUpdate)
deposit, err = ToDeposit(row, latestUpdate)
if err != nil {
return err
}
@ -212,7 +212,7 @@ func (s *SqlStore) AllDeposits(ctx context.Context) ([]*Deposit, error) {
return err
}
d, err := s.toDeposit(deposit, latestUpdate)
d, err := ToDeposit(deposit, latestUpdate)
if err != nil {
return err
}
@ -229,9 +229,9 @@ func (s *SqlStore) AllDeposits(ctx context.Context) ([]*Deposit, error) {
return allDeposits, nil
}
// toDeposit converts an sql deposit to a deposit.
func (s *SqlStore) toDeposit(row sqlc.Deposit,
lastUpdate sqlc.DepositUpdate) (*Deposit, error) {
// ToDeposit converts an sql deposit to a deposit.
func ToDeposit(row sqlc.Deposit, lastUpdate sqlc.DepositUpdate) (*Deposit,
error) {
id := ID{}
err := id.FromByteSlice(row.DepositID)

View file

@ -0,0 +1,88 @@
package loopin
import (
"context"
"fmt"
"time"
"github.com/lightninglabs/loop/loopdb"
"github.com/lightninglabs/loop/staticaddr/deposit"
"github.com/lightningnetwork/lnd/lntypes"
)
const (
// depositSwapHashMigrationID is the identifier for the deposit swap
// hash migration.
depositSwapHashMigrationID = "deposit_swap_hash"
)
// MigrateDepositSwapHash will retrieve the comma separated deposit list of
// past and pending swaps and map them to the swap hash in the deposits table.
func MigrateDepositSwapHash(ctx context.Context, db loopdb.SwapStore,
depositStore *deposit.SqlStore, swapStore *SqlStore) error {
migrationDone, err := db.HasMigration(
ctx, depositSwapHashMigrationID,
)
if err != nil {
return fmt.Errorf("unable to check migration status: %w", err)
}
if migrationDone {
log.Infof("Deposit swap hash migration already done, " +
"skipping")
return nil
}
log.Infof("Starting deposit swap hash migration")
startTs := time.Now()
defer func() {
log.Infof("Finished deposit swap hash migration in %v",
time.Since(startTs))
}()
// First we'll fetch all past loop in swaps from the database.
swaps, err := swapStore.GetStaticAddressLoopInSwapsByStates(
ctx, AllStates,
)
if err != nil {
return err
}
// Now we'll map each deposit of a swap to its respective swap hash.
depositsToSwapHashes := make(map[deposit.ID]lntypes.Hash)
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)
}
if _, ok := depositsToSwapHashes[deposit.ID]; !ok {
depositsToSwapHashes[deposit.ID] = swap.SwapHash
} else {
log.Warnf("Duplicate deposit ID %s found for "+
"outpoint %s, skipping",
deposit.ID, outpoint)
}
}
}
log.Infof("Batch-mapping %d deposits to swap hashes",
len(depositsToSwapHashes))
err = swapStore.BatchMapDepositsToSwapHashes(ctx, depositsToSwapHashes)
if err != nil {
return err
}
// Finally mark the migration as done.
return db.SetMigration(ctx, depositSwapHashMigrationID)
}

View file

@ -0,0 +1,195 @@
package loopin
import (
"context"
"strings"
"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/loopdb/sqlc"
"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"
)
const (
P2wkhAddr = "bcrt1qq68r6ff4k4pjx39efs44gcyccf7unqnu5qtjjz"
)
// TestDepositSwapHashMigration tests deposit to swap hash migration.
func TestDepositSwapHashMigration(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.RegressionNetParams,
)
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)
addr, err := btcutil.DecodeAddress(P2wkhAddr, nil)
require.NoError(t, err)
swapHash := lntypes.Hash{0x1, 0x2, 0x3, 0x4}
loopIn := StaticAddressLoopIn{
SwapHash: swapHash,
DepositOutpoints: outpoints,
ClientPubkey: clientPubKey,
ServerPubkey: serverPubKey,
HtlcTimeoutSweepAddress: addr,
}
loopIn.SetState(Succeeded)
// Insert the swap without the deposit mapping.
err = swapStore.baseDB.ExecTx(ctxb, loopdb.NewSqlWriteOpts(),
func(q Querier) error {
swapArgs := sqlc.InsertSwapParams{
SwapHash: loopIn.SwapHash[:],
Preimage: loopIn.SwapPreimage[:],
InitiationTime: loopIn.InitiationTime,
AmountRequested: int64(loopIn.TotalDepositAmount()),
CltvExpiry: loopIn.HtlcCltvExpiry,
MaxSwapFee: int64(loopIn.MaxSwapFee),
InitiationHeight: int32(loopIn.InitiationHeight),
ProtocolVersion: int32(loopIn.ProtocolVersion),
Label: loopIn.Label,
}
htlcKeyArgs := sqlc.InsertHtlcKeysParams{
SwapHash: loopIn.SwapHash[:],
SenderScriptPubkey: loopIn.ClientPubkey.SerializeCompressed(),
ReceiverScriptPubkey: loopIn.ServerPubkey.SerializeCompressed(),
ClientKeyFamily: int32(loopIn.HtlcKeyLocator.Family),
ClientKeyIndex: int32(loopIn.HtlcKeyLocator.Index),
}
// Sanity check, if any of the outpoints contain the outpoint separator.
// If so, we reject the loop-in to prevent potential issues with
// parsing.
for _, outpoint := range loopIn.DepositOutpoints {
if strings.Contains(outpoint, outpointSeparator) {
return ErrInvalidOutpoint
}
}
joinedOutpoints := strings.Join(
loopIn.DepositOutpoints, outpointSeparator,
)
staticAddressLoopInParams := sqlc.InsertStaticAddressLoopInParams{
SwapHash: loopIn.SwapHash[:],
SwapInvoice: loopIn.SwapInvoice,
LastHop: loopIn.LastHop,
QuotedSwapFeeSatoshis: int64(loopIn.QuotedSwapFee),
HtlcTimeoutSweepAddress: loopIn.HtlcTimeoutSweepAddress.String(),
HtlcTxFeeRateSatKw: int64(loopIn.HtlcTxFeeRate),
DepositOutpoints: joinedOutpoints,
PaymentTimeoutSeconds: int32(loopIn.PaymentTimeoutSeconds),
}
updateArgs := sqlc.InsertStaticAddressMetaUpdateParams{
SwapHash: loopIn.SwapHash[:],
UpdateTimestamp: testClock.Now(),
UpdateState: string(loopIn.GetState()),
}
err := q.InsertSwap(ctxb, swapArgs)
if err != nil {
return err
}
err = q.InsertHtlcKeys(ctxb, htlcKeyArgs)
if err != nil {
return err
}
err = q.InsertStaticAddressLoopIn(
ctxb, staticAddressLoopInParams,
)
if err != nil {
return err
}
return q.InsertStaticAddressMetaUpdate(ctxb, updateArgs)
},
)
require.NoError(t, err)
depositIDs, err := swapStore.DepositIDsForSwapHash(ctxb, swapHash)
require.NoError(t, err)
require.Len(t, depositIDs, 0)
swapHashes, err := swapStore.SwapHashesForDepositIDs(
ctxb, []deposit.ID{d1.ID, d2.ID},
)
require.NoError(t, err)
require.Len(t, swapHashes, 0)
err = MigrateDepositSwapHash(ctxb, db, depositStore, swapStore)
require.NoError(t, err)
depositIDs, err = swapStore.DepositIDsForSwapHash(ctxb, swapHash)
require.NoError(t, err)
require.Len(t, depositIDs, 2)
require.Contains(t, depositIDs, d1.ID)
require.Contains(t, depositIDs, d2.ID)
swapHashes, err = swapStore.SwapHashesForDepositIDs(
ctxb, []deposit.ID{d1.ID, d2.ID},
)
require.NoError(t, err)
require.Len(t, swapHashes, 1)
require.Len(t, swapHashes[swapHash], 2)
require.Contains(t, swapHashes[swapHash], d1.ID)
require.Contains(t, swapHashes[swapHash], d2.ID)
}

View file

@ -130,15 +130,16 @@ var (
var PendingStates = []fsm.StateType{
InitHtlcTx, SignHtlcTx, MonitorInvoiceAndHtlcTx, PaymentReceived,
SweepHtlcTimeout, MonitorHtlcTimeoutSweep,
UnlockDeposits,
SweepHtlcTimeout, MonitorHtlcTimeoutSweep, UnlockDeposits,
}
var FinalStates = []fsm.StateType{
HtlcTimeoutSwept, Succeeded, SucceededTransitioningFailed, Failed,
}
var AllStates = append(PendingStates, FinalStates...)
var AllStates = append(
append([]fsm.StateType{}, PendingStates...), FinalStates...,
)
// Events.
var (
@ -149,7 +150,6 @@ var (
OnHtlcTimeoutSweepPublished = fsm.EventType("OnHtlcTimeoutSweepPublished")
OnHtlcTimeoutSwept = fsm.EventType("OnHtlcTimeoutSwept")
OnPaymentReceived = fsm.EventType("OnPaymentReceived")
OnPaymentDeadlineExceeded = fsm.EventType("OnPaymentDeadlineExceeded")
OnSwapTimedOut = fsm.EventType("OnSwapTimedOut")
OnSucceeded = fsm.EventType("OnSucceeded")
OnRecover = fsm.EventType("OnRecover")

View file

@ -10,9 +10,11 @@ import (
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/jackc/pgx/v5"
"github.com/lightninglabs/loop/fsm"
"github.com/lightninglabs/loop/loopdb"
"github.com/lightninglabs/loop/loopdb/sqlc"
"github.com/lightninglabs/loop/staticaddr/deposit"
"github.com/lightninglabs/loop/staticaddr/version"
"github.com/lightningnetwork/lnd/clock"
"github.com/lightningnetwork/lnd/keychain"
@ -68,6 +70,24 @@ type Querier interface {
// IsStored returns true if a swap with the given hash is stored in the
// database, false otherwise.
IsStored(ctx context.Context, swapHash []byte) (bool, error)
// MapDepositToSwap maps a deposit to a swap in the database.
MapDepositToSwap(ctx context.Context,
arg sqlc.MapDepositToSwapParams) error
// SwapHashForDepositID retrieves the swap hash for the given deposit
// ID.
SwapHashForDepositID(ctx context.Context,
depositID []byte) ([]byte, error)
// DepositIDsForSwapHash retrieves all deposit IDs for a given swap
// hash.
DepositIDsForSwapHash(ctx context.Context,
swapHash []byte) ([][]byte, error)
// DepositsForSwapHash retrieves all deposits for a given swap hash.
DepositsForSwapHash(ctx context.Context,
swapHash []byte) ([]sqlc.DepositsForSwapHashRow, error)
}
// BaseDB is the interface that contains all the queries generated by sqlc for
@ -106,10 +126,15 @@ func (s *SqlStore) GetLoopInByHash(ctx context.Context,
var (
err error
row sqlc.GetStaticAddressLoopInSwapRow
swap sqlc.GetStaticAddressLoopInSwapRow
updates []sqlc.StaticAddressSwapUpdate
)
row, err = s.baseDB.GetStaticAddressLoopInSwap(ctx, swapHash[:])
swap, err = s.baseDB.GetStaticAddressLoopInSwap(ctx, swapHash[:])
if err != nil {
return nil, err
}
deposits, err := s.baseDB.DepositsForSwapHash(ctx, swapHash[:])
if err != nil {
return nil, err
}
@ -119,9 +144,7 @@ func (s *SqlStore) GetLoopInByHash(ctx context.Context,
return nil, err
}
return toStaticAddressLoopIn(
ctx, s.network, row, updates,
)
return toStaticAddressLoopIn(ctx, s.network, swap, deposits, updates)
}
// GetStaticAddressLoopInSwapsByStates returns all static address loop-ins from
@ -149,6 +172,11 @@ func (s *SqlStore) GetStaticAddressLoopInSwapsByStates(ctx context.Context,
loopIns := make([]*StaticAddressLoopIn, 0, len(rows))
for _, row := range rows {
deposits, err := s.baseDB.DepositsForSwapHash(ctx, row.SwapHash)
if err != nil {
return nil, err
}
updates, err = s.baseDB.GetLoopInSwapUpdates(
ctx, row.SwapHash,
)
@ -158,7 +186,7 @@ func (s *SqlStore) GetStaticAddressLoopInSwapsByStates(ctx context.Context,
loopIn, err = toStaticAddressLoopIn(
ctx, s.network, sqlc.GetStaticAddressLoopInSwapRow(row),
updates,
deposits, updates,
)
if err != nil {
return nil, err
@ -190,6 +218,14 @@ func toStrings(states []fsm.StateType) []string {
func (s *SqlStore) CreateLoopIn(ctx context.Context,
loopIn *StaticAddressLoopIn) error {
if loopIn == nil {
return errors.New("loop-in cannot be nil")
}
if len(loopIn.Deposits) == 0 {
return errors.New("loop-in must have at least one deposit")
}
swapArgs := sqlc.InsertSwapParams{
SwapHash: loopIn.SwapHash[:],
Preimage: loopIn.SwapPreimage[:],
@ -258,8 +294,24 @@ func (s *SqlStore) CreateLoopIn(ctx context.Context,
return err
}
// Map each deposit to the swap hash in the
// deposit_to_swap table. This allows us to track which
// deposits are used for which swaps.
for _, d := range loopIn.Deposits {
err = q.MapDepositToSwap(
ctx, sqlc.MapDepositToSwapParams{
DepositID: d.ID[:],
SwapHash: loopIn.SwapHash[:],
},
)
if err != nil {
return err
}
}
return q.InsertStaticAddressMetaUpdate(ctx, updateArgs)
})
},
)
}
// UpdateLoopIn updates the loop-in in the database.
@ -306,35 +358,119 @@ func (s *SqlStore) IsStored(ctx context.Context, swapHash lntypes.Hash) (bool,
return s.baseDB.IsStored(ctx, swapHash[:])
}
// BatchMapDepositsToSwapHashes maps multiple deposits to their respective swap
// hashes in a single transaction.
func (s *SqlStore) BatchMapDepositsToSwapHashes(ctx context.Context,
depositsToHashes map[deposit.ID]lntypes.Hash) error {
return s.baseDB.ExecTx(ctx, loopdb.NewSqlWriteOpts(),
func(q Querier) error {
for deposit, swapHash := range depositsToHashes {
err := q.MapDepositToSwap(
ctx, sqlc.MapDepositToSwapParams{
DepositID: deposit[:],
SwapHash: swapHash[:],
},
)
if err != nil {
return err
}
}
return nil
})
}
// SwapHashesForDepositIDs retrieves the swap hashes for the given deposit IDs.
func (s *SqlStore) SwapHashesForDepositIDs(ctx context.Context,
depositIDs []deposit.ID) (map[lntypes.Hash][]deposit.ID, error) {
swapHashes := make(map[lntypes.Hash][]deposit.ID)
for _, id := range depositIDs {
swapHash, err := s.baseDB.SwapHashForDepositID(ctx, id[:])
if err != nil {
if errors.Is(err, sql.ErrNoRows) ||
errors.Is(err, pgx.ErrNoRows) {
return nil, nil
}
return nil, err
}
if swapHash == nil {
return nil, nil
}
if len(swapHash) != lntypes.HashSize {
return nil, errors.New("invalid swap hash length")
}
swapHashParsed, err := lntypes.MakeHash(swapHash)
if err != nil {
return nil, err
}
// Place the deposit ID in the map under the
// corresponding swap hash.
swapHashes[swapHashParsed] = append(
swapHashes[swapHashParsed], id,
)
}
return swapHashes, nil
}
// DepositIDsForSwapHash retrieves all deposit IDs for a given swap hash.
func (s *SqlStore) DepositIDsForSwapHash(ctx context.Context,
swapHash lntypes.Hash) ([]deposit.ID, error) {
byteIDs, err := s.baseDB.DepositIDsForSwapHash(ctx, swapHash[:])
if err != nil {
return nil, err
}
depositIDs := make([]deposit.ID, len(byteIDs))
for i, id := range byteIDs {
if len(id) != deposit.IdLength {
return nil, errors.New("invalid deposit ID length")
}
copy(depositIDs[i][:], id)
}
return depositIDs, nil
}
// toStaticAddressLoopIn converts sql rows to an instant out struct.
func toStaticAddressLoopIn(_ context.Context, network *chaincfg.Params,
row sqlc.GetStaticAddressLoopInSwapRow,
swap sqlc.GetStaticAddressLoopInSwapRow,
deposits []sqlc.DepositsForSwapHashRow,
updates []sqlc.StaticAddressSwapUpdate) (*StaticAddressLoopIn, error) {
swapHash, err := lntypes.MakeHash(row.SwapHash)
swapHash, err := lntypes.MakeHash(swap.SwapHash)
if err != nil {
return nil, err
}
swapPreImage, err := lntypes.MakePreimage(row.Preimage)
swapPreImage, err := lntypes.MakePreimage(swap.Preimage)
if err != nil {
return nil, err
}
clientKey, err := btcec.ParsePubKey(row.SenderScriptPubkey)
clientKey, err := btcec.ParsePubKey(swap.SenderScriptPubkey)
if err != nil {
return nil, err
}
serverKey, err := btcec.ParsePubKey(row.ReceiverScriptPubkey)
serverKey, err := btcec.ParsePubKey(swap.ReceiverScriptPubkey)
if err != nil {
return nil, err
}
var htlcTimeoutSweepTxHash *chainhash.Hash
if row.HtlcTimeoutSweepTxID.Valid {
if swap.HtlcTimeoutSweepTxID.Valid {
htlcTimeoutSweepTxHash, err = chainhash.NewHashFromStr(
row.HtlcTimeoutSweepTxID.String,
swap.HtlcTimeoutSweepTxID.String,
)
if err != nil {
return nil, err
@ -342,10 +478,10 @@ func toStaticAddressLoopIn(_ context.Context, network *chaincfg.Params,
}
depositOutpoints := strings.Split(
row.DepositOutpoints, outpointSeparator,
swap.DepositOutpoints, outpointSeparator,
)
timeoutAddressString := row.HtlcTimeoutSweepAddress
timeoutAddressString := swap.HtlcTimeoutSweepAddress
var timeoutAddress btcutil.Address
if timeoutAddressString != "" {
timeoutAddress, err = btcutil.DecodeAddress(
@ -356,33 +492,68 @@ func toStaticAddressLoopIn(_ context.Context, network *chaincfg.Params,
}
}
depositList := make([]*deposit.Deposit, 0, len(deposits))
for _, d := range deposits {
id := deposit.ID{}
err = id.FromByteSlice(d.DepositID)
if err != nil {
return nil, err
}
sqlcDeposit := sqlc.Deposit{
DepositID: id[:],
TxHash: d.TxHash,
Amount: d.Amount,
OutIndex: d.OutIndex,
ConfirmationHeight: d.ConfirmationHeight,
TimeoutSweepPkScript: d.TimeoutSweepPkScript,
ExpirySweepTxid: d.ExpirySweepTxid,
FinalizedWithdrawalTx: d.FinalizedWithdrawalTx,
}
sqlcDepositUpdate := sqlc.DepositUpdate{
DepositID: id[:],
UpdateState: d.UpdateState.String,
UpdateTimestamp: d.UpdateTimestamp.Time,
}
deposit, err := deposit.ToDeposit(
sqlcDeposit, sqlcDepositUpdate,
)
if err != nil {
return nil, err
}
depositList = append(depositList, deposit)
}
loopIn := &StaticAddressLoopIn{
SwapHash: swapHash,
SwapPreimage: swapPreImage,
HtlcCltvExpiry: row.CltvExpiry,
MaxSwapFee: btcutil.Amount(row.MaxSwapFee),
InitiationHeight: uint32(row.InitiationHeight),
InitiationTime: row.InitiationTime,
HtlcCltvExpiry: swap.CltvExpiry,
MaxSwapFee: btcutil.Amount(swap.MaxSwapFee),
InitiationHeight: uint32(swap.InitiationHeight),
InitiationTime: swap.InitiationTime,
ProtocolVersion: version.AddressProtocolVersion(
row.ProtocolVersion,
swap.ProtocolVersion,
),
Label: row.Label,
Label: swap.Label,
ClientPubkey: clientKey,
ServerPubkey: serverKey,
HtlcKeyLocator: keychain.KeyLocator{
Family: keychain.KeyFamily(row.ClientKeyFamily),
Index: uint32(row.ClientKeyIndex),
Family: keychain.KeyFamily(swap.ClientKeyFamily),
Index: uint32(swap.ClientKeyIndex),
},
SwapInvoice: row.SwapInvoice,
PaymentTimeoutSeconds: uint32(row.PaymentTimeoutSeconds),
LastHop: row.LastHop,
QuotedSwapFee: btcutil.Amount(row.QuotedSwapFeeSatoshis),
SwapInvoice: swap.SwapInvoice,
PaymentTimeoutSeconds: uint32(swap.PaymentTimeoutSeconds),
LastHop: swap.LastHop,
QuotedSwapFee: btcutil.Amount(swap.QuotedSwapFeeSatoshis),
DepositOutpoints: depositOutpoints,
HtlcTxFeeRate: chainfee.SatPerKWeight(
row.HtlcTxFeeRateSatKw,
swap.HtlcTxFeeRateSatKw,
),
HtlcTimeoutSweepAddress: timeoutAddress,
HtlcTimeoutSweepTxHash: htlcTimeoutSweepTxHash,
Deposits: depositList,
}
if len(updates) > 0 {

View file

@ -0,0 +1,273 @@
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"
)
// TestGetStaticAddressLoopInSwapsByStates tests that we can retrieve
// StaticAddressLoopIn swaps by their states and that the deposits
// associated with the swaps are correctly populated.
func TestGetStaticAddressLoopInSwapsByStates(t *testing.T) {
// Set up test context objects.
ctxb := context.Background()
testDb := loopdb.NewTestDB(t)
testClock := clock.NewTestClock(time.Now())
defer testDb.Close()
depositStore := deposit.NewSqlStore(testDb.BaseDB)
swapStore := NewSqlStore(
loopdb.NewTypedStore[Querier](testDb), testClock,
&chaincfg.RegressionNetParams,
)
newID := func() deposit.ID {
did, err := deposit.GetRandomDepositID()
require.NoError(t, err)
return did
}
loopingDepositID := newID()
loopedInDepositID := newID()
d1, d2 := &deposit.Deposit{
ID: loopingDepositID,
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: loopedInDepositID,
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)
// Add two updates per deposit, expect the last to be retrieved.
d1.SetState(deposit.Deposited)
d2.SetState(deposit.Deposited)
err = depositStore.UpdateDeposit(ctxb, d1)
require.NoError(t, err)
err = depositStore.UpdateDeposit(ctxb, d2)
require.NoError(t, err)
d1.SetState(deposit.LoopingIn)
d2.SetState(deposit.LoopedIn)
err = depositStore.UpdateDeposit(ctxb, d1)
require.NoError(t, err)
err = depositStore.UpdateDeposit(ctxb, d2)
require.NoError(t, err)
_, clientPubKey := test.CreateKey(1)
_, serverPubKey := test.CreateKey(2)
addr, err := btcutil.DecodeAddress(P2wkhAddr, nil)
require.NoError(t, err)
// Create pending swap.
swapHashPending := lntypes.Hash{0x1, 0x2, 0x3, 0x4}
swapPending := StaticAddressLoopIn{
SwapHash: swapHashPending,
SwapPreimage: lntypes.Preimage{0x1, 0x2, 0x3, 0x4},
DepositOutpoints: []string{d1.OutPoint.String()},
Deposits: []*deposit.Deposit{d1},
ClientPubkey: clientPubKey,
ServerPubkey: serverPubKey,
HtlcTimeoutSweepAddress: addr,
}
swapPending.SetState(SignHtlcTx)
err = swapStore.CreateLoopIn(ctxb, &swapPending)
require.NoError(t, err)
// Create succeeded swap.
swapHashSucceeded := lntypes.Hash{0x2, 0x2, 0x3, 0x5}
swapSucceeded := StaticAddressLoopIn{
SwapHash: swapHashSucceeded,
SwapPreimage: lntypes.Preimage{0x2, 0x2, 0x3, 0x5},
DepositOutpoints: []string{d2.OutPoint.String()},
Deposits: []*deposit.Deposit{d2},
ClientPubkey: clientPubKey,
ServerPubkey: serverPubKey,
HtlcTimeoutSweepAddress: addr,
}
swapSucceeded.SetState(Succeeded)
err = swapStore.CreateLoopIn(ctxb, &swapSucceeded)
require.NoError(t, err)
pendingSwaps, err := swapStore.GetStaticAddressLoopInSwapsByStates(ctxb, PendingStates)
require.NoError(t, err)
require.Len(t, pendingSwaps, 1)
require.Equal(t, swapHashPending, pendingSwaps[0].SwapHash)
require.Equal(t, []string{d1.OutPoint.String()}, pendingSwaps[0].DepositOutpoints)
require.Equal(t, SignHtlcTx, pendingSwaps[0].GetState())
pendingDeposits := pendingSwaps[0].Deposits
require.Len(t, pendingDeposits, 1)
require.Equal(t, d1.ID, pendingDeposits[0].ID)
require.Equal(t, d1.OutPoint, pendingDeposits[0].OutPoint)
require.Equal(t, d1.Value, pendingDeposits[0].Value)
require.Equal(t, deposit.LoopingIn, pendingDeposits[0].GetState())
finalizedSwaps, err := swapStore.GetStaticAddressLoopInSwapsByStates(ctxb, FinalStates)
require.NoError(t, err)
require.Len(t, finalizedSwaps, 1)
require.Equal(t, swapHashSucceeded, finalizedSwaps[0].SwapHash)
require.Equal(t, []string{d2.OutPoint.String()}, finalizedSwaps[0].DepositOutpoints)
require.Equal(t, Succeeded, finalizedSwaps[0].GetState())
finalizedDeposits := finalizedSwaps[0].Deposits
require.Len(t, finalizedDeposits, 1)
require.Equal(t, d2.ID, finalizedDeposits[0].ID)
require.Equal(t, d2.OutPoint, finalizedDeposits[0].OutPoint)
require.Equal(t, d2.Value, finalizedDeposits[0].Value)
require.Equal(t, deposit.LoopedIn, finalizedDeposits[0].GetState())
}
// TestCreateLoopIn tests that CreateLoopIn correctly creates a new
// StaticAddressLoopIn swap and associates it with the provided deposits.
func TestCreateLoopIn(t *testing.T) {
// Set up test context objects.
ctxb := context.Background()
testDb := loopdb.NewTestDB(t)
testClock := clock.NewTestClock(time.Now())
defer testDb.Close()
depositStore := deposit.NewSqlStore(testDb.BaseDB)
swapStore := NewSqlStore(
loopdb.NewTypedStore[Querier](testDb), testClock,
&chaincfg.RegressionNetParams,
)
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)
d1.SetState(deposit.LoopingIn)
d2.SetState(deposit.LoopingIn)
err = depositStore.UpdateDeposit(ctxb, d1)
require.NoError(t, err)
err = depositStore.UpdateDeposit(ctxb, d2)
require.NoError(t, err)
_, clientPubKey := test.CreateKey(1)
_, serverPubKey := test.CreateKey(2)
addr, err := btcutil.DecodeAddress(P2wkhAddr, nil)
require.NoError(t, err)
// Create pending swap.
swapHashPending := lntypes.Hash{0x1, 0x2, 0x3, 0x4}
swapPending := StaticAddressLoopIn{
SwapHash: swapHashPending,
SwapPreimage: lntypes.Preimage{0x1, 0x2, 0x3, 0x4},
DepositOutpoints: []string{d1.OutPoint.String(),
d2.OutPoint.String()},
Deposits: []*deposit.Deposit{d1, d2},
ClientPubkey: clientPubKey,
ServerPubkey: serverPubKey,
HtlcTimeoutSweepAddress: addr,
}
swapPending.SetState(SignHtlcTx)
err = swapStore.CreateLoopIn(ctxb, &swapPending)
require.NoError(t, err)
depositIDs, err := swapStore.DepositIDsForSwapHash(
ctxb, swapHashPending,
)
require.NoError(t, err)
require.Len(t, depositIDs, 2)
require.Contains(t, depositIDs, d1.ID)
require.Contains(t, depositIDs, d2.ID)
swapHashes, err := swapStore.SwapHashesForDepositIDs(
ctxb, []deposit.ID{depositIDs[0], depositIDs[1]},
)
require.NoError(t, err)
require.Len(t, swapHashes, 1)
require.Len(t, swapHashes[swapHashPending], 2)
require.Contains(t, swapHashes[swapHashPending], depositIDs[0])
require.Contains(t, swapHashes[swapHashPending], depositIDs[1])
swap, err := swapStore.GetLoopInByHash(ctxb, swapHashPending)
require.NoError(t, err)
require.Equal(t, swapHashPending, swap.SwapHash)
require.Equal(t, []string{d1.OutPoint.String(), d2.OutPoint.String()},
swap.DepositOutpoints)
require.Equal(t, SignHtlcTx, swap.GetState())
require.Len(t, swap.Deposits, 2)
require.Equal(t, d1.ID, swap.Deposits[0].ID)
require.Equal(t, d1.OutPoint, swap.Deposits[0].OutPoint)
require.Equal(t, d1.Value, swap.Deposits[0].Value)
require.Equal(t, deposit.LoopingIn, swap.Deposits[0].GetState())
require.Equal(t, d2.ID, swap.Deposits[1].ID)
require.Equal(t, d2.OutPoint, swap.Deposits[1].OutPoint)
require.Equal(t, d2.Value, swap.Deposits[1].Value)
require.Equal(t, deposit.LoopingIn, swap.Deposits[1].GetState())
}