diff --git a/docs/release-notes/release-notes-next.md b/docs/release-notes/release-notes-next.md index 8cea37ddd..a569210e0 100644 --- a/docs/release-notes/release-notes-next.md +++ b/docs/release-notes/release-notes-next.md @@ -31,6 +31,11 @@ #### Maintenance +* Reserved distinct static-address receive and change key families and + persisted deposit address ownership metadata in preparation for multi-address + Static Address Loop In support. + [PR #1210](https://github.com/lightninglabs/loop/pull/1210) + * Updated the gRPC dependency to v1.83.1. * Updated the Taproot Assets dependency to v0.8.1; asset conversions that diff --git a/loopdb/migration_22_deposit_address_test.go b/loopdb/migration_22_deposit_address_test.go new file mode 100644 index 000000000..3b86f2530 --- /dev/null +++ b/loopdb/migration_22_deposit_address_test.go @@ -0,0 +1,101 @@ +package loopdb + +import ( + "context" + "database/sql" + "io/fs" + "net/http" + "path/filepath" + "testing" + "testing/fstest" + + "github.com/golang-migrate/migrate/v4" + sqlite_migrate "github.com/golang-migrate/migrate/v4/database/sqlite" + "github.com/golang-migrate/migrate/v4/source/httpfs" + "github.com/stretchr/testify/require" +) + +// TestDepositAddressBackfill verifies that migration 22 only assigns legacy +// deposits when their static-address owner is unambiguous. +func TestDepositAddressBackfill(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + addressCount int + wantOwner bool + }{ + {name: "no address", addressCount: 0}, + {name: "one address", addressCount: 1, wantOwner: true}, + {name: "multiple addresses", addressCount: 2}, + } + + for _, testCase := range tests { + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + db, err := sql.Open( + "sqlite", filepath.Join(t.TempDir(), "loop.db"), + ) + require.NoError(t, err) + t.Cleanup(func() { + require.NoError(t, db.Close()) + }) + + ctx := context.Background() + _, err = db.ExecContext(ctx, ` + CREATE TABLE static_addresses (id INTEGER PRIMARY KEY); + CREATE TABLE deposits (id INTEGER PRIMARY KEY);`) + require.NoError(t, err) + + migrationSQL, err := fs.ReadFile( + sqlSchemas, + "sqlc/migrations/000022_deposit_static_address_id.up.sql", + ) + require.NoError(t, err) + migrationFS := fstest.MapFS{ + "migrations/000001_deposit_address.up.sql": { + Data: migrationSQL, + }, + } + + driver, err := sqlite_migrate.WithInstance( + db, &sqlite_migrate.Config{}, + ) + require.NoError(t, err) + source, err := httpfs.New( + http.FS(migrationFS), "migrations", + ) + require.NoError(t, err) + migrator, err := migrate.NewWithInstance( + "migrations", source, "sqlc", driver, + ) + require.NoError(t, err) + + for i := 0; i < testCase.addressCount; i++ { + _, err := db.ExecContext(ctx, + "INSERT INTO static_addresses (id) VALUES (?)", + i+1, + ) + require.NoError(t, err) + } + + _, err = db.ExecContext(ctx, + "INSERT INTO deposits (id) VALUES (1)", + ) + require.NoError(t, err) + + require.NoError(t, migrator.Up()) + + var owner sql.NullInt64 + err = db.QueryRowContext(ctx, + "SELECT static_address_id FROM deposits", + ).Scan(&owner) + require.NoError(t, err) + require.Equal(t, testCase.wantOwner, owner.Valid) + if testCase.wantOwner { + require.EqualValues(t, 1, owner.Int64) + } + }) + } +} diff --git a/loopdb/sqlc/migrations/000022_deposit_static_address_id.down.sql b/loopdb/sqlc/migrations/000022_deposit_static_address_id.down.sql new file mode 100644 index 000000000..e112a7b1f --- /dev/null +++ b/loopdb/sqlc/migrations/000022_deposit_static_address_id.down.sql @@ -0,0 +1 @@ +ALTER TABLE deposits DROP COLUMN static_address_id; diff --git a/loopdb/sqlc/migrations/000022_deposit_static_address_id.up.sql b/loopdb/sqlc/migrations/000022_deposit_static_address_id.up.sql new file mode 100644 index 000000000..cc9676b88 --- /dev/null +++ b/loopdb/sqlc/migrations/000022_deposit_static_address_id.up.sql @@ -0,0 +1,8 @@ +ALTER TABLE deposits ADD static_address_id INT REFERENCES static_addresses(id); + +UPDATE deposits +SET static_address_id = ( + SELECT id FROM static_addresses ORDER BY id ASC LIMIT 1 +) +WHERE static_address_id IS NULL + AND (SELECT COUNT(*) FROM static_addresses) = 1; diff --git a/loopdb/sqlc/models.go b/loopdb/sqlc/models.go index 78a75d042..34a924256 100644 --- a/loopdb/sqlc/models.go +++ b/loopdb/sqlc/models.go @@ -20,6 +20,7 @@ type Deposit struct { ExpirySweepTxid []byte FinalizedWithdrawalTx sql.NullString SwapHash []byte + StaticAddressID sql.NullInt32 } type DepositUpdate struct { diff --git a/loopdb/sqlc/querier.go b/loopdb/sqlc/querier.go index ba3c35eb9..8bdc95508 100644 --- a/loopdb/sqlc/querier.go +++ b/loopdb/sqlc/querier.go @@ -11,6 +11,7 @@ import ( type Querier interface { AllDeposits(ctx context.Context) ([]Deposit, error) + AllDepositsWithAddress(ctx context.Context) ([]AllDepositsWithAddressRow, error) AllStaticAddresses(ctx context.Context) ([]StaticAddress, error) CancelBatch(ctx context.Context, id int32) error CreateDeposit(ctx context.Context, arg CreateDepositParams) error @@ -19,6 +20,7 @@ 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) + DepositForOutpointWithAddress(ctx context.Context, arg DepositForOutpointWithAddressParams) (DepositForOutpointWithAddressRow, error) DepositIDsForSwapHash(ctx context.Context, swapHash []byte) ([][]byte, error) DepositsForSwapHash(ctx context.Context, swapHash []byte) ([]DepositsForSwapHashRow, error) FetchLiquidityParams(ctx context.Context) ([]byte, error) @@ -26,11 +28,13 @@ type Querier interface { GetBatchSweeps(ctx context.Context, batchID int32) ([]Sweep, error) GetBatchSweptAmount(ctx context.Context, batchID int32) (int64, error) GetDeposit(ctx context.Context, depositID []byte) (Deposit, error) + GetDepositWithAddress(ctx context.Context, depositID []byte) (GetDepositWithAddressRow, error) 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) GetLatestDepositUpdate(ctx context.Context, depositID []byte) (DepositUpdate, error) + GetLegacyAddress(ctx context.Context) (StaticAddress, error) GetLoopInSwap(ctx context.Context, swapHash []byte) (GetLoopInSwapRow, error) GetLoopInSwapUpdates(ctx context.Context, swapHash []byte) ([]StaticAddressSwapUpdate, error) GetLoopInSwaps(ctx context.Context) ([]GetLoopInSwapsRow, error) @@ -42,6 +46,7 @@ type Querier interface { GetReservationUpdates(ctx context.Context, reservationID []byte) ([]ReservationUpdate, error) GetReservations(ctx context.Context) ([]Reservation, error) GetStaticAddress(ctx context.Context, pkscript []byte) (StaticAddress, error) + GetStaticAddressID(ctx context.Context, pkscript []byte) (int32, error) GetStaticAddressLoopInSwap(ctx context.Context, swapHash []byte) (GetStaticAddressLoopInSwapRow, error) GetStaticAddressLoopInSwapsByStates(ctx context.Context, dollar_1 sql.NullString) ([]GetStaticAddressLoopInSwapsByStatesRow, error) GetSwapUpdates(ctx context.Context, swapHash []byte) ([]SwapUpdate, error) diff --git a/loopdb/sqlc/queries/static_address_deposits.sql b/loopdb/sqlc/queries/static_address_deposits.sql index 2987e469e..8d7463bf1 100644 --- a/loopdb/sqlc/queries/static_address_deposits.sql +++ b/loopdb/sqlc/queries/static_address_deposits.sql @@ -7,7 +7,8 @@ INSERT INTO deposits ( confirmation_height, timeout_sweep_pk_script, expiry_sweep_txid, - finalized_withdrawal_tx + finalized_withdrawal_tx, + static_address_id ) VALUES ( $1, $2, @@ -16,7 +17,8 @@ INSERT INTO deposits ( $5, $6, $7, - $8 + $8, + $9 ); -- name: UpdateDeposit :exec @@ -42,30 +44,66 @@ INSERT INTO deposit_updates ( ); -- name: GetDeposit :one +SELECT * FROM deposits WHERE deposit_id = $1; + +-- name: GetDepositWithAddress :one SELECT - * + d.*, + sa.client_pubkey client_pubkey, + sa.server_pubkey server_pubkey, + sa.expiry expiry, + sa.client_key_family client_key_family, + sa.client_key_index client_key_index, + sa.pkscript pkscript, + sa.protocol_version protocol_version, + sa.initiation_height initiation_height FROM - deposits + deposits d + LEFT JOIN static_addresses sa ON sa.id = d.static_address_id WHERE deposit_id = $1; -- name: DepositForOutpoint :one +SELECT * FROM deposits WHERE tx_hash = $1 AND out_index = $2; + +-- name: DepositForOutpointWithAddress :one SELECT - * + d.*, + sa.client_pubkey client_pubkey, + sa.server_pubkey server_pubkey, + sa.expiry expiry, + sa.client_key_family client_key_family, + sa.client_key_index client_key_index, + sa.pkscript pkscript, + sa.protocol_version protocol_version, + sa.initiation_height initiation_height FROM - deposits + deposits d + LEFT JOIN static_addresses sa ON sa.id = d.static_address_id WHERE tx_hash = $1 AND out_index = $2; -- name: AllDeposits :many +SELECT * FROM deposits ORDER BY id ASC; + +-- name: AllDepositsWithAddress :many SELECT - * + d.*, + sa.client_pubkey client_pubkey, + sa.server_pubkey server_pubkey, + sa.expiry expiry, + sa.client_key_family client_key_family, + sa.client_key_index client_key_index, + sa.pkscript pkscript, + sa.protocol_version protocol_version, + sa.initiation_height initiation_height FROM - deposits + deposits d + LEFT JOIN static_addresses sa ON sa.id = d.static_address_id ORDER BY - id ASC; + d.id ASC; -- name: GetLatestDepositUpdate :one SELECT @@ -76,4 +114,4 @@ WHERE deposit_id = $1 ORDER BY update_timestamp DESC -LIMIT 1; \ No newline at end of file +LIMIT 1; diff --git a/loopdb/sqlc/queries/static_address_loopin.sql b/loopdb/sqlc/queries/static_address_loopin.sql index b4fca5d45..ecd252f6d 100644 --- a/loopdb/sqlc/queries/static_address_loopin.sql +++ b/loopdb/sqlc/queries/static_address_loopin.sql @@ -147,10 +147,19 @@ WHERE -- name: DepositsForSwapHash :many SELECT d.*, + sa.client_pubkey client_pubkey, + sa.server_pubkey server_pubkey, + sa.expiry expiry, + sa.client_key_family client_key_family, + sa.client_key_index client_key_index, + sa.pkscript pkscript, + sa.protocol_version protocol_version, + sa.initiation_height initiation_height, u.update_state, u.update_timestamp FROM deposits d + LEFT JOIN static_addresses sa ON sa.id = d.static_address_id LEFT JOIN deposit_updates u ON u.id = ( SELECT id @@ -162,4 +171,3 @@ FROM WHERE d.swap_hash = $1; - diff --git a/loopdb/sqlc/queries/static_addresses.sql b/loopdb/sqlc/queries/static_addresses.sql index c613cfd93..cc86fa7e2 100644 --- a/loopdb/sqlc/queries/static_addresses.sql +++ b/loopdb/sqlc/queries/static_addresses.sql @@ -1,10 +1,15 @@ -- name: AllStaticAddresses :many -SELECT * FROM static_addresses; +SELECT * FROM static_addresses +ORDER BY id ASC; -- name: GetStaticAddress :one SELECT * FROM static_addresses WHERE pkscript=$1; +-- name: GetStaticAddressID :one +SELECT id FROM static_addresses +WHERE pkscript=$1; + -- name: CreateStaticAddress :exec INSERT INTO static_addresses ( client_pubkey, @@ -24,4 +29,9 @@ INSERT INTO static_addresses ( $6, $7, $8 - ); \ No newline at end of file + ); + +-- name: GetLegacyAddress :one +SELECT * FROM static_addresses +ORDER BY id ASC +LIMIT 1; diff --git a/loopdb/sqlc/static_address_deposits.sql.go b/loopdb/sqlc/static_address_deposits.sql.go index 191f1f563..c4883997d 100644 --- a/loopdb/sqlc/static_address_deposits.sql.go +++ b/loopdb/sqlc/static_address_deposits.sql.go @@ -12,12 +12,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, swap_hash -FROM - deposits -ORDER BY - id ASC +SELECT id, deposit_id, tx_hash, out_index, amount, confirmation_height, timeout_sweep_pk_script, expiry_sweep_txid, finalized_withdrawal_tx, swap_hash, static_address_id FROM deposits ORDER BY id ASC ` func (q *Queries) AllDeposits(ctx context.Context) ([]Deposit, error) { @@ -40,6 +35,90 @@ func (q *Queries) AllDeposits(ctx context.Context) ([]Deposit, error) { &i.ExpirySweepTxid, &i.FinalizedWithdrawalTx, &i.SwapHash, + &i.StaticAddressID, + ); 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 allDepositsWithAddress = `-- name: AllDepositsWithAddress :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, d.static_address_id, + sa.client_pubkey client_pubkey, + sa.server_pubkey server_pubkey, + sa.expiry expiry, + sa.client_key_family client_key_family, + sa.client_key_index client_key_index, + sa.pkscript pkscript, + sa.protocol_version protocol_version, + sa.initiation_height initiation_height +FROM + deposits d + LEFT JOIN static_addresses sa ON sa.id = d.static_address_id +ORDER BY + d.id ASC +` + +type AllDepositsWithAddressRow struct { + ID int32 + DepositID []byte + TxHash []byte + OutIndex int32 + Amount int64 + ConfirmationHeight int64 + TimeoutSweepPkScript []byte + ExpirySweepTxid []byte + FinalizedWithdrawalTx sql.NullString + SwapHash []byte + StaticAddressID sql.NullInt32 + ClientPubkey []byte + ServerPubkey []byte + Expiry sql.NullInt32 + ClientKeyFamily sql.NullInt32 + ClientKeyIndex sql.NullInt32 + Pkscript []byte + ProtocolVersion sql.NullInt32 + InitiationHeight sql.NullInt32 +} + +func (q *Queries) AllDepositsWithAddress(ctx context.Context) ([]AllDepositsWithAddressRow, error) { + rows, err := q.db.QueryContext(ctx, allDepositsWithAddress) + if err != nil { + return nil, err + } + defer rows.Close() + var items []AllDepositsWithAddressRow + for rows.Next() { + var i AllDepositsWithAddressRow + 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.StaticAddressID, + &i.ClientPubkey, + &i.ServerPubkey, + &i.Expiry, + &i.ClientKeyFamily, + &i.ClientKeyIndex, + &i.Pkscript, + &i.ProtocolVersion, + &i.InitiationHeight, ); err != nil { return nil, err } @@ -63,7 +142,8 @@ INSERT INTO deposits ( confirmation_height, timeout_sweep_pk_script, expiry_sweep_txid, - finalized_withdrawal_tx + finalized_withdrawal_tx, + static_address_id ) VALUES ( $1, $2, @@ -72,7 +152,8 @@ INSERT INTO deposits ( $5, $6, $7, - $8 + $8, + $9 ) ` @@ -85,6 +166,7 @@ type CreateDepositParams struct { TimeoutSweepPkScript []byte ExpirySweepTxid []byte FinalizedWithdrawalTx sql.NullString + StaticAddressID sql.NullInt32 } func (q *Queries) CreateDeposit(ctx context.Context, arg CreateDepositParams) error { @@ -97,29 +179,89 @@ func (q *Queries) CreateDeposit(ctx context.Context, arg CreateDepositParams) er arg.TimeoutSweepPkScript, arg.ExpirySweepTxid, arg.FinalizedWithdrawalTx, + arg.StaticAddressID, ) return err } 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, swap_hash, static_address_id FROM deposits WHERE tx_hash = $1 AND out_index = $2 +` + +type DepositForOutpointParams struct { + TxHash []byte + OutIndex int32 +} + +func (q *Queries) DepositForOutpoint(ctx context.Context, arg DepositForOutpointParams) (Deposit, error) { + row := q.db.QueryRowContext(ctx, depositForOutpoint, arg.TxHash, arg.OutIndex) + var i Deposit + err := row.Scan( + &i.ID, + &i.DepositID, + &i.TxHash, + &i.OutIndex, + &i.Amount, + &i.ConfirmationHeight, + &i.TimeoutSweepPkScript, + &i.ExpirySweepTxid, + &i.FinalizedWithdrawalTx, + &i.SwapHash, + &i.StaticAddressID, + ) + return i, err +} + +const depositForOutpointWithAddress = `-- name: DepositForOutpointWithAddress :one SELECT - id, deposit_id, tx_hash, out_index, amount, confirmation_height, timeout_sweep_pk_script, expiry_sweep_txid, finalized_withdrawal_tx, swap_hash + 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, d.static_address_id, + sa.client_pubkey client_pubkey, + sa.server_pubkey server_pubkey, + sa.expiry expiry, + sa.client_key_family client_key_family, + sa.client_key_index client_key_index, + sa.pkscript pkscript, + sa.protocol_version protocol_version, + sa.initiation_height initiation_height FROM - deposits + deposits d + LEFT JOIN static_addresses sa ON sa.id = d.static_address_id WHERE tx_hash = $1 AND out_index = $2 ` -type DepositForOutpointParams struct { +type DepositForOutpointWithAddressParams struct { TxHash []byte OutIndex int32 } -func (q *Queries) DepositForOutpoint(ctx context.Context, arg DepositForOutpointParams) (Deposit, error) { - row := q.db.QueryRowContext(ctx, depositForOutpoint, arg.TxHash, arg.OutIndex) - var i Deposit +type DepositForOutpointWithAddressRow struct { + ID int32 + DepositID []byte + TxHash []byte + OutIndex int32 + Amount int64 + ConfirmationHeight int64 + TimeoutSweepPkScript []byte + ExpirySweepTxid []byte + FinalizedWithdrawalTx sql.NullString + SwapHash []byte + StaticAddressID sql.NullInt32 + ClientPubkey []byte + ServerPubkey []byte + Expiry sql.NullInt32 + ClientKeyFamily sql.NullInt32 + ClientKeyIndex sql.NullInt32 + Pkscript []byte + ProtocolVersion sql.NullInt32 + InitiationHeight sql.NullInt32 +} + +func (q *Queries) DepositForOutpointWithAddress(ctx context.Context, arg DepositForOutpointWithAddressParams) (DepositForOutpointWithAddressRow, error) { + row := q.db.QueryRowContext(ctx, depositForOutpointWithAddress, arg.TxHash, arg.OutIndex) + var i DepositForOutpointWithAddressRow err := row.Scan( &i.ID, &i.DepositID, @@ -131,22 +273,85 @@ func (q *Queries) DepositForOutpoint(ctx context.Context, arg DepositForOutpoint &i.ExpirySweepTxid, &i.FinalizedWithdrawalTx, &i.SwapHash, + &i.StaticAddressID, + &i.ClientPubkey, + &i.ServerPubkey, + &i.Expiry, + &i.ClientKeyFamily, + &i.ClientKeyIndex, + &i.Pkscript, + &i.ProtocolVersion, + &i.InitiationHeight, ) 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, swap_hash, static_address_id FROM deposits WHERE deposit_id = $1 +` + +func (q *Queries) GetDeposit(ctx context.Context, depositID []byte) (Deposit, error) { + row := q.db.QueryRowContext(ctx, getDeposit, depositID) + var i Deposit + err := row.Scan( + &i.ID, + &i.DepositID, + &i.TxHash, + &i.OutIndex, + &i.Amount, + &i.ConfirmationHeight, + &i.TimeoutSweepPkScript, + &i.ExpirySweepTxid, + &i.FinalizedWithdrawalTx, + &i.SwapHash, + &i.StaticAddressID, + ) + return i, err +} + +const getDepositWithAddress = `-- name: GetDepositWithAddress :one SELECT - id, deposit_id, tx_hash, out_index, amount, confirmation_height, timeout_sweep_pk_script, expiry_sweep_txid, finalized_withdrawal_tx, swap_hash + 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, d.static_address_id, + sa.client_pubkey client_pubkey, + sa.server_pubkey server_pubkey, + sa.expiry expiry, + sa.client_key_family client_key_family, + sa.client_key_index client_key_index, + sa.pkscript pkscript, + sa.protocol_version protocol_version, + sa.initiation_height initiation_height FROM - deposits + deposits d + LEFT JOIN static_addresses sa ON sa.id = d.static_address_id WHERE deposit_id = $1 ` -func (q *Queries) GetDeposit(ctx context.Context, depositID []byte) (Deposit, error) { - row := q.db.QueryRowContext(ctx, getDeposit, depositID) - var i Deposit +type GetDepositWithAddressRow struct { + ID int32 + DepositID []byte + TxHash []byte + OutIndex int32 + Amount int64 + ConfirmationHeight int64 + TimeoutSweepPkScript []byte + ExpirySweepTxid []byte + FinalizedWithdrawalTx sql.NullString + SwapHash []byte + StaticAddressID sql.NullInt32 + ClientPubkey []byte + ServerPubkey []byte + Expiry sql.NullInt32 + ClientKeyFamily sql.NullInt32 + ClientKeyIndex sql.NullInt32 + Pkscript []byte + ProtocolVersion sql.NullInt32 + InitiationHeight sql.NullInt32 +} + +func (q *Queries) GetDepositWithAddress(ctx context.Context, depositID []byte) (GetDepositWithAddressRow, error) { + row := q.db.QueryRowContext(ctx, getDepositWithAddress, depositID) + var i GetDepositWithAddressRow err := row.Scan( &i.ID, &i.DepositID, @@ -158,6 +363,15 @@ func (q *Queries) GetDeposit(ctx context.Context, depositID []byte) (Deposit, er &i.ExpirySweepTxid, &i.FinalizedWithdrawalTx, &i.SwapHash, + &i.StaticAddressID, + &i.ClientPubkey, + &i.ServerPubkey, + &i.Expiry, + &i.ClientKeyFamily, + &i.ClientKeyIndex, + &i.Pkscript, + &i.ProtocolVersion, + &i.InitiationHeight, ) return i, err } diff --git a/loopdb/sqlc/static_address_loopin.sql.go b/loopdb/sqlc/static_address_loopin.sql.go index 319340168..8cb2aef6d 100644 --- a/loopdb/sqlc/static_address_loopin.sql.go +++ b/loopdb/sqlc/static_address_loopin.sql.go @@ -45,11 +45,20 @@ func (q *Queries) DepositIDsForSwapHash(ctx context.Context, swapHash []byte) ([ 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, + 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, d.static_address_id, + sa.client_pubkey client_pubkey, + sa.server_pubkey server_pubkey, + sa.expiry expiry, + sa.client_key_family client_key_family, + sa.client_key_index client_key_index, + sa.pkscript pkscript, + sa.protocol_version protocol_version, + sa.initiation_height initiation_height, u.update_state, u.update_timestamp FROM deposits d + LEFT JOIN static_addresses sa ON sa.id = d.static_address_id LEFT JOIN deposit_updates u ON u.id = ( SELECT id @@ -73,6 +82,15 @@ type DepositsForSwapHashRow struct { ExpirySweepTxid []byte FinalizedWithdrawalTx sql.NullString SwapHash []byte + StaticAddressID sql.NullInt32 + ClientPubkey []byte + ServerPubkey []byte + Expiry sql.NullInt32 + ClientKeyFamily sql.NullInt32 + ClientKeyIndex sql.NullInt32 + Pkscript []byte + ProtocolVersion sql.NullInt32 + InitiationHeight sql.NullInt32 UpdateState sql.NullString UpdateTimestamp sql.NullTime } @@ -97,6 +115,15 @@ func (q *Queries) DepositsForSwapHash(ctx context.Context, swapHash []byte) ([]D &i.ExpirySweepTxid, &i.FinalizedWithdrawalTx, &i.SwapHash, + &i.StaticAddressID, + &i.ClientPubkey, + &i.ServerPubkey, + &i.Expiry, + &i.ClientKeyFamily, + &i.ClientKeyIndex, + &i.Pkscript, + &i.ProtocolVersion, + &i.InitiationHeight, &i.UpdateState, &i.UpdateTimestamp, ); err != nil { diff --git a/loopdb/sqlc/static_addresses.sql.go b/loopdb/sqlc/static_addresses.sql.go index 054c07364..dbdb0e271 100644 --- a/loopdb/sqlc/static_addresses.sql.go +++ b/loopdb/sqlc/static_addresses.sql.go @@ -11,6 +11,7 @@ import ( const allStaticAddresses = `-- name: AllStaticAddresses :many SELECT id, client_pubkey, server_pubkey, expiry, client_key_family, client_key_index, pkscript, protocol_version, initiation_height FROM static_addresses +ORDER BY id ASC ` func (q *Queries) AllStaticAddresses(ctx context.Context) ([]StaticAddress, error) { @@ -93,6 +94,29 @@ func (q *Queries) CreateStaticAddress(ctx context.Context, arg CreateStaticAddre return err } +const getLegacyAddress = `-- name: GetLegacyAddress :one +SELECT id, client_pubkey, server_pubkey, expiry, client_key_family, client_key_index, pkscript, protocol_version, initiation_height FROM static_addresses +ORDER BY id ASC +LIMIT 1 +` + +func (q *Queries) GetLegacyAddress(ctx context.Context) (StaticAddress, error) { + row := q.db.QueryRowContext(ctx, getLegacyAddress) + var i StaticAddress + err := row.Scan( + &i.ID, + &i.ClientPubkey, + &i.ServerPubkey, + &i.Expiry, + &i.ClientKeyFamily, + &i.ClientKeyIndex, + &i.Pkscript, + &i.ProtocolVersion, + &i.InitiationHeight, + ) + return i, err +} + const getStaticAddress = `-- name: GetStaticAddress :one SELECT id, client_pubkey, server_pubkey, expiry, client_key_family, client_key_index, pkscript, protocol_version, initiation_height FROM static_addresses WHERE pkscript=$1 @@ -114,3 +138,15 @@ func (q *Queries) GetStaticAddress(ctx context.Context, pkscript []byte) (Static ) return i, err } + +const getStaticAddressID = `-- name: GetStaticAddressID :one +SELECT id FROM static_addresses +WHERE pkscript=$1 +` + +func (q *Queries) GetStaticAddressID(ctx context.Context, pkscript []byte) (int32, error) { + row := q.db.QueryRowContext(ctx, getStaticAddressID, pkscript) + var id int32 + err := row.Scan(&id) + return id, err +} diff --git a/staticaddr/address/manager_test.go b/staticaddr/address/manager_test.go index b7bbf79ae..d88181d6c 100644 --- a/staticaddr/address/manager_test.go +++ b/staticaddr/address/manager_test.go @@ -132,6 +132,13 @@ func TestManager(t *testing.T) { // The expiry has to match. require.EqualValues(t, defaultExpiry, expiry) + + // A newly created address must be immediately usable as the owner of a + // deposit in this daemon session. In particular, its database-assigned + // ID must be available without requiring a restart and reload. + params, err := testContext.manager.GetStaticAddressParameters(ctxb) + require.NoError(t, err) + require.Positive(t, params.ID) } // TestNewAddressValidatesServerResponse tests that the untrusted diff --git a/staticaddr/address/sql_store.go b/staticaddr/address/sql_store.go index 43257b81d..9092aaec5 100644 --- a/staticaddr/address/sql_store.go +++ b/staticaddr/address/sql_store.go @@ -39,10 +39,39 @@ func (s *SqlStore) CreateStaticAddress(ctx context.Context, InitiationHeight: addrParams.InitiationHeight, } - return s.baseDB.Queries.CreateStaticAddress(ctx, createArgs) + var addressID int32 + err := s.baseDB.ExecTx(ctx, &loopdb.SqliteTxOptions{}, + func(q *sqlc.Queries) error { + err := q.CreateStaticAddress(ctx, createArgs) + if err != nil { + return err + } + + addressID, err = q.GetStaticAddressID( + ctx, addrParams.PkScript, + ) + return err + }) + if err != nil { + return err + } + + // Keep the in-memory parameters consistent with rows loaded back from + // the database. Callers can safely attach them to a deposit immediately + // after creating an address, without requiring a daemon restart. + addrParams.ID = addressID + + return nil } -// GetAllStaticAddresses returns all address known to the server. +// GetStaticAddressID retrieves the database ID for a static address script. +func (s *SqlStore) GetStaticAddressID(ctx context.Context, + pkScript []byte) (int32, error) { + + return s.baseDB.Queries.GetStaticAddressID(ctx, pkScript) +} + +// GetAllStaticAddresses returns all addresses known to the client. func (s *SqlStore) GetAllStaticAddresses(ctx context.Context) ( []*script.Parameters, error) { @@ -64,6 +93,18 @@ func (s *SqlStore) GetAllStaticAddresses(ctx context.Context) ( return result, nil } +// GetLegacyParameters returns the first static address created for this L402. +func (s *SqlStore) GetLegacyParameters(ctx context.Context) ( + *script.Parameters, error) { + + staticAddress, err := s.baseDB.Queries.GetLegacyAddress(ctx) + if err != nil { + return nil, err + } + + return s.toAddressParameters(staticAddress) +} + // toAddressParameters transforms a database representation of a static address // to an AddressParameters struct. func (s *SqlStore) toAddressParameters(row sqlc.StaticAddress) ( @@ -80,6 +121,7 @@ func (s *SqlStore) toAddressParameters(row sqlc.StaticAddress) ( } return &script.Parameters{ + ID: row.ID, ClientPubkey: clientPubkey, ServerPubkey: serverPubkey, PkScript: row.Pkscript, diff --git a/staticaddr/address/sql_store_test.go b/staticaddr/address/sql_store_test.go new file mode 100644 index 000000000..2508015c2 --- /dev/null +++ b/staticaddr/address/sql_store_test.go @@ -0,0 +1,44 @@ +package address + +import ( + "testing" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/lightninglabs/loop/loopdb" + "github.com/lightninglabs/loop/staticaddr/script" + "github.com/lightninglabs/loop/staticaddr/version" + "github.com/lightningnetwork/lnd/keychain" + "github.com/stretchr/testify/require" +) + +// TestCreateStaticAddressSetsID verifies that address parameters are ready to +// own deposits immediately after they are persisted. +func TestCreateStaticAddressSetsID(t *testing.T) { + clientKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + serverKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + + params := &script.Parameters{ + ClientPubkey: clientKey.PubKey(), + ServerPubkey: serverKey.PubKey(), + PkScript: []byte{0x51}, + Expiry: 144, + KeyLocator: keychain.KeyLocator{ + Family: 1, + Index: 2, + }, + ProtocolVersion: version.ProtocolVersion_V0, + InitiationHeight: 100, + } + + db := loopdb.NewTestDB(t) + store := NewSqlStore(db.BaseDB) + require.NoError(t, store.CreateStaticAddress(t.Context(), params)) + require.Positive(t, params.ID) + + storedParams, err := store.GetAllStaticAddresses(t.Context()) + require.NoError(t, err) + require.Len(t, storedParams, 1) + require.Equal(t, params.ID, storedParams[0].ID) +} diff --git a/staticaddr/deposit/deposit.go b/staticaddr/deposit/deposit.go index d63cc4b74..5b85d61ff 100644 --- a/staticaddr/deposit/deposit.go +++ b/staticaddr/deposit/deposit.go @@ -1,6 +1,7 @@ package deposit import ( + "bytes" "crypto/rand" "fmt" "sync" @@ -9,6 +10,9 @@ import ( "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/wire" "github.com/lightninglabs/loop/fsm" + "github.com/lightninglabs/loop/staticaddr/script" + "github.com/lightninglabs/loop/staticaddr/version" + "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/lntypes" ) @@ -70,6 +74,11 @@ type Deposit struct { // FinalizedWithdrawalTx is the coop-signed withdrawal transaction. It // is republished on new block arrivals and on client restarts. FinalizedWithdrawalTx *wire.MsgTx + + // AddressParams are the static address parameters that produced this + // deposit's pkScript. Spending code must use these per-deposit + // parameters rather than assuming all deposits belong to one address. + AddressParams *script.Parameters } // IsInFinalState returns true if the deposit is final. @@ -152,6 +161,43 @@ func (d *Deposit) GetConfirmationHeightNoLock() int64 { return d.ConfirmationHeight } +// GetStaticAddressScript reconstructs the static address script for this +// deposit's matched address parameters. +func (d *Deposit) GetStaticAddressScript() (*script.StaticAddress, error) { + if d.AddressParams == nil { + return nil, fmt.Errorf("missing static address parameters") + } + + var muSig2Version input.MuSig2Version + switch d.AddressParams.ProtocolVersion { + case version.ProtocolVersion_V0: + muSig2Version = input.MuSig2Version100RC2 + + default: + return nil, fmt.Errorf("unsupported static address protocol version: %v", + d.AddressParams.ProtocolVersion) + } + + staticAddress, err := script.NewStaticAddress( + muSig2Version, int64(d.AddressParams.Expiry), + d.AddressParams.ClientPubkey, d.AddressParams.ServerPubkey, + ) + if err != nil { + return nil, err + } + + pkScript, err := staticAddress.StaticAddressScript() + if err != nil { + return nil, err + } + if !bytes.Equal(pkScript, d.AddressParams.PkScript) { + return nil, fmt.Errorf("reconstructed static address script does not " + + "match persisted pkScript") + } + + return staticAddress, nil +} + // GetRandomDepositID generates a random deposit ID. func GetRandomDepositID() (ID, error) { var id ID diff --git a/staticaddr/deposit/deposit_test.go b/staticaddr/deposit/deposit_test.go index 3215e116a..3740535e3 100644 --- a/staticaddr/deposit/deposit_test.go +++ b/staticaddr/deposit/deposit_test.go @@ -3,9 +3,44 @@ package deposit import ( "testing" + "github.com/lightninglabs/loop/staticaddr/script" + "github.com/lightninglabs/loop/staticaddr/version" + "github.com/lightninglabs/loop/swap" + "github.com/lightninglabs/loop/test" + "github.com/lightningnetwork/lnd/input" + "github.com/lightningnetwork/lnd/keychain" "github.com/stretchr/testify/require" ) +func testAddressParameters(t *testing.T, id int32) *script.Parameters { + t.Helper() + + _, clientKey := test.CreateKey(id) + _, serverKey := test.CreateKey(id + 100) + staticAddress, err := script.NewStaticAddress( + input.MuSig2Version100RC2, int64(defaultExpiry), clientKey, + serverKey, + ) + require.NoError(t, err) + + pkScript, err := staticAddress.StaticAddressScript() + require.NoError(t, err) + + return &script.Parameters{ + ID: id, + ClientPubkey: clientKey, + ServerPubkey: serverKey, + PkScript: pkScript, + Expiry: defaultExpiry, + KeyLocator: keychain.KeyLocator{ + Family: keychain.KeyFamily(swap.StaticAddressKeyFamily), + Index: uint32(id), + }, + ProtocolVersion: version.ProtocolVersion_V0, + InitiationHeight: 100, + } +} + // TestDepositIsExpiredUnconfirmed verifies that unconfirmed deposits do not // expire because their CSV timeout has not started yet. func TestDepositIsExpiredUnconfirmed(t *testing.T) { @@ -15,3 +50,21 @@ func TestDepositIsExpiredUnconfirmed(t *testing.T) { require.False(t, d.IsExpired(1_000, 144)) } + +func TestGetStaticAddressScriptValidatesPersistedScript(t *testing.T) { + t.Parallel() + + params := testAddressParameters(t, 1) + d := &Deposit{AddressParams: params} + + _, err := d.GetStaticAddressScript() + require.NoError(t, err) + + d.AddressParams.PkScript = []byte{0x51} + _, err = d.GetStaticAddressScript() + require.ErrorContains(t, err, "does not match persisted pkScript") + + d.AddressParams.ProtocolVersion = 999 + _, err = d.GetStaticAddressScript() + require.ErrorContains(t, err, "unsupported static address protocol version") +} diff --git a/staticaddr/deposit/fsm.go b/staticaddr/deposit/fsm.go index c5bb85c30..ea02e7a1d 100644 --- a/staticaddr/deposit/fsm.go +++ b/staticaddr/deposit/fsm.go @@ -181,13 +181,12 @@ func NewFSM(ctx context.Context, deposit *Deposit, cfg *ManagerConfig, finalizedDepositChan chan wire.OutPoint, recoverStateMachine bool) (*FSM, error) { - params, err := cfg.AddressManager.GetStaticAddressParameters(ctx) - if err != nil { - return nil, fmt.Errorf("unable to get static address "+ - "parameters: %w", err) + if deposit.AddressParams == nil { + return nil, fmt.Errorf("missing deposit static address parameters") } + params := deposit.AddressParams - address, err := cfg.AddressManager.GetStaticAddress(ctx) + address, err := deposit.GetStaticAddressScript() if err != nil { return nil, fmt.Errorf("unable to get static address: %w", err) } diff --git a/staticaddr/deposit/manager.go b/staticaddr/deposit/manager.go index 61fc8e769..ef8928e7a 100644 --- a/staticaddr/deposit/manager.go +++ b/staticaddr/deposit/manager.go @@ -231,6 +231,18 @@ func (m *Manager) recoverDeposits(ctx context.Context) error { continue } + // Deposits without an owning static address are deliberately left + // unassigned by the ownership migration when that ownership is + // ambiguous. Keep these deposits in the known-deposit set so they + // aren't rediscovered, but don't let one ambiguous legacy row stop + // recovery of every other deposit. + if d.AddressParams == nil { + log.Warnf("Quarantining deposit %x: static address "+ + "ownership is missing", d.ID) + + continue + } + log.Debugf("Recovering deposit %x", d.ID) // Create a state machine for a given deposit. @@ -376,6 +388,20 @@ func (m *Manager) createNewDeposit(ctx context.Context, if err != nil { return nil, err } + + addressParams, err := m.cfg.AddressManager. + GetStaticAddressParameters(ctx) + if err != nil { + return nil, fmt.Errorf("unable to get static address parameters: %w", + err) + } + if addressParams == nil { + return nil, fmt.Errorf("missing static address parameters") + } + if addressParams.ID <= 0 { + return nil, fmt.Errorf("missing static address ID") + } + deposit := &Deposit{ ID: id, state: Deposited, @@ -383,6 +409,7 @@ func (m *Manager) createNewDeposit(ctx context.Context, Value: utxo.Value, ConfirmationHeight: confirmationHeight, TimeOutSweepPkScript: timeoutSweepPkScript, + AddressParams: addressParams, } err = m.cfg.Store.CreateDeposit(ctx, deposit) @@ -511,6 +538,13 @@ func (m *Manager) syncActiveDeposits(ctx context.Context, continue } + // Ownerless legacy deposits are quarantined during recovery. + // Avoid trying to reactivate them on every reconciliation, + // which would otherwise make all future reconciliations fail. + if deposit.AddressParams == nil { + continue + } + toActivate = append(toActivate, deposit) } diff --git a/staticaddr/deposit/manager_reconcile_test.go b/staticaddr/deposit/manager_reconcile_test.go index 15b2f0a69..e9065a7ec 100644 --- a/staticaddr/deposit/manager_reconcile_test.go +++ b/staticaddr/deposit/manager_reconcile_test.go @@ -2,7 +2,6 @@ package deposit import ( "context" - "errors" "strings" "sync" "sync/atomic" @@ -14,7 +13,6 @@ import ( "github.com/btcsuite/btcd/wire" "github.com/lightninglabs/loop/fsm" "github.com/lightninglabs/loop/staticaddr/script" - "github.com/lightninglabs/loop/staticaddr/version" "github.com/lightninglabs/loop/test" "github.com/lightningnetwork/lnd/lnwallet" "github.com/stretchr/testify/mock" @@ -36,13 +34,15 @@ func TestReconcileDepositsSerialized(t *testing.T) { }, } + addressParams := testAddressParameters(t, 1) + addressParams.ProtocolVersion = 999 mockAddressManager := new(mockAddressManager) mockAddressManager.On( "ListUnspent", mock.Anything, int32(0), int32(MaxConfs), ).Return([]*lnwallet.Utxo{utxo}, nil) mockAddressManager.On( "GetStaticAddressParameters", mock.Anything, - ).Return((*script.Parameters)(nil), errors.New("fsm init failed")) + ).Return(addressParams, nil) mockStore := new(mockStore) var createCalls atomic.Int32 @@ -132,13 +132,15 @@ func TestReconcileConfirmedDepositUsesCurrentHeight(t *testing.T) { }, } + addressParams := testAddressParameters(t, 2) + addressParams.ProtocolVersion = 999 mockAddressManager := new(mockAddressManager) mockAddressManager.On( "ListUnspent", mock.Anything, int32(0), int32(MaxConfs), ).Return([]*lnwallet.Utxo{utxo}, nil) mockAddressManager.On( "GetStaticAddressParameters", mock.Anything, - ).Return((*script.Parameters)(nil), errors.New("fsm init failed")) + ).Return(addressParams, nil) mockStore := new(mockStore) mockStore.On( @@ -471,6 +473,7 @@ func TestReconcileDepositsReactivatesReappearedDeposit(t *testing.T) { OutPoint: outpoint, Value: btcutil.Amount(100_000), ConfirmationHeight: 77, + AddressParams: testAddressParameters(t, 3), } deposit.SetState(Deposited) @@ -486,9 +489,7 @@ func TestReconcileDepositsReactivatesReappearedDeposit(t *testing.T) { ).Return([]*lnwallet.Utxo{utxo}, nil) mockAddressManager.On( "GetStaticAddressParameters", mock.Anything, - ).Return(&script.Parameters{ - ProtocolVersion: version.ProtocolVersion_V0, - }, nil) + ).Return(testAddressParameters(t, 4), nil) mockAddressManager.On( "GetStaticAddress", mock.Anything, ).Return((*script.StaticAddress)(nil), nil) @@ -529,10 +530,13 @@ func TestReconcileDepositsKeepsInactiveOnFSMStartFailure(t *testing.T) { Index: 5, } + addressParams := testAddressParameters(t, 5) + addressParams.ProtocolVersion = 999 deposit := &Deposit{ OutPoint: outpoint, Value: btcutil.Amount(100_000), ConfirmationHeight: 77, + AddressParams: addressParams, } deposit.SetState(Deposited) @@ -546,10 +550,6 @@ func TestReconcileDepositsKeepsInactiveOnFSMStartFailure(t *testing.T) { mockAddressManager.On( "ListUnspent", mock.Anything, int32(0), int32(MaxConfs), ).Return([]*lnwallet.Utxo{utxo}, nil) - mockAddressManager.On( - "GetStaticAddressParameters", mock.Anything, - ).Return((*script.Parameters)(nil), errors.New("fsm init failed")) - var ( updateStates []fsm.StateType updateHeights []int64 @@ -594,9 +594,12 @@ func TestReconcileDepositsDeactivatesBeforeActivationFailure(t *testing.T) { Index: 6, } + addressParams := testAddressParameters(t, 6) + addressParams.ProtocolVersion = 999 visibleDeposit := &Deposit{ - OutPoint: visibleOutpoint, - Value: btcutil.Amount(100_000), + OutPoint: visibleOutpoint, + Value: btcutil.Amount(100_000), + AddressParams: addressParams, } visibleDeposit.SetState(Deposited) @@ -616,10 +619,6 @@ func TestReconcileDepositsDeactivatesBeforeActivationFailure(t *testing.T) { mockAddressManager.On( "ListUnspent", mock.Anything, int32(0), int32(MaxConfs), ).Return([]*lnwallet.Utxo{utxo}, nil) - mockAddressManager.On( - "GetStaticAddressParameters", mock.Anything, - ).Return((*script.Parameters)(nil), errors.New("fsm init failed")) - manager := NewManager(&ManagerConfig{ AddressManager: mockAddressManager, Store: new(mockStore), @@ -687,9 +686,7 @@ func TestReconcileReplacementDepositCreatesNewDeposit(t *testing.T) { ).Return([]*lnwallet.Utxo{utxo}, nil) mockAddressManager.On( "GetStaticAddressParameters", mock.Anything, - ).Return(&script.Parameters{ - ProtocolVersion: version.ProtocolVersion_V0, - }, nil) + ).Return(testAddressParameters(t, 5), nil) mockAddressManager.On( "GetStaticAddress", mock.Anything, ).Return((*script.StaticAddress)(nil), nil) diff --git a/staticaddr/deposit/manager_test.go b/staticaddr/deposit/manager_test.go index 78663f9a7..ae6cae797 100644 --- a/staticaddr/deposit/manager_test.go +++ b/staticaddr/deposit/manager_test.go @@ -2,7 +2,6 @@ package deposit import ( "context" - "encoding/hex" "errors" "testing" "time" @@ -13,7 +12,6 @@ import ( "github.com/btcsuite/btcd/wire" "github.com/lightninglabs/lndclient" "github.com/lightninglabs/loop/staticaddr/script" - "github.com/lightninglabs/loop/swap" "github.com/lightninglabs/loop/swapserverrpc" "github.com/lightninglabs/loop/test" "github.com/lightningnetwork/lnd/chainntnfs" @@ -26,10 +24,6 @@ import ( ) var ( - defaultServerPubkeyBytes, _ = hex.DecodeString("021c97a90a411ff2b10dc2a8e32de2f29d2fa49d41bfbb52bd416e460db0747d0d") - - defaultServerPubkey, _ = btcec.ParsePubKey(defaultServerPubkeyBytes) - defaultExpiry = uint32(100) defaultDepositConfirmations = uint32(3) @@ -386,6 +380,22 @@ func TestManagerReplaysStartupBlockToRecoveredDeposits(t *testing.T) { } } +// TestManagerQuarantinesOwnerlessDeposit verifies that an ambiguous legacy +// deposit left without a static-address owner does not prevent the deposit +// manager from starting or reconciling the remaining wallet view. +func TestManagerQuarantinesOwnerlessDeposit(t *testing.T) { + testContext := newManagerTestContext(t) + testContext.storedDeposits[0].AddressParams = nil + + require.NoError(t, testContext.manager.recoverDeposits(t.Context())) + require.NoError(t, testContext.manager.reconcileDeposits(t.Context())) + + testContext.manager.mu.Lock() + require.Len(t, testContext.manager.deposits, 1) + require.Empty(t, testContext.manager.activeDeposits) + testContext.manager.mu.Unlock() +} + // TestManagerSkipsExpiryNotificationOnReconcileFailure verifies that deposit // FSMs cannot make an expiry decision from stale confirmation data when wallet // reconciliation fails at startup or while processing a later block. @@ -493,6 +503,7 @@ type ManagerTestContext struct { confErrChan chan error blockChan chan int32 blockErrChan chan error + storedDeposits []*Deposit } // newManagerTestContext creates a new test context for the reservation manager. @@ -521,6 +532,7 @@ func newManagerTestContext(t *testing.T) *ManagerTestContext { }, } require.NoError(t, err) + addressParams := testAddressParameters(t, 1) storedDeposits := []*Deposit{ { ID: ID, @@ -529,6 +541,7 @@ func newManagerTestContext(t *testing.T) *ManagerTestContext { Value: utxo.Value, ConfirmationHeight: 3, TimeOutSweepPkScript: []byte{0x42, 0x21, 0x69}, + AddressParams: addressParams, }, } @@ -543,9 +556,7 @@ func newManagerTestContext(t *testing.T) *ManagerTestContext { var manager *Manager mockAddressManager.On( "GetStaticAddressParameters", mock.Anything, - ).Return(&script.Parameters{ - Expiry: defaultExpiry, - }, nil) + ).Return(addressParams, nil) mockAddressManager.On( "ListUnspent", mock.Anything, mock.Anything, mock.Anything, @@ -593,31 +604,17 @@ func newManagerTestContext(t *testing.T) *ManagerTestContext { confErrChan: confErrChan, blockChan: blockChan, blockErrChan: blockErrChan, + storedDeposits: storedDeposits, } - staticAddress := generateStaticAddress( - context.Background(), testContext, + staticAddress, err := script.NewStaticAddress( + input.MuSig2Version100RC2, int64(addressParams.Expiry), + addressParams.ClientPubkey, addressParams.ServerPubkey, ) + require.NoError(t, err) mockAddressManager.On( "GetStaticAddress", mock.Anything, ).Return(staticAddress, nil) return testContext } - -func generateStaticAddress(ctx context.Context, - t *ManagerTestContext) *script.StaticAddress { - - keyDescriptor, err := t.mockLnd.WalletKit.DeriveNextKey( - ctx, swap.StaticAddressKeyFamily, - ) - require.NoError(t.context.T, err) - - staticAddress, err := script.NewStaticAddress( - input.MuSig2Version100RC2, int64(defaultExpiry), - keyDescriptor.PubKey, defaultServerPubkey, - ) - require.NoError(t.context.T, err) - - return staticAddress -} diff --git a/staticaddr/deposit/sql_store.go b/staticaddr/deposit/sql_store.go index a49550e5c..990227b70 100644 --- a/staticaddr/deposit/sql_store.go +++ b/staticaddr/deposit/sql_store.go @@ -6,14 +6,19 @@ import ( "database/sql" "encoding/hex" "errors" + "fmt" + "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/wire" "github.com/lightninglabs/loop/fsm" "github.com/lightninglabs/loop/loopdb" "github.com/lightninglabs/loop/loopdb/sqlc" + "github.com/lightninglabs/loop/staticaddr/script" + "github.com/lightninglabs/loop/staticaddr/version" "github.com/lightningnetwork/lnd/clock" + "github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/lntypes" ) @@ -42,6 +47,13 @@ func NewSqlStore(db *loopdb.BaseDB) *SqlStore { // CreateDeposit creates a static address deposit record in the database. func (s *SqlStore) CreateDeposit(ctx context.Context, deposit *Deposit) error { + if deposit.AddressParams == nil { + return fmt.Errorf("static address parameters must be set") + } + if deposit.AddressParams.ID <= 0 { + return fmt.Errorf("static address ID must be set") + } + createArgs := sqlc.CreateDepositParams{ DepositID: deposit.ID[:], TxHash: deposit.Hash[:], @@ -49,6 +61,10 @@ func (s *SqlStore) CreateDeposit(ctx context.Context, deposit *Deposit) error { Amount: int64(deposit.Value), ConfirmationHeight: deposit.GetConfirmationHeight(), TimeoutSweepPkScript: deposit.TimeOutSweepPkScript, + StaticAddressID: sql.NullInt32{ + Int32: deposit.AddressParams.ID, + Valid: true, + }, } updateArgs := sqlc.InsertDepositUpdateParams{ @@ -135,7 +151,7 @@ func (s *SqlStore) GetDeposit(ctx context.Context, id ID) (*Deposit, error) { var deposit *Deposit err := s.baseDB.ExecTx(ctx, loopdb.NewSqlReadOpts(), func(q *sqlc.Queries) error { - row, err := q.GetDeposit(ctx, id[:]) + row, err := q.GetDepositWithAddress(ctx, id[:]) if err != nil { return err } @@ -147,7 +163,9 @@ func (s *SqlStore) GetDeposit(ctx context.Context, id ID) (*Deposit, error) { return err } - deposit, err = ToDeposit(row, latestUpdate) + deposit, err = toDeposit( + depositRowFromGet(row), latestUpdate, + ) if err != nil { return err } @@ -173,11 +191,11 @@ func (s *SqlStore) DepositForOutpoint(ctx context.Context, if err != nil { return err } - params := sqlc.DepositForOutpointParams{ + params := sqlc.DepositForOutpointWithAddressParams{ TxHash: op.Hash[:], OutIndex: int32(op.Index), } - row, err := q.DepositForOutpoint(ctx, params) + row, err := q.DepositForOutpointWithAddress(ctx, params) if err != nil { if errors.Is(err, sql.ErrNoRows) { return ErrDepositNotFound @@ -193,7 +211,9 @@ func (s *SqlStore) DepositForOutpoint(ctx context.Context, return err } - deposit, err = ToDeposit(row, latestUpdate) + deposit, err = toDeposit( + depositRowFromOutpoint(row), latestUpdate, + ) if err != nil { return err } @@ -215,7 +235,7 @@ func (s *SqlStore) AllDeposits(ctx context.Context) ([]*Deposit, error) { func(q *sqlc.Queries) error { var err error - deposits, err := q.AllDeposits(ctx) + deposits, err := q.AllDepositsWithAddress(ctx) if err != nil { return err } @@ -228,7 +248,7 @@ func (s *SqlStore) AllDeposits(ctx context.Context) ([]*Deposit, error) { return err } - d, err := ToDeposit(deposit, latestUpdate) + d, err := ToDepositWithAddress(deposit, latestUpdate) if err != nil { return err } @@ -245,10 +265,167 @@ func (s *SqlStore) AllDeposits(ctx context.Context) ([]*Deposit, error) { return allDeposits, nil } -// ToDeposit converts an sql deposit to a deposit. +// ToDeposit converts an SQL deposit to a deposit. This preserves the original +// exported conversion API for callers that do not have joined static-address +// metadata. func ToDeposit(row sqlc.Deposit, lastUpdate sqlc.DepositUpdate) (*Deposit, error) { + return toDeposit(depositRowFromDeposit(row), lastUpdate) +} + +// ToDepositWithAddress converts an SQL deposit row with joined static-address +// metadata to a deposit. +func ToDepositWithAddress(row sqlc.AllDepositsWithAddressRow, + lastUpdate sqlc.DepositUpdate) (*Deposit, error) { + + return toDeposit(depositRowFromAll(row), lastUpdate) +} + +// ToDepositForSwapHash converts the joined deposit row returned while +// recovering a static-address Loop-In swap. Keeping this conversion beside the +// other deposit-row converters prevents the Loop-In store from maintaining a +// separate hand-copy of every deposit and static-address field. +func ToDepositForSwapHash(row sqlc.DepositsForSwapHashRow) (*Deposit, error) { + lastUpdate := sqlc.DepositUpdate{ + DepositID: row.DepositID, + UpdateState: row.UpdateState.String, + UpdateTimestamp: row.UpdateTimestamp.Time, + } + + return toDeposit(depositRowFromSwapHash(row), lastUpdate) +} + +type depositRow struct { + DepositID []byte + TxHash []byte + OutIndex int32 + Amount int64 + ConfirmationHeight int64 + TimeoutSweepPkScript []byte + ExpirySweepTxid []byte + FinalizedWithdrawalTx sql.NullString + SwapHash []byte + StaticAddressID sql.NullInt32 + ClientPubkey []byte + ServerPubkey []byte + Expiry sql.NullInt32 + ClientKeyFamily sql.NullInt32 + ClientKeyIndex sql.NullInt32 + Pkscript []byte + ProtocolVersion sql.NullInt32 + InitiationHeight sql.NullInt32 +} + +func depositRowFromDeposit(row sqlc.Deposit) depositRow { + return depositRow{ + DepositID: row.DepositID, + TxHash: row.TxHash, + OutIndex: row.OutIndex, + Amount: row.Amount, + ConfirmationHeight: row.ConfirmationHeight, + TimeoutSweepPkScript: row.TimeoutSweepPkScript, + ExpirySweepTxid: row.ExpirySweepTxid, + FinalizedWithdrawalTx: row.FinalizedWithdrawalTx, + SwapHash: row.SwapHash, + } +} + +func depositRowFromAll(row sqlc.AllDepositsWithAddressRow) depositRow { + return depositRow{ + DepositID: row.DepositID, + TxHash: row.TxHash, + OutIndex: row.OutIndex, + Amount: row.Amount, + ConfirmationHeight: row.ConfirmationHeight, + TimeoutSweepPkScript: row.TimeoutSweepPkScript, + ExpirySweepTxid: row.ExpirySweepTxid, + FinalizedWithdrawalTx: row.FinalizedWithdrawalTx, + SwapHash: row.SwapHash, + StaticAddressID: row.StaticAddressID, + ClientPubkey: row.ClientPubkey, + ServerPubkey: row.ServerPubkey, + Expiry: row.Expiry, + ClientKeyFamily: row.ClientKeyFamily, + ClientKeyIndex: row.ClientKeyIndex, + Pkscript: row.Pkscript, + ProtocolVersion: row.ProtocolVersion, + InitiationHeight: row.InitiationHeight, + } +} + +func depositRowFromSwapHash(row sqlc.DepositsForSwapHashRow) depositRow { + return depositRow{ + DepositID: row.DepositID, + TxHash: row.TxHash, + OutIndex: row.OutIndex, + Amount: row.Amount, + ConfirmationHeight: row.ConfirmationHeight, + TimeoutSweepPkScript: row.TimeoutSweepPkScript, + ExpirySweepTxid: row.ExpirySweepTxid, + FinalizedWithdrawalTx: row.FinalizedWithdrawalTx, + SwapHash: row.SwapHash, + StaticAddressID: row.StaticAddressID, + ClientPubkey: row.ClientPubkey, + ServerPubkey: row.ServerPubkey, + Expiry: row.Expiry, + ClientKeyFamily: row.ClientKeyFamily, + ClientKeyIndex: row.ClientKeyIndex, + Pkscript: row.Pkscript, + ProtocolVersion: row.ProtocolVersion, + InitiationHeight: row.InitiationHeight, + } +} + +func depositRowFromGet(row sqlc.GetDepositWithAddressRow) depositRow { + return depositRow{ + DepositID: row.DepositID, + TxHash: row.TxHash, + OutIndex: row.OutIndex, + Amount: row.Amount, + ConfirmationHeight: row.ConfirmationHeight, + TimeoutSweepPkScript: row.TimeoutSweepPkScript, + ExpirySweepTxid: row.ExpirySweepTxid, + FinalizedWithdrawalTx: row.FinalizedWithdrawalTx, + SwapHash: row.SwapHash, + StaticAddressID: row.StaticAddressID, + ClientPubkey: row.ClientPubkey, + ServerPubkey: row.ServerPubkey, + Expiry: row.Expiry, + ClientKeyFamily: row.ClientKeyFamily, + ClientKeyIndex: row.ClientKeyIndex, + Pkscript: row.Pkscript, + ProtocolVersion: row.ProtocolVersion, + InitiationHeight: row.InitiationHeight, + } +} + +func depositRowFromOutpoint(row sqlc.DepositForOutpointWithAddressRow) depositRow { + return depositRow{ + DepositID: row.DepositID, + TxHash: row.TxHash, + OutIndex: row.OutIndex, + Amount: row.Amount, + ConfirmationHeight: row.ConfirmationHeight, + TimeoutSweepPkScript: row.TimeoutSweepPkScript, + ExpirySweepTxid: row.ExpirySweepTxid, + FinalizedWithdrawalTx: row.FinalizedWithdrawalTx, + SwapHash: row.SwapHash, + StaticAddressID: row.StaticAddressID, + ClientPubkey: row.ClientPubkey, + ServerPubkey: row.ServerPubkey, + Expiry: row.Expiry, + ClientKeyFamily: row.ClientKeyFamily, + ClientKeyIndex: row.ClientKeyIndex, + Pkscript: row.Pkscript, + ProtocolVersion: row.ProtocolVersion, + InitiationHeight: row.InitiationHeight, + } +} + +func toDeposit(row depositRow, lastUpdate sqlc.DepositUpdate) (*Deposit, + error) { + id := ID{} err := id.FromByteSlice(row.DepositID) if err != nil { @@ -296,7 +473,7 @@ func ToDeposit(row sqlc.Deposit, lastUpdate sqlc.DepositUpdate) (*Deposit, swapHash = &hash } - return &Deposit{ + deposit := &Deposit{ ID: id, state: fsm.StateType(lastUpdate.UpdateState), OutPoint: wire.OutPoint{ @@ -309,5 +486,37 @@ func ToDeposit(row sqlc.Deposit, lastUpdate sqlc.DepositUpdate) (*Deposit, ExpirySweepTxid: expirySweepTxid, SwapHash: swapHash, FinalizedWithdrawalTx: finalizedWithdrawalTx, - }, nil + } + + if row.StaticAddressID.Valid { + clientPubkey, err := btcec.ParsePubKey(row.ClientPubkey) + if err != nil { + return nil, err + } + + serverPubkey, err := btcec.ParsePubKey(row.ServerPubkey) + if err != nil { + return nil, err + } + + deposit.AddressParams = &script.Parameters{ + ID: row.StaticAddressID.Int32, + ClientPubkey: clientPubkey, + ServerPubkey: serverPubkey, + Expiry: uint32(row.Expiry.Int32), + PkScript: row.Pkscript, + KeyLocator: keychain.KeyLocator{ + Family: keychain.KeyFamily( + row.ClientKeyFamily.Int32, + ), + Index: uint32(row.ClientKeyIndex.Int32), + }, + ProtocolVersion: version.AddressProtocolVersion( + row.ProtocolVersion.Int32, + ), + InitiationHeight: row.InitiationHeight.Int32, + } + } + + return deposit, nil } diff --git a/staticaddr/deposit/sql_store_test.go b/staticaddr/deposit/sql_store_test.go index 5656e386a..06bb743be 100644 --- a/staticaddr/deposit/sql_store_test.go +++ b/staticaddr/deposit/sql_store_test.go @@ -1,17 +1,48 @@ package deposit import ( + "context" "database/sql" + "reflect" "testing" + "time" "github.com/btcsuite/btcd/wire" "github.com/jackc/pgx/v5" "github.com/lightninglabs/loop/fsm" "github.com/lightninglabs/loop/loopdb/sqlc" + "github.com/lightninglabs/loop/staticaddr/script" "github.com/lightningnetwork/lnd/lntypes" "github.com/stretchr/testify/require" ) +func TestCreateDepositRejectsUnpersistedAddress(t *testing.T) { + store := NewSqlStore(nil) + tests := []struct { + name string + params *script.Parameters + wantErr string + }{ + { + name: "missing parameters", + wantErr: "static address parameters must be set", + }, + { + name: "missing database ID", + params: &script.Parameters{}, + wantErr: "static address ID must be set", + }, + } + + for _, testCase := range tests { + t.Run(testCase.name, func(t *testing.T) { + deposit := &Deposit{AddressParams: testCase.params} + err := store.CreateDeposit(context.Background(), deposit) + require.ErrorContains(t, err, testCase.wantErr) + }) + } +} + func TestToDeposit(t *testing.T) { depositID, err := GetRandomDepositID() require.NoError(t, err) @@ -72,6 +103,156 @@ func TestToDeposit(t *testing.T) { } } +func TestToDepositWithAddress(t *testing.T) { + depositID, err := GetRandomDepositID() + require.NoError(t, err) + + txHash := wire.NewMsgTx(2).TxHash() + params := testAddressParameters(t, 7) + row := sqlc.AllDepositsWithAddressRow{ + DepositID: depositID[:], + TxHash: txHash[:], + Amount: 100_000, + ConfirmationHeight: 123, + StaticAddressID: sql.NullInt32{ + Int32: params.ID, + Valid: true, + }, + ClientPubkey: params.ClientPubkey.SerializeCompressed(), + ServerPubkey: params.ServerPubkey.SerializeCompressed(), + Expiry: sql.NullInt32{ + Int32: int32(params.Expiry), + Valid: true, + }, + ClientKeyFamily: sql.NullInt32{ + Int32: int32(params.KeyLocator.Family), + Valid: true, + }, + ClientKeyIndex: sql.NullInt32{ + Int32: int32(params.KeyLocator.Index), + Valid: true, + }, + Pkscript: params.PkScript, + ProtocolVersion: sql.NullInt32{ + Int32: int32(params.ProtocolVersion), + Valid: true, + }, + InitiationHeight: sql.NullInt32{ + Int32: params.InitiationHeight, + Valid: true, + }, + } + lastUpdate := sqlc.DepositUpdate{UpdateState: "completed"} + + result, err := ToDepositWithAddress(row, lastUpdate) + require.NoError(t, err) + require.NotNil(t, result.AddressParams) + require.Equal(t, params.ID, result.AddressParams.ID) + require.True(t, params.ClientPubkey.IsEqual( + result.AddressParams.ClientPubkey, + )) + require.True(t, params.ServerPubkey.IsEqual( + result.AddressParams.ServerPubkey, + )) + require.Equal(t, params.Expiry, result.AddressParams.Expiry) + require.Equal(t, params.PkScript, result.AddressParams.PkScript) + require.Equal(t, params.KeyLocator, result.AddressParams.KeyLocator) + require.Equal(t, params.ProtocolVersion, + result.AddressParams.ProtocolVersion) + require.Equal(t, params.InitiationHeight, + result.AddressParams.InitiationHeight) + + row.ClientPubkey = []byte{0x01} + result, err = ToDepositWithAddress(row, lastUpdate) + require.Error(t, err) + require.Nil(t, result) +} + +func TestDepositRowConvertersStayInSync(t *testing.T) { + t.Parallel() + + all := populatedSQLRow[sqlc.AllDepositsWithAddressRow](t) + require.Equal(t, expectedDepositRow(t, all), depositRowFromAll(all)) + + get := populatedSQLRow[sqlc.GetDepositWithAddressRow](t) + require.Equal(t, expectedDepositRow(t, get), depositRowFromGet(get)) + + outpoint := populatedSQLRow[sqlc.DepositForOutpointWithAddressRow](t) + require.Equal(t, expectedDepositRow(t, outpoint), + depositRowFromOutpoint(outpoint)) + + swapHash := populatedSQLRow[sqlc.DepositsForSwapHashRow](t) + require.Equal(t, + expectedDepositRow(t, swapHash, "UpdateState", "UpdateTimestamp"), + depositRowFromSwapHash(swapHash)) +} + +func populatedSQLRow[T any](t *testing.T) T { + t.Helper() + + var row T + value := reflect.ValueOf(&row).Elem() + for i := 0; i < value.NumField(); i++ { + field := value.Field(i) + n := int64(i + 1) + switch field.Type() { + case reflect.TypeFor[[]byte](): + field.SetBytes([]byte{byte(n)}) + + case reflect.TypeFor[int32](), reflect.TypeFor[int64](): + field.SetInt(n) + + case reflect.TypeFor[sql.NullString](): + field.Set(reflect.ValueOf(sql.NullString{ + String: "set", + Valid: true, + })) + + case reflect.TypeFor[sql.NullInt32](): + field.Set(reflect.ValueOf(sql.NullInt32{ + Int32: int32(n), + Valid: true, + })) + + case reflect.TypeFor[sql.NullTime](): + field.Set(reflect.ValueOf(sql.NullTime{ + Time: time.Unix(n, 0).UTC(), + Valid: true, + })) + + default: + t.Fatalf("unsupported SQL row field %s", field.Type()) + } + } + + return row +} + +func expectedDepositRow(t *testing.T, sqlRow any, + additionalFields ...string) depositRow { + + t.Helper() + + source := reflect.ValueOf(sqlRow) + target := reflect.ValueOf(&depositRow{}).Elem() + require.Equal(t, target.NumField()+1+len(additionalFields), + source.NumField()) + require.Equal(t, "ID", source.Type().Field(0).Name) + for _, fieldName := range additionalFields { + require.True(t, source.FieldByName(fieldName).IsValid(), fieldName) + } + + for i := 0; i < target.NumField(); i++ { + targetField := target.Type().Field(i) + sourceField := source.FieldByName(targetField.Name) + require.True(t, sourceField.IsValid(), targetField.Name) + require.Equal(t, targetField.Type, sourceField.Type()) + target.Field(i).Set(sourceField) + } + + return target.Interface().(depositRow) +} + func dummyHashBytes() []byte { return []byte{0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, diff --git a/staticaddr/loopin/deposit_swaphash_migration_test.go b/staticaddr/loopin/deposit_swaphash_migration_test.go index ab93a10d2..59f716f20 100644 --- a/staticaddr/loopin/deposit_swaphash_migration_test.go +++ b/staticaddr/loopin/deposit_swaphash_migration_test.go @@ -67,6 +67,9 @@ func TestDepositSwapHashMigration(t *testing.T) { 0x00, 0x14, 0x1a, 0x2b, 0x3c, 0x4d, }, } + addressParams := persistTestAddressParameters(t, ctxb, testDb.BaseDB) + d1.AddressParams = addressParams + d2.AddressParams = addressParams err := depositStore.CreateDeposit(ctxb, d1) require.NoError(t, err) diff --git a/staticaddr/loopin/selected_amount_migration_test.go b/staticaddr/loopin/selected_amount_migration_test.go index b68269fc0..62278eba3 100644 --- a/staticaddr/loopin/selected_amount_migration_test.go +++ b/staticaddr/loopin/selected_amount_migration_test.go @@ -61,6 +61,9 @@ func TestMigrateSelectedSwapAmount(t *testing.T) { 0x00, 0x14, 0x1a, 0x2b, 0x3c, 0x4d, }, } + addressParams := persistTestAddressParameters(t, ctxb, testDb.BaseDB) + d1.AddressParams = addressParams + d2.AddressParams = addressParams err := depositStore.CreateDeposit(ctxb, d1) require.NoError(t, err) diff --git a/staticaddr/loopin/sql_store.go b/staticaddr/loopin/sql_store.go index 9dc2a084c..673d531e9 100644 --- a/staticaddr/loopin/sql_store.go +++ b/staticaddr/loopin/sql_store.go @@ -595,31 +595,7 @@ 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, - ) + deposit, err := deposit.ToDepositForSwapHash(d) if err != nil { return nil, err } diff --git a/staticaddr/loopin/sql_store_test.go b/staticaddr/loopin/sql_store_test.go index c08d940f2..0c29d4f32 100644 --- a/staticaddr/loopin/sql_store_test.go +++ b/staticaddr/loopin/sql_store_test.go @@ -10,9 +10,15 @@ import ( "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/wire" "github.com/lightninglabs/loop/loopdb" + staticaddress "github.com/lightninglabs/loop/staticaddr/address" "github.com/lightninglabs/loop/staticaddr/deposit" + "github.com/lightninglabs/loop/staticaddr/script" + "github.com/lightninglabs/loop/staticaddr/version" + "github.com/lightninglabs/loop/swap" "github.com/lightninglabs/loop/test" "github.com/lightningnetwork/lnd/clock" + "github.com/lightningnetwork/lnd/input" + "github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/lntypes" "github.com/stretchr/testify/require" ) @@ -88,6 +94,10 @@ func TestGetStaticAddressLoopInSwapsByStates(t *testing.T) { 0x00, 0x14, 0x1a, 0x2b, 0x3c, 0x50, }, } + addressParams := persistTestAddressParameters(t, ctxb, testDb.BaseDB) + for _, d := range []*deposit.Deposit{d1, d2, d3, d4} { + d.AddressParams = addressParams + } err := depositStore.CreateDeposit(ctxb, d1) require.NoError(t, err) @@ -212,6 +222,9 @@ func TestGetStaticAddressLoopInSwapsByStates(t *testing.T) { require.Equal(t, d1.OutPoint, pendingDeposits[0].OutPoint) require.Equal(t, d1.Value, pendingDeposits[0].Value) require.Equal(t, deposit.LoopingIn, pendingDeposits[0].GetState()) + require.Equal(t, addressParams.ID, pendingDeposits[0].AddressParams.ID) + require.Equal(t, addressParams.PkScript, + pendingDeposits[0].AddressParams.PkScript) finalizedSwaps, err := swapStore.GetStaticAddressLoopInSwapsByStates(ctxb, FinalStates) require.NoError(t, err) @@ -292,6 +305,9 @@ func TestCreateLoopIn(t *testing.T) { 0x00, 0x14, 0x1a, 0x2b, 0x3c, 0x4d, }, } + addressParams := persistTestAddressParameters(t, ctx, testDb.BaseDB) + d1.AddressParams = addressParams + d2.AddressParams = addressParams err := depositStore.CreateDeposit(ctx, d1) require.NoError(t, err) @@ -504,6 +520,9 @@ func TestGetLoopInByHashOrdersDepositsBySnapshot(t *testing.T) { 0x00, 0x14, 0x1a, 0x2b, 0x3c, 0x4d, }, } + addressParams := persistTestAddressParameters(t, ctx, testDb.BaseDB) + d1.AddressParams = addressParams + d2.AddressParams = addressParams require.NoError(t, depositStore.CreateDeposit(ctx, d1)) require.NoError(t, depositStore.CreateDeposit(ctx, d2)) @@ -578,6 +597,7 @@ func TestGetLoopInByHashPreservesStoredDepositOutpoints(t *testing.T) { 0x00, 0x14, 0x1a, 0x2b, 0x3c, 0x41, }, } + d.AddressParams = persistTestAddressParameters(t, ctxb, testDb.BaseDB) require.NoError(t, depositStore.CreateDeposit(ctxb, d)) d.SetState(deposit.LoopingIn) @@ -616,3 +636,37 @@ func TestGetLoopInByHashPreservesStoredDepositOutpoints(t *testing.T) { require.Equal(t, currentOutpoint, storedSwap.Deposits[0].OutPoint) require.Equal(t, int64(42), storedSwap.Deposits[0].ConfirmationHeight) } + +func persistTestAddressParameters(t *testing.T, ctx context.Context, + db *loopdb.BaseDB) *script.Parameters { + + t.Helper() + + _, clientKey := test.CreateKey(31) + _, serverKey := test.CreateKey(32) + staticAddress, err := script.NewStaticAddress( + input.MuSig2Version100RC2, 144, clientKey, serverKey, + ) + require.NoError(t, err) + pkScript, err := staticAddress.StaticAddressScript() + require.NoError(t, err) + + params := &script.Parameters{ + ClientPubkey: clientKey, + ServerPubkey: serverKey, + PkScript: pkScript, + Expiry: 144, + KeyLocator: keychain.KeyLocator{ + Family: keychain.KeyFamily(swap.StaticAddressKeyFamily), + Index: 31, + }, + ProtocolVersion: version.ProtocolVersion_V0, + InitiationHeight: 100, + } + addressStore := staticaddress.NewSqlStore(db) + require.NoError(t, addressStore.CreateStaticAddress(ctx, params)) + params.ID, err = addressStore.GetStaticAddressID(ctx, pkScript) + require.NoError(t, err) + + return params +} diff --git a/staticaddr/script/parameters.go b/staticaddr/script/parameters.go index 89e2470b6..0fa1f73b4 100644 --- a/staticaddr/script/parameters.go +++ b/staticaddr/script/parameters.go @@ -9,6 +9,10 @@ import ( // Parameters holds all the necessary information for the 2-of-2 multisig // address. type Parameters struct { + // ID is the database primary key of the static address row. A zero value + // means the parameters have not been persisted yet. + ID int32 + // ClientPubkey is the client's pubkey for the static address. It is // used for the 2-of-2 funding output as well as for the client's // timeout path. diff --git a/staticaddr/withdraw/sql_store_test.go b/staticaddr/withdraw/sql_store_test.go index 5897f20ef..193088182 100644 --- a/staticaddr/withdraw/sql_store_test.go +++ b/staticaddr/withdraw/sql_store_test.go @@ -7,7 +7,14 @@ import ( "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/wire" "github.com/lightninglabs/loop/loopdb" + staticaddress "github.com/lightninglabs/loop/staticaddr/address" "github.com/lightninglabs/loop/staticaddr/deposit" + "github.com/lightninglabs/loop/staticaddr/script" + "github.com/lightninglabs/loop/staticaddr/version" + "github.com/lightninglabs/loop/swap" + "github.com/lightninglabs/loop/test" + "github.com/lightningnetwork/lnd/input" + "github.com/lightningnetwork/lnd/keychain" "github.com/stretchr/testify/require" ) @@ -41,6 +48,11 @@ func TestSqlStore(t *testing.T) { 0x00, 0x14, 0x1a, 0x2b, 0x3c, 0x4d, }, } + addressParams := persistWithdrawalTestAddressParameters( + t, ctxb, testDb.BaseDB, + ) + d1.AddressParams = addressParams + d2.AddressParams = addressParams withdrawalTx := &wire.MsgTx{ Version: 2, @@ -99,3 +111,37 @@ func TestSqlStore(t *testing.T) { require.EqualValues(t, 100, withdrawals[0].ChangeAmount) require.EqualValues(t, 6, withdrawals[0].ConfirmationHeight) } + +func persistWithdrawalTestAddressParameters(t *testing.T, ctx context.Context, + db *loopdb.BaseDB) *script.Parameters { + + t.Helper() + + _, clientKey := test.CreateKey(41) + _, serverKey := test.CreateKey(42) + staticAddress, err := script.NewStaticAddress( + input.MuSig2Version100RC2, 144, clientKey, serverKey, + ) + require.NoError(t, err) + pkScript, err := staticAddress.StaticAddressScript() + require.NoError(t, err) + + params := &script.Parameters{ + ClientPubkey: clientKey, + ServerPubkey: serverKey, + PkScript: pkScript, + Expiry: 144, + KeyLocator: keychain.KeyLocator{ + Family: keychain.KeyFamily(swap.StaticAddressKeyFamily), + Index: 41, + }, + ProtocolVersion: version.ProtocolVersion_V0, + InitiationHeight: 100, + } + addressStore := staticaddress.NewSqlStore(db) + require.NoError(t, addressStore.CreateStaticAddress(ctx, params)) + params.ID, err = addressStore.GetStaticAddressID(ctx, pkScript) + require.NoError(t, err) + + return params +} diff --git a/swap/keychain.go b/swap/keychain.go index 37106950c..eded48133 100644 --- a/swap/keychain.go +++ b/swap/keychain.go @@ -5,7 +5,16 @@ var ( // spending of the htlc. KeyFamily = int32(99) - // StaticAddressKeyFamily is the key family used to generate static - // address keys. + // StaticAddressKeyFamily is the legacy static-address key family. It is + // used for the V0 single static-address key and for static-address HTLC + // keys. StaticAddressKeyFamily = int32(42060) + + // StaticMultiAddressKeyFamily is the key family used to generate + // externally visible multi-address static-address receive keys. + StaticMultiAddressKeyFamily = int32(42061) + + // StaticAddressChangeKeyFamily is the key family used to generate + // static-address change outputs. + StaticAddressChangeKeyFamily = int32(42062) ) diff --git a/swap/keychain_test.go b/swap/keychain_test.go new file mode 100644 index 000000000..d45a38940 --- /dev/null +++ b/swap/keychain_test.go @@ -0,0 +1,24 @@ +package swap + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +// TestStaticAddressKeyFamiliesAreDisjoint documents the key-family split used +// by static-address HTLC, receive and change key derivation. +func TestStaticAddressKeyFamiliesAreDisjoint(t *testing.T) { + families := map[int32]string{ + KeyFamily: "swap htlc", + StaticAddressKeyFamily: "legacy static address and htlc", + StaticMultiAddressKeyFamily: "multi-address receive", + StaticAddressChangeKeyFamily: "static-address change", + } + + require.Len(t, families, 4) + require.EqualValues(t, 99, KeyFamily) + require.EqualValues(t, 42060, StaticAddressKeyFamily) + require.EqualValues(t, 42061, StaticMultiAddressKeyFamily) + require.EqualValues(t, 42062, StaticAddressChangeKeyFamily) +}