mirror of
https://github.com/lightninglabs/loop.git
synced 2026-08-13 12:33:03 +02:00
staticaddr/loopin: use generated change addresses
Create a fresh static change address for fractional loop-ins and persist its key locator with the selected HTLC outpoint. Recovery reconstructs the same change output instead of returning funds to the legacy root address.
This commit is contained in:
parent
b8157ef901
commit
dc42446bae
18 changed files with 878 additions and 90 deletions
|
|
@ -0,0 +1 @@
|
|||
ALTER TABLE static_address_swaps DROP COLUMN change_static_address_id;
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
ALTER TABLE static_address_swaps
|
||||
ADD change_static_address_id INT REFERENCES static_addresses(id);
|
||||
|
||||
-- Existing fractional swaps sent change back to the legacy static address.
|
||||
-- Backfill that relation so in-flight swaps remain recoverable after the
|
||||
-- client starts requiring explicit per-swap change metadata.
|
||||
UPDATE static_address_swaps
|
||||
SET change_static_address_id = (
|
||||
SELECT id FROM static_addresses ORDER BY id ASC LIMIT 1
|
||||
)
|
||||
WHERE selected_amount > 0
|
||||
AND change_static_address_id IS NULL
|
||||
AND EXISTS (SELECT 1 FROM static_addresses);
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
ALTER TABLE static_address_swaps
|
||||
DROP COLUMN confirmed_htlc_output_value;
|
||||
|
||||
ALTER TABLE static_address_swaps
|
||||
DROP COLUMN confirmed_htlc_output_index;
|
||||
|
||||
ALTER TABLE static_address_swaps
|
||||
DROP COLUMN confirmed_htlc_tx_id;
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
ALTER TABLE static_address_swaps
|
||||
ADD confirmed_htlc_tx_id TEXT;
|
||||
|
||||
ALTER TABLE static_address_swaps
|
||||
ADD confirmed_htlc_output_index INTEGER;
|
||||
|
||||
ALTER TABLE static_address_swaps
|
||||
ADD confirmed_htlc_output_value BIGINT;
|
||||
|
|
@ -152,6 +152,10 @@ type StaticAddressSwap struct {
|
|||
Fast bool
|
||||
ConfirmationRiskDecision string
|
||||
ConfirmationRiskDecisionTime sql.NullTime
|
||||
ChangeStaticAddressID sql.NullInt32
|
||||
ConfirmedHtlcTxID sql.NullString
|
||||
ConfirmedHtlcOutputIndex sql.NullInt32
|
||||
ConfirmedHtlcOutputValue sql.NullInt64
|
||||
}
|
||||
|
||||
type StaticAddressSwapUpdate struct {
|
||||
|
|
|
|||
|
|
@ -10,7 +10,8 @@ INSERT INTO static_address_swaps (
|
|||
htlc_tx_fee_rate_sat_kw,
|
||||
htlc_timeout_sweep_tx_id,
|
||||
htlc_timeout_sweep_address,
|
||||
fast
|
||||
fast,
|
||||
change_static_address_id
|
||||
) VALUES (
|
||||
$1,
|
||||
$2,
|
||||
|
|
@ -22,14 +23,18 @@ INSERT INTO static_address_swaps (
|
|||
$8,
|
||||
$9,
|
||||
$10,
|
||||
$11
|
||||
$11,
|
||||
$12
|
||||
);
|
||||
|
||||
-- name: UpdateStaticAddressLoopIn :exec
|
||||
UPDATE static_address_swaps
|
||||
SET
|
||||
htlc_tx_fee_rate_sat_kw = $2,
|
||||
htlc_timeout_sweep_tx_id = $3
|
||||
htlc_timeout_sweep_tx_id = $3,
|
||||
confirmed_htlc_tx_id = $4,
|
||||
confirmed_htlc_output_index = $5,
|
||||
confirmed_htlc_output_value = $6
|
||||
WHERE
|
||||
swap_hash = $1;
|
||||
|
||||
|
|
@ -64,13 +69,24 @@ INSERT INTO static_address_swap_updates (
|
|||
SELECT
|
||||
swaps.*,
|
||||
static_address_swaps.*,
|
||||
htlc_keys.*
|
||||
htlc_keys.*,
|
||||
change_address.client_pubkey change_client_pubkey,
|
||||
change_address.server_pubkey change_server_pubkey,
|
||||
change_address.expiry change_expiry,
|
||||
change_address.client_key_family change_client_key_family,
|
||||
change_address.client_key_index change_client_key_index,
|
||||
change_address.pkscript change_pkscript,
|
||||
change_address.protocol_version change_protocol_version,
|
||||
change_address.initiation_height change_initiation_height
|
||||
FROM
|
||||
swaps
|
||||
JOIN
|
||||
static_address_swaps ON swaps.swap_hash = static_address_swaps.swap_hash
|
||||
JOIN
|
||||
htlc_keys ON swaps.swap_hash = htlc_keys.swap_hash
|
||||
LEFT JOIN
|
||||
static_addresses change_address
|
||||
ON static_address_swaps.change_static_address_id = change_address.id
|
||||
WHERE
|
||||
swaps.swap_hash = $1;
|
||||
|
||||
|
|
@ -78,13 +94,24 @@ WHERE
|
|||
SELECT
|
||||
swaps.*,
|
||||
static_address_swaps.*,
|
||||
htlc_keys.*
|
||||
htlc_keys.*,
|
||||
change_address.client_pubkey change_client_pubkey,
|
||||
change_address.server_pubkey change_server_pubkey,
|
||||
change_address.expiry change_expiry,
|
||||
change_address.client_key_family change_client_key_family,
|
||||
change_address.client_key_index change_client_key_index,
|
||||
change_address.pkscript change_pkscript,
|
||||
change_address.protocol_version change_protocol_version,
|
||||
change_address.initiation_height change_initiation_height
|
||||
FROM
|
||||
swaps
|
||||
JOIN
|
||||
static_address_swaps ON swaps.swap_hash = static_address_swaps.swap_hash
|
||||
JOIN
|
||||
htlc_keys ON swaps.swap_hash = htlc_keys.swap_hash
|
||||
LEFT JOIN
|
||||
static_addresses change_address
|
||||
ON static_address_swaps.change_static_address_id = change_address.id
|
||||
JOIN
|
||||
static_address_swap_updates u ON swaps.swap_hash = u.swap_hash
|
||||
-- This subquery ensures that we are checking only the latest update for
|
||||
|
|
@ -170,5 +197,3 @@ FROM
|
|||
)
|
||||
WHERE
|
||||
d.swap_hash = $1;
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -180,14 +180,25 @@ func (q *Queries) GetLoopInSwapUpdates(ctx context.Context, swapHash []byte) ([]
|
|||
const getStaticAddressLoopInSwap = `-- name: GetStaticAddressLoopInSwap :one
|
||||
SELECT
|
||||
swaps.id, swaps.swap_hash, swaps.preimage, swaps.initiation_time, swaps.amount_requested, swaps.cltv_expiry, swaps.max_miner_fee, swaps.max_swap_fee, swaps.initiation_height, swaps.protocol_version, swaps.label,
|
||||
static_address_swaps.id, static_address_swaps.swap_hash, static_address_swaps.swap_invoice, static_address_swaps.last_hop, static_address_swaps.payment_timeout_seconds, static_address_swaps.quoted_swap_fee_satoshis, static_address_swaps.deposit_outpoints, static_address_swaps.htlc_tx_fee_rate_sat_kw, static_address_swaps.htlc_timeout_sweep_tx_id, static_address_swaps.htlc_timeout_sweep_address, static_address_swaps.selected_amount, static_address_swaps.fast, static_address_swaps.confirmation_risk_decision, static_address_swaps.confirmation_risk_decision_time,
|
||||
htlc_keys.swap_hash, htlc_keys.sender_script_pubkey, htlc_keys.receiver_script_pubkey, htlc_keys.sender_internal_pubkey, htlc_keys.receiver_internal_pubkey, htlc_keys.client_key_family, htlc_keys.client_key_index
|
||||
static_address_swaps.id, static_address_swaps.swap_hash, static_address_swaps.swap_invoice, static_address_swaps.last_hop, static_address_swaps.payment_timeout_seconds, static_address_swaps.quoted_swap_fee_satoshis, static_address_swaps.deposit_outpoints, static_address_swaps.htlc_tx_fee_rate_sat_kw, static_address_swaps.htlc_timeout_sweep_tx_id, static_address_swaps.htlc_timeout_sweep_address, static_address_swaps.selected_amount, static_address_swaps.fast, static_address_swaps.confirmation_risk_decision, static_address_swaps.confirmation_risk_decision_time, static_address_swaps.change_static_address_id, static_address_swaps.confirmed_htlc_tx_id, static_address_swaps.confirmed_htlc_output_index, static_address_swaps.confirmed_htlc_output_value,
|
||||
htlc_keys.swap_hash, htlc_keys.sender_script_pubkey, htlc_keys.receiver_script_pubkey, htlc_keys.sender_internal_pubkey, htlc_keys.receiver_internal_pubkey, htlc_keys.client_key_family, htlc_keys.client_key_index,
|
||||
change_address.client_pubkey change_client_pubkey,
|
||||
change_address.server_pubkey change_server_pubkey,
|
||||
change_address.expiry change_expiry,
|
||||
change_address.client_key_family change_client_key_family,
|
||||
change_address.client_key_index change_client_key_index,
|
||||
change_address.pkscript change_pkscript,
|
||||
change_address.protocol_version change_protocol_version,
|
||||
change_address.initiation_height change_initiation_height
|
||||
FROM
|
||||
swaps
|
||||
JOIN
|
||||
static_address_swaps ON swaps.swap_hash = static_address_swaps.swap_hash
|
||||
JOIN
|
||||
htlc_keys ON swaps.swap_hash = htlc_keys.swap_hash
|
||||
LEFT JOIN
|
||||
static_addresses change_address
|
||||
ON static_address_swaps.change_static_address_id = change_address.id
|
||||
WHERE
|
||||
swaps.swap_hash = $1
|
||||
`
|
||||
|
|
@ -218,6 +229,10 @@ type GetStaticAddressLoopInSwapRow struct {
|
|||
Fast bool
|
||||
ConfirmationRiskDecision string
|
||||
ConfirmationRiskDecisionTime sql.NullTime
|
||||
ChangeStaticAddressID sql.NullInt32
|
||||
ConfirmedHtlcTxID sql.NullString
|
||||
ConfirmedHtlcOutputIndex sql.NullInt32
|
||||
ConfirmedHtlcOutputValue sql.NullInt64
|
||||
SwapHash_3 []byte
|
||||
SenderScriptPubkey []byte
|
||||
ReceiverScriptPubkey []byte
|
||||
|
|
@ -225,6 +240,14 @@ type GetStaticAddressLoopInSwapRow struct {
|
|||
ReceiverInternalPubkey []byte
|
||||
ClientKeyFamily int32
|
||||
ClientKeyIndex int32
|
||||
ChangeClientPubkey []byte
|
||||
ChangeServerPubkey []byte
|
||||
ChangeExpiry sql.NullInt32
|
||||
ChangeClientKeyFamily sql.NullInt32
|
||||
ChangeClientKeyIndex sql.NullInt32
|
||||
ChangePkscript []byte
|
||||
ChangeProtocolVersion sql.NullInt32
|
||||
ChangeInitiationHeight sql.NullInt32
|
||||
}
|
||||
|
||||
func (q *Queries) GetStaticAddressLoopInSwap(ctx context.Context, swapHash []byte) (GetStaticAddressLoopInSwapRow, error) {
|
||||
|
|
@ -256,6 +279,10 @@ func (q *Queries) GetStaticAddressLoopInSwap(ctx context.Context, swapHash []byt
|
|||
&i.Fast,
|
||||
&i.ConfirmationRiskDecision,
|
||||
&i.ConfirmationRiskDecisionTime,
|
||||
&i.ChangeStaticAddressID,
|
||||
&i.ConfirmedHtlcTxID,
|
||||
&i.ConfirmedHtlcOutputIndex,
|
||||
&i.ConfirmedHtlcOutputValue,
|
||||
&i.SwapHash_3,
|
||||
&i.SenderScriptPubkey,
|
||||
&i.ReceiverScriptPubkey,
|
||||
|
|
@ -263,6 +290,14 @@ func (q *Queries) GetStaticAddressLoopInSwap(ctx context.Context, swapHash []byt
|
|||
&i.ReceiverInternalPubkey,
|
||||
&i.ClientKeyFamily,
|
||||
&i.ClientKeyIndex,
|
||||
&i.ChangeClientPubkey,
|
||||
&i.ChangeServerPubkey,
|
||||
&i.ChangeExpiry,
|
||||
&i.ChangeClientKeyFamily,
|
||||
&i.ChangeClientKeyIndex,
|
||||
&i.ChangePkscript,
|
||||
&i.ChangeProtocolVersion,
|
||||
&i.ChangeInitiationHeight,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
|
@ -270,14 +305,25 @@ func (q *Queries) GetStaticAddressLoopInSwap(ctx context.Context, swapHash []byt
|
|||
const getStaticAddressLoopInSwapsByStates = `-- name: GetStaticAddressLoopInSwapsByStates :many
|
||||
SELECT
|
||||
swaps.id, swaps.swap_hash, swaps.preimage, swaps.initiation_time, swaps.amount_requested, swaps.cltv_expiry, swaps.max_miner_fee, swaps.max_swap_fee, swaps.initiation_height, swaps.protocol_version, swaps.label,
|
||||
static_address_swaps.id, static_address_swaps.swap_hash, static_address_swaps.swap_invoice, static_address_swaps.last_hop, static_address_swaps.payment_timeout_seconds, static_address_swaps.quoted_swap_fee_satoshis, static_address_swaps.deposit_outpoints, static_address_swaps.htlc_tx_fee_rate_sat_kw, static_address_swaps.htlc_timeout_sweep_tx_id, static_address_swaps.htlc_timeout_sweep_address, static_address_swaps.selected_amount, static_address_swaps.fast, static_address_swaps.confirmation_risk_decision, static_address_swaps.confirmation_risk_decision_time,
|
||||
htlc_keys.swap_hash, htlc_keys.sender_script_pubkey, htlc_keys.receiver_script_pubkey, htlc_keys.sender_internal_pubkey, htlc_keys.receiver_internal_pubkey, htlc_keys.client_key_family, htlc_keys.client_key_index
|
||||
static_address_swaps.id, static_address_swaps.swap_hash, static_address_swaps.swap_invoice, static_address_swaps.last_hop, static_address_swaps.payment_timeout_seconds, static_address_swaps.quoted_swap_fee_satoshis, static_address_swaps.deposit_outpoints, static_address_swaps.htlc_tx_fee_rate_sat_kw, static_address_swaps.htlc_timeout_sweep_tx_id, static_address_swaps.htlc_timeout_sweep_address, static_address_swaps.selected_amount, static_address_swaps.fast, static_address_swaps.confirmation_risk_decision, static_address_swaps.confirmation_risk_decision_time, static_address_swaps.change_static_address_id, static_address_swaps.confirmed_htlc_tx_id, static_address_swaps.confirmed_htlc_output_index, static_address_swaps.confirmed_htlc_output_value,
|
||||
htlc_keys.swap_hash, htlc_keys.sender_script_pubkey, htlc_keys.receiver_script_pubkey, htlc_keys.sender_internal_pubkey, htlc_keys.receiver_internal_pubkey, htlc_keys.client_key_family, htlc_keys.client_key_index,
|
||||
change_address.client_pubkey change_client_pubkey,
|
||||
change_address.server_pubkey change_server_pubkey,
|
||||
change_address.expiry change_expiry,
|
||||
change_address.client_key_family change_client_key_family,
|
||||
change_address.client_key_index change_client_key_index,
|
||||
change_address.pkscript change_pkscript,
|
||||
change_address.protocol_version change_protocol_version,
|
||||
change_address.initiation_height change_initiation_height
|
||||
FROM
|
||||
swaps
|
||||
JOIN
|
||||
static_address_swaps ON swaps.swap_hash = static_address_swaps.swap_hash
|
||||
JOIN
|
||||
htlc_keys ON swaps.swap_hash = htlc_keys.swap_hash
|
||||
LEFT JOIN
|
||||
static_addresses change_address
|
||||
ON static_address_swaps.change_static_address_id = change_address.id
|
||||
JOIN
|
||||
static_address_swap_updates u ON swaps.swap_hash = u.swap_hash
|
||||
-- This subquery ensures that we are checking only the latest update for
|
||||
|
|
@ -319,6 +365,10 @@ type GetStaticAddressLoopInSwapsByStatesRow struct {
|
|||
Fast bool
|
||||
ConfirmationRiskDecision string
|
||||
ConfirmationRiskDecisionTime sql.NullTime
|
||||
ChangeStaticAddressID sql.NullInt32
|
||||
ConfirmedHtlcTxID sql.NullString
|
||||
ConfirmedHtlcOutputIndex sql.NullInt32
|
||||
ConfirmedHtlcOutputValue sql.NullInt64
|
||||
SwapHash_3 []byte
|
||||
SenderScriptPubkey []byte
|
||||
ReceiverScriptPubkey []byte
|
||||
|
|
@ -326,6 +376,14 @@ type GetStaticAddressLoopInSwapsByStatesRow struct {
|
|||
ReceiverInternalPubkey []byte
|
||||
ClientKeyFamily int32
|
||||
ClientKeyIndex int32
|
||||
ChangeClientPubkey []byte
|
||||
ChangeServerPubkey []byte
|
||||
ChangeExpiry sql.NullInt32
|
||||
ChangeClientKeyFamily sql.NullInt32
|
||||
ChangeClientKeyIndex sql.NullInt32
|
||||
ChangePkscript []byte
|
||||
ChangeProtocolVersion sql.NullInt32
|
||||
ChangeInitiationHeight sql.NullInt32
|
||||
}
|
||||
|
||||
func (q *Queries) GetStaticAddressLoopInSwapsByStates(ctx context.Context, dollar_1 sql.NullString) ([]GetStaticAddressLoopInSwapsByStatesRow, error) {
|
||||
|
|
@ -363,6 +421,10 @@ func (q *Queries) GetStaticAddressLoopInSwapsByStates(ctx context.Context, dolla
|
|||
&i.Fast,
|
||||
&i.ConfirmationRiskDecision,
|
||||
&i.ConfirmationRiskDecisionTime,
|
||||
&i.ChangeStaticAddressID,
|
||||
&i.ConfirmedHtlcTxID,
|
||||
&i.ConfirmedHtlcOutputIndex,
|
||||
&i.ConfirmedHtlcOutputValue,
|
||||
&i.SwapHash_3,
|
||||
&i.SenderScriptPubkey,
|
||||
&i.ReceiverScriptPubkey,
|
||||
|
|
@ -370,6 +432,14 @@ func (q *Queries) GetStaticAddressLoopInSwapsByStates(ctx context.Context, dolla
|
|||
&i.ReceiverInternalPubkey,
|
||||
&i.ClientKeyFamily,
|
||||
&i.ClientKeyIndex,
|
||||
&i.ChangeClientPubkey,
|
||||
&i.ChangeServerPubkey,
|
||||
&i.ChangeExpiry,
|
||||
&i.ChangeClientKeyFamily,
|
||||
&i.ChangeClientKeyIndex,
|
||||
&i.ChangePkscript,
|
||||
&i.ChangeProtocolVersion,
|
||||
&i.ChangeInitiationHeight,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -396,7 +466,8 @@ INSERT INTO static_address_swaps (
|
|||
htlc_tx_fee_rate_sat_kw,
|
||||
htlc_timeout_sweep_tx_id,
|
||||
htlc_timeout_sweep_address,
|
||||
fast
|
||||
fast,
|
||||
change_static_address_id
|
||||
) VALUES (
|
||||
$1,
|
||||
$2,
|
||||
|
|
@ -408,7 +479,8 @@ INSERT INTO static_address_swaps (
|
|||
$8,
|
||||
$9,
|
||||
$10,
|
||||
$11
|
||||
$11,
|
||||
$12
|
||||
)
|
||||
`
|
||||
|
||||
|
|
@ -424,6 +496,7 @@ type InsertStaticAddressLoopInParams struct {
|
|||
HtlcTimeoutSweepTxID sql.NullString
|
||||
HtlcTimeoutSweepAddress string
|
||||
Fast bool
|
||||
ChangeStaticAddressID sql.NullInt32
|
||||
}
|
||||
|
||||
func (q *Queries) InsertStaticAddressLoopIn(ctx context.Context, arg InsertStaticAddressLoopInParams) error {
|
||||
|
|
@ -439,6 +512,7 @@ func (q *Queries) InsertStaticAddressLoopIn(ctx context.Context, arg InsertStati
|
|||
arg.HtlcTimeoutSweepTxID,
|
||||
arg.HtlcTimeoutSweepAddress,
|
||||
arg.Fast,
|
||||
arg.ChangeStaticAddressID,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
|
@ -565,18 +639,31 @@ const updateStaticAddressLoopIn = `-- name: UpdateStaticAddressLoopIn :exec
|
|||
UPDATE static_address_swaps
|
||||
SET
|
||||
htlc_tx_fee_rate_sat_kw = $2,
|
||||
htlc_timeout_sweep_tx_id = $3
|
||||
htlc_timeout_sweep_tx_id = $3,
|
||||
confirmed_htlc_tx_id = $4,
|
||||
confirmed_htlc_output_index = $5,
|
||||
confirmed_htlc_output_value = $6
|
||||
WHERE
|
||||
swap_hash = $1
|
||||
`
|
||||
|
||||
type UpdateStaticAddressLoopInParams struct {
|
||||
SwapHash []byte
|
||||
HtlcTxFeeRateSatKw int64
|
||||
HtlcTimeoutSweepTxID sql.NullString
|
||||
SwapHash []byte
|
||||
HtlcTxFeeRateSatKw int64
|
||||
HtlcTimeoutSweepTxID sql.NullString
|
||||
ConfirmedHtlcTxID sql.NullString
|
||||
ConfirmedHtlcOutputIndex sql.NullInt32
|
||||
ConfirmedHtlcOutputValue sql.NullInt64
|
||||
}
|
||||
|
||||
func (q *Queries) UpdateStaticAddressLoopIn(ctx context.Context, arg UpdateStaticAddressLoopInParams) error {
|
||||
_, err := q.db.ExecContext(ctx, updateStaticAddressLoopIn, arg.SwapHash, arg.HtlcTxFeeRateSatKw, arg.HtlcTimeoutSweepTxID)
|
||||
_, err := q.db.ExecContext(ctx, updateStaticAddressLoopIn,
|
||||
arg.SwapHash,
|
||||
arg.HtlcTxFeeRateSatKw,
|
||||
arg.HtlcTimeoutSweepTxID,
|
||||
arg.ConfirmedHtlcTxID,
|
||||
arg.ConfirmedHtlcOutputIndex,
|
||||
arg.ConfirmedHtlcOutputValue,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package loopin
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"errors"
|
||||
|
|
@ -109,6 +110,29 @@ func (f *FSM) InitHtlcAction(ctx context.Context,
|
|||
}
|
||||
swapInvoiceAmt := swapAmount - f.loopIn.QuotedSwapFee
|
||||
|
||||
var changeOutput *swapserverrpc.StaticAddressChangeOutput
|
||||
if hasChange {
|
||||
changeAmount := f.loopIn.ExpectedChangeAmount()
|
||||
f.loopIn.ChangeAddressParams, err =
|
||||
f.cfg.AddressManager.NewChangeAddress(ctx)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("unable to create static address "+
|
||||
"change output: %w", err)
|
||||
|
||||
return returnError(err)
|
||||
}
|
||||
|
||||
changeOutput, err = staticutil.ChangeOutput(
|
||||
f.loopIn.ChangeAddressParams, changeAmount,
|
||||
)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("unable to prepare static address "+
|
||||
"change output: %w", err)
|
||||
|
||||
return returnError(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Generate random preimage.
|
||||
var swapPreimage lntypes.Preimage
|
||||
if _, err = rand.Read(swapPreimage[:]); err != nil {
|
||||
|
|
@ -179,6 +203,7 @@ func (f *FSM) InitHtlcAction(ctx context.Context,
|
|||
PaymentTimeoutSeconds: f.loopIn.PaymentTimeoutSeconds,
|
||||
Fast: f.loopIn.Fast,
|
||||
DepositToClientPubkeys: depositClientPubkeys,
|
||||
ChangeOutput: changeOutput,
|
||||
}
|
||||
if f.loopIn.LastHop != nil {
|
||||
loopInReq.LastHop = f.loopIn.LastHop
|
||||
|
|
@ -1147,9 +1172,14 @@ func (f *FSM) MonitorInvoiceAndHtlcTxAction(ctx context.Context,
|
|||
htlcConfirmed := false
|
||||
for {
|
||||
select {
|
||||
case <-htlcConfChan:
|
||||
case conf := <-htlcConfChan:
|
||||
f.Infof("htlc tx confirmed")
|
||||
|
||||
err = f.recordConfirmedHtlc(ctx, conf, htlc.PkScript)
|
||||
if err != nil {
|
||||
return f.HandleError(err)
|
||||
}
|
||||
|
||||
htlcConfirmed = true
|
||||
if invoiceCanceledForNonPayment {
|
||||
err = transitionDepositsToHtlcTimeout(
|
||||
|
|
@ -1190,6 +1220,10 @@ func (f *FSM) MonitorInvoiceAndHtlcTxAction(ctx context.Context,
|
|||
// confirmation and re-register for the next
|
||||
// confirmation.
|
||||
htlcConfirmed = false
|
||||
err = f.clearConfirmedHtlc(ctx)
|
||||
if err != nil {
|
||||
return f.HandleError(err)
|
||||
}
|
||||
|
||||
htlcConfChan, htlcErrConfChan, err = registerHtlcConf()
|
||||
if err != nil {
|
||||
|
|
@ -1367,6 +1401,51 @@ func (f *FSM) MonitorInvoiceAndHtlcTxAction(ctx context.Context,
|
|||
}
|
||||
}
|
||||
|
||||
func (f *FSM) recordConfirmedHtlc(ctx context.Context,
|
||||
conf *chainntnfs.TxConfirmation, htlcPkScript []byte) error {
|
||||
|
||||
if conf == nil || conf.Tx == nil {
|
||||
return errors.New("htlc confirmation missing transaction")
|
||||
}
|
||||
if f.cfg.Store == nil {
|
||||
return errors.New("missing static address loop-in store")
|
||||
}
|
||||
|
||||
tx := conf.Tx
|
||||
txHash := tx.TxHash()
|
||||
for idx, txOut := range tx.TxOut {
|
||||
if !bytes.Equal(txOut.PkScript, htlcPkScript) {
|
||||
continue
|
||||
}
|
||||
|
||||
f.loopIn.HtlcTxHash = &txHash
|
||||
f.loopIn.HtlcOutputIndex = uint32(idx)
|
||||
f.loopIn.HtlcOutputValue = btcutil.Amount(txOut.Value)
|
||||
|
||||
return f.cfg.Store.UpdateLoopIn(ctx, f.loopIn)
|
||||
}
|
||||
|
||||
return fmt.Errorf("confirmed htlc tx %v missing expected htlc "+
|
||||
"output", txHash)
|
||||
}
|
||||
|
||||
func (f *FSM) clearConfirmedHtlc(ctx context.Context) error {
|
||||
if f.loopIn.HtlcTxHash == nil && f.loopIn.HtlcOutputIndex == 0 &&
|
||||
f.loopIn.HtlcOutputValue == 0 {
|
||||
|
||||
return nil
|
||||
}
|
||||
if f.cfg.Store == nil {
|
||||
return errors.New("missing static address loop-in store")
|
||||
}
|
||||
|
||||
f.loopIn.HtlcTxHash = nil
|
||||
f.loopIn.HtlcOutputIndex = 0
|
||||
f.loopIn.HtlcOutputValue = 0
|
||||
|
||||
return f.cfg.Store.UpdateLoopIn(ctx, f.loopIn)
|
||||
}
|
||||
|
||||
// htlcTimeoutSweepRetryDelay is the delay between retries when publishing the
|
||||
// htlc timeout sweep transaction fails.
|
||||
const htlcTimeoutSweepRetryDelay = time.Hour
|
||||
|
|
|
|||
|
|
@ -12,12 +12,14 @@ import (
|
|||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/lightninglabs/lndclient"
|
||||
"github.com/lightninglabs/loop/fsm"
|
||||
"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/swapserverrpc"
|
||||
"github.com/lightninglabs/loop/test"
|
||||
"github.com/lightningnetwork/lnd/chainntnfs"
|
||||
"github.com/lightningnetwork/lnd/invoices"
|
||||
"github.com/lightningnetwork/lnd/lntypes"
|
||||
"github.com/lightningnetwork/lnd/zpay32"
|
||||
|
|
@ -899,6 +901,7 @@ func TestCheckDepositsAvailableRejectsDivergentDepositOutpoints(
|
|||
// update failure must not roll back the action or state transition.
|
||||
func TestInitHtlcActionIgnoresSendUpdateErrorAfterPersistence(t *testing.T) {
|
||||
mockLnd := test.NewMockLnd()
|
||||
_, depositClientPubkey := test.CreateKey(21)
|
||||
_, serverKey := test.CreateKey(22)
|
||||
|
||||
server := &mockStaticAddressServer{
|
||||
|
|
@ -913,6 +916,10 @@ func TestInitHtlcActionIgnoresSendUpdateErrorAfterPersistence(t *testing.T) {
|
|||
Index: 0,
|
||||
},
|
||||
Value: 500_000,
|
||||
AddressParams: &address.Parameters{
|
||||
ClientPubkey: depositClientPubkey,
|
||||
PkScript: []byte{0x51, 0x20, 0x02},
|
||||
},
|
||||
}
|
||||
|
||||
loopIn := &StaticAddressLoopIn{
|
||||
|
|
@ -933,6 +940,7 @@ func TestInitHtlcActionIgnoresSendUpdateErrorAfterPersistence(t *testing.T) {
|
|||
Server: server,
|
||||
DepositManager: &noopDepositManager{},
|
||||
LndClient: mockLnd.Client,
|
||||
InvoicesClient: mockLnd.LndServices.Invoices,
|
||||
WalletKit: mockLnd.WalletKit,
|
||||
ChainParams: mockLnd.ChainParams,
|
||||
Store: &mockStore{},
|
||||
|
|
@ -956,6 +964,82 @@ func TestInitHtlcActionIgnoresSendUpdateErrorAfterPersistence(t *testing.T) {
|
|||
require.True(t, sendUpdateCalled)
|
||||
}
|
||||
|
||||
// TestInitHtlcActionSendsChangeOutput asserts that fractional loop-ins create
|
||||
// and send an operation-specific static change output to the server.
|
||||
func TestInitHtlcActionSendsChangeOutput(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
mockLnd := test.NewMockLnd()
|
||||
_, depositClientPubkey := test.CreateKey(31)
|
||||
_, changeClientPubkey := test.CreateKey(32)
|
||||
_, serverKey := test.CreateKey(33)
|
||||
|
||||
server := &mockStaticAddressServer{
|
||||
response: testStaticAddressLoopInResponse(
|
||||
serverKey.SerializeCompressed(),
|
||||
),
|
||||
}
|
||||
|
||||
dep := &deposit.Deposit{
|
||||
OutPoint: wire.OutPoint{
|
||||
Hash: chainhash.Hash{3},
|
||||
Index: 0,
|
||||
},
|
||||
Value: 500_000,
|
||||
AddressParams: &address.Parameters{
|
||||
ClientPubkey: depositClientPubkey,
|
||||
PkScript: []byte{0x51, 0x20, 0x02},
|
||||
},
|
||||
}
|
||||
changeParams := &address.Parameters{
|
||||
ID: 1,
|
||||
ClientPubkey: changeClientPubkey,
|
||||
PkScript: []byte{0x51, 0x20, 0x01},
|
||||
}
|
||||
|
||||
loopIn := &StaticAddressLoopIn{
|
||||
Deposits: []*deposit.Deposit{dep},
|
||||
DepositOutpoints: []string{dep.OutPoint.String()},
|
||||
SelectedAmount: 300_000,
|
||||
QuotedSwapFee: 1_000,
|
||||
InitiationHeight: uint32(mockLnd.Height),
|
||||
InitiationTime: time.Now(),
|
||||
PaymentTimeoutSeconds: 3_600,
|
||||
}
|
||||
|
||||
f := &FSM{
|
||||
StateMachine: &fsm.StateMachine{},
|
||||
cfg: &Config{
|
||||
Server: server,
|
||||
AddressManager: &mockAddressManager{params: changeParams},
|
||||
DepositManager: &noopDepositManager{},
|
||||
LndClient: mockLnd.Client,
|
||||
WalletKit: mockLnd.WalletKit,
|
||||
ChainParams: mockLnd.ChainParams,
|
||||
Store: &mockStore{},
|
||||
ValidateLoopInContract: testValidateLoopInContract,
|
||||
MaxStaticAddrHtlcFeePercentage: 1,
|
||||
MaxStaticAddrHtlcBackupFeePercentage: 1,
|
||||
},
|
||||
loopIn: loopIn,
|
||||
}
|
||||
|
||||
event := f.InitHtlcAction(t.Context(), nil)
|
||||
require.Equal(t, OnHtlcInitiated, event)
|
||||
require.Nil(t, f.LastActionError)
|
||||
require.NotNil(t, server.request.ChangeOutput)
|
||||
require.EqualValues(t, 200_000, server.request.ChangeOutput.Amount)
|
||||
require.Equal(
|
||||
t, changeClientPubkey.SerializeCompressed(),
|
||||
server.request.ChangeOutput.StaticAddress.GetPubkey(),
|
||||
)
|
||||
require.Equal(
|
||||
t, changeParams.PkScript,
|
||||
server.request.ChangeOutput.StaticAddress.GetPkScript(),
|
||||
)
|
||||
require.Same(t, changeParams, loopIn.ChangeAddressParams)
|
||||
}
|
||||
|
||||
// mockStaticAddressServer captures static-address loop-in requests in tests.
|
||||
type mockStaticAddressServer struct {
|
||||
swapserverrpc.StaticAddressServerClient
|
||||
|
|
@ -994,6 +1078,70 @@ func testStaticAddressLoopInResponse(
|
|||
}
|
||||
}
|
||||
|
||||
type recordingLoopInStore struct {
|
||||
mockStore
|
||||
|
||||
updates []*StaticAddressLoopIn
|
||||
}
|
||||
|
||||
func (s *recordingLoopInStore) UpdateLoopIn(_ context.Context,
|
||||
loopIn *StaticAddressLoopIn) error {
|
||||
|
||||
s.updates = append(s.updates, loopIn)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// TestRecordConfirmedHtlcPersistsOutpoint verifies that the FSM records the
|
||||
// exact confirmed server HTLC output before the timeout branch can sweep it.
|
||||
func TestRecordConfirmedHtlcPersistsOutpoint(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
clientKey, err := btcec.NewPrivateKey()
|
||||
require.NoError(t, err)
|
||||
serverKey, err := btcec.NewPrivateKey()
|
||||
require.NoError(t, err)
|
||||
|
||||
loopIn := &StaticAddressLoopIn{
|
||||
SwapHash: lntypes.Hash{1, 2, 4},
|
||||
HtlcCltvExpiry: 800,
|
||||
ClientPubkey: clientKey.PubKey(),
|
||||
ServerPubkey: serverKey.PubKey(),
|
||||
}
|
||||
htlc, err := loopIn.getHtlc(test.NewMockLnd().ChainParams)
|
||||
require.NoError(t, err)
|
||||
|
||||
htlcValue := int64(123_456)
|
||||
tx := wire.NewMsgTx(2)
|
||||
tx.AddTxOut(&wire.TxOut{
|
||||
Value: 1,
|
||||
PkScript: []byte{0x51},
|
||||
})
|
||||
tx.AddTxOut(&wire.TxOut{
|
||||
Value: htlcValue,
|
||||
PkScript: htlc.PkScript,
|
||||
})
|
||||
|
||||
store := &recordingLoopInStore{}
|
||||
f := &FSM{
|
||||
cfg: &Config{Store: store},
|
||||
loopIn: loopIn,
|
||||
}
|
||||
|
||||
err = f.recordConfirmedHtlc(
|
||||
t.Context(), &chainntnfs.TxConfirmation{Tx: tx},
|
||||
htlc.PkScript,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
txHash := tx.TxHash()
|
||||
require.NotNil(t, loopIn.HtlcTxHash)
|
||||
require.Equal(t, txHash, *loopIn.HtlcTxHash)
|
||||
require.EqualValues(t, 1, loopIn.HtlcOutputIndex)
|
||||
require.EqualValues(t, htlcValue, loopIn.HtlcOutputValue)
|
||||
require.Len(t, store.updates, 1)
|
||||
}
|
||||
|
||||
// testStaticAddressRouteHints returns deterministic route hints for static
|
||||
// loop-in invoice regression tests.
|
||||
func testStaticAddressRouteHints() [][]zpay32.HopHint {
|
||||
|
|
@ -1102,10 +1250,18 @@ func TestMonitorInvoiceAndHtlcTxLocksConfirmedHtlcAtDeadline(t *testing.T) {
|
|||
LndClient: mockLnd.Client,
|
||||
ChainParams: mockLnd.ChainParams,
|
||||
NotificationManager: notificationMgr,
|
||||
Store: &recordingLoopInStore{},
|
||||
}
|
||||
|
||||
f, err := NewFSM(ctx, loopIn, cfg, false)
|
||||
require.NoError(t, err)
|
||||
htlc, err := loopIn.getHtlc(mockLnd.ChainParams)
|
||||
require.NoError(t, err)
|
||||
htlcTx := wire.NewMsgTx(2)
|
||||
htlcTx.AddTxOut(&wire.TxOut{
|
||||
Value: 1,
|
||||
PkScript: htlc.PkScript,
|
||||
})
|
||||
|
||||
resultChan := make(chan fsm.EventType, 1)
|
||||
go func() {
|
||||
|
|
@ -1124,7 +1280,7 @@ func TestMonitorInvoiceAndHtlcTxLocksConfirmedHtlcAtDeadline(t *testing.T) {
|
|||
case <-ctx.Done():
|
||||
t.Fatalf("htlc conf registration not received: %v", ctx.Err())
|
||||
}
|
||||
confRegistration.ConfChan <- nil
|
||||
confRegistration.ConfChan <- &chainntnfs.TxConfirmation{Tx: htlcTx}
|
||||
|
||||
select {
|
||||
case hash := <-mockLnd.FailInvoiceChannel:
|
||||
|
|
@ -3367,6 +3523,13 @@ func (m *mockAddressManager) GetStaticAddress(_ context.Context) (
|
|||
return nil, nil
|
||||
}
|
||||
|
||||
// NewChangeAddress returns configured parameters for tests that need change.
|
||||
func (m *mockAddressManager) NewChangeAddress(_ context.Context) (
|
||||
*address.Parameters, error) {
|
||||
|
||||
return m.params, nil
|
||||
}
|
||||
|
||||
// noopDepositManager is a stub DepositManager used to satisfy FSM config.
|
||||
type noopDepositManager struct {
|
||||
deposits []*deposit.Deposit
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import (
|
|||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/lightninglabs/loop"
|
||||
"github.com/lightninglabs/loop/fsm"
|
||||
"github.com/lightninglabs/loop/staticaddr/address"
|
||||
"github.com/lightninglabs/loop/staticaddr/deposit"
|
||||
"github.com/lightninglabs/loop/staticaddr/script"
|
||||
"github.com/lightninglabs/loop/swapserverrpc"
|
||||
|
|
@ -41,6 +42,10 @@ type AddressManager interface {
|
|||
// GetStaticAddress returns the deposit address for the given client and
|
||||
// server public keys.
|
||||
GetStaticAddress(ctx context.Context) (*script.StaticAddress, error)
|
||||
|
||||
// NewChangeAddress derives and persists a fresh static address from the
|
||||
// change key family for this operation's change output.
|
||||
NewChangeAddress(ctx context.Context) (*address.Parameters, error)
|
||||
}
|
||||
|
||||
// DepositManager handles the interaction of loop-ins with deposits.
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import (
|
|||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/lightninglabs/lndclient"
|
||||
"github.com/lightninglabs/loop/fsm"
|
||||
"github.com/lightninglabs/loop/staticaddr/address"
|
||||
"github.com/lightninglabs/loop/staticaddr/deposit"
|
||||
"github.com/lightninglabs/loop/staticaddr/script"
|
||||
"github.com/lightninglabs/loop/staticaddr/staticutil"
|
||||
|
|
@ -166,6 +167,11 @@ type StaticAddressLoopIn struct {
|
|||
// Address is the address script that is used for the swap.
|
||||
Address *script.StaticAddress
|
||||
|
||||
// ChangeAddressParams are the static address parameters for the change
|
||||
// output that belongs to this swap. It is set only when SelectedAmount
|
||||
// leaves non-dust change.
|
||||
ChangeAddressParams *address.Parameters
|
||||
|
||||
// HTLC fields.
|
||||
|
||||
// HtlcTxFeeRate is the fee rate that is used for the htlc transaction.
|
||||
|
|
@ -182,6 +188,16 @@ type StaticAddressLoopIn struct {
|
|||
// HtlcTimeoutSweepTxHash is the hash of the htlc timeout sweep tx.
|
||||
HtlcTimeoutSweepTxHash *chainhash.Hash
|
||||
|
||||
// HtlcTxHash is the hash of the confirmed htlc tx published by the
|
||||
// server.
|
||||
HtlcTxHash *chainhash.Hash
|
||||
|
||||
// HtlcOutputIndex is the output index of the confirmed htlc output.
|
||||
HtlcOutputIndex uint32
|
||||
|
||||
// HtlcOutputValue is the value of the confirmed htlc output.
|
||||
HtlcOutputValue btcutil.Amount
|
||||
|
||||
// HtlcTimeoutSweepAddress
|
||||
HtlcTimeoutSweepAddress btcutil.Address
|
||||
|
||||
|
|
@ -306,11 +322,10 @@ func (l *StaticAddressLoopIn) createHtlcTx(chainParams *chaincfg.Params,
|
|||
// change.
|
||||
var (
|
||||
swapAmt = l.TotalDepositAmount()
|
||||
changeAmount btcutil.Amount
|
||||
changeAmount = l.ExpectedChangeAmount()
|
||||
)
|
||||
if l.SelectedAmount > 0 {
|
||||
swapAmt = l.SelectedAmount
|
||||
changeAmount = l.TotalDepositAmount() - l.SelectedAmount
|
||||
}
|
||||
|
||||
// Calculate htlc tx fee for server provided fee rate.
|
||||
|
|
@ -346,9 +361,14 @@ func (l *StaticAddressLoopIn) createHtlcTx(chainParams *chaincfg.Params,
|
|||
|
||||
// We expect change to be sent back to our static address output script.
|
||||
if changeAmount > 0 {
|
||||
if l.ChangeAddressParams == nil {
|
||||
return nil, fmt.Errorf("missing static address change " +
|
||||
"parameters")
|
||||
}
|
||||
|
||||
msgTx.AddTxOut(&wire.TxOut{
|
||||
Value: int64(changeAmount),
|
||||
PkScript: l.AddressParams.PkScript,
|
||||
PkScript: l.ChangeAddressParams.PkScript,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -408,36 +428,18 @@ func (l *StaticAddressLoopIn) createHtlcSweepTx(ctx context.Context,
|
|||
return nil, err
|
||||
}
|
||||
|
||||
htlcTx, err := l.createHtlcTx(
|
||||
network, l.HtlcTxFeeRate, maxFeePercentage,
|
||||
htlcOutpoint, htlcOutValue, err := l.confirmedHtlcOutpoint(
|
||||
network, maxFeePercentage,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// The HTLC output is always at index 0 (createHtlcTx adds it first).
|
||||
// If there is a change output, it is at index 1. Verify this invariant
|
||||
// so we fail fast if createHtlcTx's layout ever changes.
|
||||
const htlcInputIndex = uint32(0)
|
||||
if len(htlcTx.TxOut) == 2 {
|
||||
if bytes.Equal(
|
||||
htlcTx.TxOut[0].PkScript, l.AddressParams.PkScript,
|
||||
) {
|
||||
|
||||
return nil, fmt.Errorf("htlc tx output layout " +
|
||||
"invariant violated: expected HTLC output " +
|
||||
"at index 0, got change output")
|
||||
}
|
||||
}
|
||||
|
||||
// Add the htlc input.
|
||||
sweepTx.AddTxIn(&wire.TxIn{
|
||||
PreviousOutPoint: wire.OutPoint{
|
||||
Hash: htlcTx.TxHash(),
|
||||
Index: htlcInputIndex,
|
||||
},
|
||||
SignatureScript: htlc.SigScript,
|
||||
Sequence: htlc.SuccessSequence(),
|
||||
PreviousOutPoint: htlcOutpoint,
|
||||
SignatureScript: htlc.SigScript,
|
||||
Sequence: htlc.SuccessSequence(),
|
||||
})
|
||||
|
||||
// Add the sweep output.
|
||||
|
|
@ -448,7 +450,6 @@ func (l *StaticAddressLoopIn) createHtlcSweepTx(ctx context.Context,
|
|||
|
||||
fee := feeRate.FeeForWeight(weightEstimator.Weight())
|
||||
|
||||
htlcOutValue := htlcTx.TxOut[htlcInputIndex].Value
|
||||
output := &wire.TxOut{
|
||||
Value: htlcOutValue - int64(fee),
|
||||
PkScript: sweepPkScript,
|
||||
|
|
@ -487,6 +488,56 @@ func (l *StaticAddressLoopIn) createHtlcSweepTx(ctx context.Context,
|
|||
return sweepTx, nil
|
||||
}
|
||||
|
||||
// confirmedHtlcOutpoint returns the exact confirmed htlc outpoint when it has
|
||||
// been persisted. Older loop-ins fall back to reconstructing the standard-fee
|
||||
// htlc tx, which was the historical behavior before we stored the actual
|
||||
// server-published variant.
|
||||
func (l *StaticAddressLoopIn) confirmedHtlcOutpoint(
|
||||
network *chaincfg.Params, maxFeePercentage float64) (wire.OutPoint,
|
||||
int64, error) {
|
||||
|
||||
if l.HtlcTxHash != nil {
|
||||
if l.HtlcOutputValue <= 0 {
|
||||
return wire.OutPoint{}, 0, fmt.Errorf("missing htlc "+
|
||||
"output value for confirmed htlc tx %v",
|
||||
l.HtlcTxHash)
|
||||
}
|
||||
|
||||
return wire.OutPoint{
|
||||
Hash: *l.HtlcTxHash,
|
||||
Index: l.HtlcOutputIndex,
|
||||
}, int64(l.HtlcOutputValue), nil
|
||||
}
|
||||
|
||||
htlcTx, err := l.createHtlcTx(
|
||||
network, l.HtlcTxFeeRate, maxFeePercentage,
|
||||
)
|
||||
if err != nil {
|
||||
return wire.OutPoint{}, 0, err
|
||||
}
|
||||
|
||||
// The HTLC output is always at index 0 (createHtlcTx adds it first).
|
||||
// If there is a change output, it is at index 1. Verify this invariant
|
||||
// so we fail fast if createHtlcTx's layout ever changes.
|
||||
const htlcInputIndex = uint32(0)
|
||||
if len(htlcTx.TxOut) == 2 && l.ChangeAddressParams != nil {
|
||||
if bytes.Equal(
|
||||
htlcTx.TxOut[0].PkScript,
|
||||
l.ChangeAddressParams.PkScript,
|
||||
) {
|
||||
|
||||
return wire.OutPoint{}, 0, fmt.Errorf("htlc tx " +
|
||||
"output layout invariant violated: expected " +
|
||||
"HTLC output at index 0, got change output")
|
||||
}
|
||||
}
|
||||
|
||||
return wire.OutPoint{
|
||||
Hash: htlcTx.TxHash(),
|
||||
Index: htlcInputIndex,
|
||||
}, htlcTx.TxOut[htlcInputIndex].Value, nil
|
||||
}
|
||||
|
||||
// pubkeyTo33ByteSlice converts a pubkey to a 33 byte slice.
|
||||
func pubkeyTo33ByteSlice(pubkey *btcec.PublicKey) [33]byte {
|
||||
var pubkeyBytes [33]byte
|
||||
|
|
@ -508,6 +559,22 @@ func (l *StaticAddressLoopIn) TotalDepositAmount() btcutil.Amount {
|
|||
return total
|
||||
}
|
||||
|
||||
// ExpectedChangeAmount returns the change that a fractional loop-in should send
|
||||
// to its generated static change address. A full-amount loop-in has no change.
|
||||
func (l *StaticAddressLoopIn) ExpectedChangeAmount() btcutil.Amount {
|
||||
if l.SelectedAmount <= 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
totalDepositAmount := l.TotalDepositAmount()
|
||||
changeAmount := totalDepositAmount - l.SelectedAmount
|
||||
if changeAmount <= 0 || changeAmount >= totalDepositAmount {
|
||||
return 0
|
||||
}
|
||||
|
||||
return changeAmount
|
||||
}
|
||||
|
||||
// RemainingPaymentTimeSeconds returns the remaining time in seconds until the
|
||||
// payment timeout is reached. The remaining time is calculated from the
|
||||
// initiation time of the swap. If more than the swap's configured payment
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import (
|
|||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/lightninglabs/lndclient"
|
||||
"github.com/lightninglabs/loop/staticaddr/address"
|
||||
"github.com/lightninglabs/loop/staticaddr/deposit"
|
||||
"github.com/lightninglabs/loop/staticaddr/script"
|
||||
"github.com/lightninglabs/loop/staticaddr/version"
|
||||
|
|
@ -77,7 +78,8 @@ func TestCreateHtlcSweepTxSweepValue(t *testing.T) {
|
|||
Hash: chainhash.Hash{0xaa},
|
||||
Index: 0,
|
||||
},
|
||||
Value: depositValue,
|
||||
Value: depositValue,
|
||||
AddressParams: addrParams,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -96,7 +98,7 @@ func TestCreateHtlcSweepTxSweepValue(t *testing.T) {
|
|||
ClientPubkey: clientKey.PubKey(),
|
||||
ServerPubkey: serverKey.PubKey(),
|
||||
Deposits: deposits,
|
||||
AddressParams: addrParams,
|
||||
ChangeAddressParams: addrParams,
|
||||
HtlcTxFeeRate: feeRate,
|
||||
SelectedAmount: selectedAmount,
|
||||
PaymentTimeoutSeconds: 3600,
|
||||
|
|
@ -183,6 +185,76 @@ func TestPaymentTimeoutDuration(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestCreateHtlcSweepTxUsesConfirmedHtlcOutpoint verifies that timeout sweeps
|
||||
// spend the actual server-published HTLC tx variant once it has been recorded.
|
||||
func TestCreateHtlcSweepTxUsesConfirmedHtlcOutpoint(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
clientKey, err := btcec.NewPrivateKey()
|
||||
require.NoError(t, err)
|
||||
serverKey, err := btcec.NewPrivateKey()
|
||||
require.NoError(t, err)
|
||||
|
||||
network := &chaincfg.RegressionNetParams
|
||||
staticAddr, err := newStaticAddress(
|
||||
clientKey.PubKey(), serverKey.PubKey(), 4032,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
pkScript, err := staticAddr.StaticAddressScript()
|
||||
require.NoError(t, err)
|
||||
|
||||
addrParams := &address.Parameters{
|
||||
ClientPubkey: clientKey.PubKey(),
|
||||
ServerPubkey: serverKey.PubKey(),
|
||||
PkScript: pkScript,
|
||||
Expiry: 4032,
|
||||
ProtocolVersion: version.ProtocolVersion_V0,
|
||||
}
|
||||
|
||||
dep := &deposit.Deposit{
|
||||
OutPoint: wire.OutPoint{
|
||||
Hash: chainhash.Hash{0xbb},
|
||||
Index: 0,
|
||||
},
|
||||
Value: 500_000,
|
||||
AddressParams: addrParams,
|
||||
}
|
||||
|
||||
confirmedHtlcHash := chainhash.Hash{0xcc}
|
||||
confirmedHtlcValue := btcutil.Amount(275_000)
|
||||
loopIn := &StaticAddressLoopIn{
|
||||
SwapHash: lntypes.Hash{3, 2, 1},
|
||||
HtlcCltvExpiry: 800,
|
||||
ClientPubkey: clientKey.PubKey(),
|
||||
ServerPubkey: serverKey.PubKey(),
|
||||
Deposits: []*deposit.Deposit{dep},
|
||||
HtlcTxFeeRate: chainfee.SatPerKWeight(253),
|
||||
HtlcTxHash: &confirmedHtlcHash,
|
||||
HtlcOutputIndex: 2,
|
||||
HtlcOutputValue: confirmedHtlcValue,
|
||||
}
|
||||
|
||||
sweepAddr, err := btcutil.NewAddressTaproot(make([]byte, 32), network)
|
||||
require.NoError(t, err)
|
||||
|
||||
sweepTx, err := loopIn.createHtlcSweepTx(
|
||||
t.Context(), &noopSigner{}, sweepAddr,
|
||||
chainfee.SatPerKWeight(253), network,
|
||||
uint32(loopIn.HtlcCltvExpiry)+1, 1,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, sweepTx.TxIn, 1)
|
||||
require.Equal(
|
||||
t, wire.OutPoint{
|
||||
Hash: confirmedHtlcHash,
|
||||
Index: 2,
|
||||
}, sweepTx.TxIn[0].PreviousOutPoint,
|
||||
)
|
||||
require.Less(t, sweepTx.TxOut[0].Value, int64(confirmedHtlcValue))
|
||||
require.Greater(t, sweepTx.TxOut[0].Value, int64(0))
|
||||
}
|
||||
|
||||
// newStaticAddress creates a StaticAddress for testing.
|
||||
func newStaticAddress(clientKey, serverKey *btcec.PublicKey,
|
||||
csvExpiry int64) (*script.StaticAddress, error) {
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ import (
|
|||
"github.com/lightninglabs/loop"
|
||||
"github.com/lightninglabs/loop/fsm"
|
||||
"github.com/lightninglabs/loop/staticaddr/deposit"
|
||||
"github.com/lightninglabs/loop/staticaddr/script"
|
||||
"github.com/lightninglabs/loop/staticaddr/staticutil"
|
||||
"github.com/lightninglabs/loop/swapserverrpc"
|
||||
"github.com/lightningnetwork/lnd/input"
|
||||
|
|
@ -333,7 +332,7 @@ func (m *Manager) handleLoopInSweepReq(ctx context.Context,
|
|||
// If the user selected an amount that is less than the total deposit
|
||||
// amount we'll check that the server sends us the correct change amount
|
||||
// back to our static address.
|
||||
err = m.checkChange(ctx, sweepTx, loopIn.AddressParams)
|
||||
err = m.checkChange(ctx, sweepTx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -471,7 +470,7 @@ func (m *Manager) handleLoopInSweepReq(ctx context.Context,
|
|||
// swaps with identical change outputs. The client needs to ensure that any
|
||||
// swap referenced by the inputs has a respective change output in the batch.
|
||||
func (m *Manager) checkChange(ctx context.Context,
|
||||
sweepTx *wire.MsgTx, changeAddr *script.Parameters) error {
|
||||
sweepTx *wire.MsgTx) error {
|
||||
|
||||
prevOuts := make([]string, len(sweepTx.TxIn))
|
||||
for i, in := range sweepTx.TxIn {
|
||||
|
|
@ -496,42 +495,67 @@ func (m *Manager) checkChange(ctx context.Context,
|
|||
return err
|
||||
}
|
||||
|
||||
var expectedChange btcutil.Amount
|
||||
var expectedChanges []*wire.TxOut
|
||||
for swapHash := range swapHashes {
|
||||
loopIn, err := m.cfg.Store.GetLoopInByHash(ctx, swapHash)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
totalDepositAmount := loopIn.TotalDepositAmount()
|
||||
changeAmt := totalDepositAmount - loopIn.SelectedAmount
|
||||
if changeAmt > 0 && changeAmt < totalDepositAmount {
|
||||
log.Debugf("expected change output to our "+
|
||||
"static address, total_deposit_amount=%v, "+
|
||||
"selected_amount=%v, "+
|
||||
"expected_change_amount=%v ",
|
||||
totalDepositAmount, loopIn.SelectedAmount,
|
||||
changeAmt)
|
||||
|
||||
expectedChange += changeAmt
|
||||
changeAmt := loopIn.ExpectedChangeAmount()
|
||||
if changeAmt == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
if loopIn.ChangeAddressParams == nil {
|
||||
return fmt.Errorf("missing change address for swap %x",
|
||||
swapHash[:])
|
||||
}
|
||||
|
||||
log.Debugf("expected change output to static address, "+
|
||||
"swap_hash=%x, selected_amount=%v, "+
|
||||
"expected_change_amount=%v", swapHash[:],
|
||||
loopIn.SelectedAmount, changeAmt)
|
||||
|
||||
expectedChanges = append(expectedChanges, &wire.TxOut{
|
||||
Value: int64(changeAmt),
|
||||
PkScript: loopIn.ChangeAddressParams.PkScript,
|
||||
})
|
||||
}
|
||||
|
||||
if expectedChange == 0 {
|
||||
if len(expectedChanges) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, out := range sweepTx.TxOut {
|
||||
if out.Value == int64(expectedChange) &&
|
||||
bytes.Equal(out.PkScript, changeAddr.PkScript) {
|
||||
// Match expected change outputs as a multiset. This rejects batched
|
||||
// transactions that collapse two equal client change outputs into one
|
||||
// output unless the protocol explicitly negotiates such aggregation.
|
||||
matchedOutputs := make([]bool, len(sweepTx.TxOut))
|
||||
for _, expected := range expectedChanges {
|
||||
var found bool
|
||||
for i, out := range sweepTx.TxOut {
|
||||
if matchedOutputs[i] {
|
||||
continue
|
||||
}
|
||||
|
||||
// We found the expected change output.
|
||||
return nil
|
||||
if out.Value == expected.Value &&
|
||||
bytes.Equal(out.PkScript, expected.PkScript) {
|
||||
|
||||
matchedOutputs[i] = true
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if found {
|
||||
continue
|
||||
}
|
||||
|
||||
return fmt.Errorf("couldn't find expected change of %v "+
|
||||
"satoshis sent to static address", expected.Value)
|
||||
}
|
||||
|
||||
return fmt.Errorf("couldn't find expected change of %v "+
|
||||
"satoshis sent to our static address", expectedChange)
|
||||
return nil
|
||||
}
|
||||
|
||||
// recover stars a loop-in state machine for each non-final loop-in to pick up
|
||||
|
|
|
|||
|
|
@ -666,9 +666,9 @@ func TestCheckChange(t *testing.T) {
|
|||
var hash lntypes.Hash
|
||||
hash[0] = h
|
||||
li := &StaticAddressLoopIn{
|
||||
Deposits: deposits,
|
||||
SelectedAmount: selected,
|
||||
AddressParams: changeAddr,
|
||||
Deposits: deposits,
|
||||
SelectedAmount: selected,
|
||||
ChangeAddressParams: changeAddr,
|
||||
}
|
||||
return hash, li
|
||||
}
|
||||
|
|
@ -723,7 +723,6 @@ func TestCheckChange(t *testing.T) {
|
|||
name string
|
||||
inDeps []*deposit.Deposit // deposits referenced by tx inputs
|
||||
outputs []*wire.TxOut // outputs in sweep tx
|
||||
addr *script.Parameters
|
||||
expectErr bool
|
||||
expectedErrMsg string
|
||||
}
|
||||
|
|
@ -739,7 +738,6 @@ func TestCheckChange(t *testing.T) {
|
|||
PkScript: serverAddr.PkScript,
|
||||
},
|
||||
},
|
||||
addr: changeAddr,
|
||||
},
|
||||
{
|
||||
name: "single swap change present",
|
||||
|
|
@ -754,43 +752,59 @@ func TestCheckChange(t *testing.T) {
|
|||
PkScript: changeAddr.PkScript,
|
||||
},
|
||||
},
|
||||
addr: changeAddr,
|
||||
},
|
||||
{
|
||||
name: "multiple swaps different change amounts",
|
||||
inDeps: []*deposit.Deposit{s2d1, s3d1}, // B(500)+C(400)=900
|
||||
inDeps: []*deposit.Deposit{s2d1, s3d1}, // B(500)+C(400)
|
||||
outputs: []*wire.TxOut{
|
||||
{
|
||||
Value: 1337,
|
||||
PkScript: serverAddr.PkScript,
|
||||
},
|
||||
{
|
||||
Value: 900,
|
||||
Value: 500,
|
||||
PkScript: changeAddr.PkScript,
|
||||
},
|
||||
{
|
||||
Value: 400,
|
||||
PkScript: changeAddr.PkScript,
|
||||
},
|
||||
},
|
||||
addr: changeAddr,
|
||||
},
|
||||
{
|
||||
name: "two swaps with identical change values sum correctly",
|
||||
inDeps: []*deposit.Deposit{s3d1, s4d1}, // C(400)+D(400)=800
|
||||
name: "two swaps with identical change values both present",
|
||||
inDeps: []*deposit.Deposit{s3d1, s4d1}, // C(400)+D(400)
|
||||
outputs: []*wire.TxOut{
|
||||
{
|
||||
Value: 1337,
|
||||
PkScript: serverAddr.PkScript,
|
||||
},
|
||||
{
|
||||
Value: 400,
|
||||
PkScript: changeAddr.PkScript,
|
||||
},
|
||||
{
|
||||
Value: 400,
|
||||
PkScript: changeAddr.PkScript,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "collapsed identical change output rejected",
|
||||
inDeps: []*deposit.Deposit{s3d1, s4d1}, // C(400)+D(400)
|
||||
outputs: []*wire.TxOut{
|
||||
{
|
||||
Value: 800,
|
||||
PkScript: changeAddr.PkScript,
|
||||
},
|
||||
},
|
||||
addr: changeAddr,
|
||||
expectErr: true,
|
||||
expectedErrMsg: "couldn't find expected change",
|
||||
},
|
||||
{
|
||||
name: "missing change output results in error",
|
||||
inDeps: []*deposit.Deposit{s2d1}, // expect 500
|
||||
outputs: []*wire.TxOut{},
|
||||
addr: changeAddr,
|
||||
expectErr: true,
|
||||
expectedErrMsg: "couldn't find expected change",
|
||||
},
|
||||
|
|
@ -807,7 +821,6 @@ func TestCheckChange(t *testing.T) {
|
|||
PkScript: otherAddr.PkScript,
|
||||
},
|
||||
},
|
||||
addr: changeAddr,
|
||||
expectErr: true,
|
||||
expectedErrMsg: "couldn't find expected change",
|
||||
},
|
||||
|
|
@ -824,7 +837,6 @@ func TestCheckChange(t *testing.T) {
|
|||
PkScript: changeAddr.PkScript,
|
||||
},
|
||||
},
|
||||
addr: changeAddr,
|
||||
expectErr: true,
|
||||
expectedErrMsg: "couldn't find expected change",
|
||||
},
|
||||
|
|
@ -845,7 +857,6 @@ func TestCheckChange(t *testing.T) {
|
|||
PkScript: otherAddr.PkScript,
|
||||
},
|
||||
},
|
||||
addr: changeAddr,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -868,7 +879,7 @@ func TestCheckChange(t *testing.T) {
|
|||
mgr.cfg.DepositManager = mdm
|
||||
|
||||
tx := makeSweepTx(inputs, tc.outputs)
|
||||
err := mgr.checkChange(ctx, tx, tc.addr)
|
||||
err := mgr.checkChange(ctx, tx)
|
||||
if tc.expectErr {
|
||||
require.Error(t, err)
|
||||
if tc.expectedErrMsg != "" {
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import (
|
|||
"github.com/lightninglabs/loop/fsm"
|
||||
"github.com/lightninglabs/loop/loopdb"
|
||||
"github.com/lightninglabs/loop/loopdb/sqlc"
|
||||
"github.com/lightninglabs/loop/staticaddr/address"
|
||||
"github.com/lightninglabs/loop/staticaddr/deposit"
|
||||
"github.com/lightninglabs/loop/staticaddr/version"
|
||||
"github.com/lightningnetwork/lnd/clock"
|
||||
|
|
@ -294,6 +295,17 @@ func (s *SqlStore) CreateLoopIn(ctx context.Context,
|
|||
PaymentTimeoutSeconds: int32(loopIn.PaymentTimeoutSeconds),
|
||||
Fast: loopIn.Fast,
|
||||
}
|
||||
if loopIn.ChangeAddressParams != nil {
|
||||
if loopIn.ChangeAddressParams.ID == 0 {
|
||||
return errors.New("static address change parameters " +
|
||||
"missing database ID")
|
||||
}
|
||||
|
||||
staticAddressLoopInParams.ChangeStaticAddressID = sql.NullInt32{
|
||||
Int32: loopIn.ChangeAddressParams.ID,
|
||||
Valid: true,
|
||||
}
|
||||
}
|
||||
|
||||
updateTime := sqlStoreUpdateTime(s.clock)
|
||||
updateArgs := sqlc.InsertStaticAddressMetaUpdateParams{
|
||||
|
|
@ -357,6 +369,11 @@ func (s *SqlStore) UpdateLoopIn(ctx context.Context,
|
|||
htlcTimeoutSweepTxID = loopIn.HtlcTimeoutSweepTxHash.String()
|
||||
}
|
||||
|
||||
var htlcTxID string
|
||||
if loopIn.HtlcTxHash != nil {
|
||||
htlcTxID = loopIn.HtlcTxHash.String()
|
||||
}
|
||||
|
||||
updateParams := sqlc.UpdateStaticAddressLoopInParams{
|
||||
SwapHash: loopIn.SwapHash[:],
|
||||
HtlcTxFeeRateSatKw: int64(loopIn.HtlcTxFeeRate),
|
||||
|
|
@ -364,6 +381,18 @@ func (s *SqlStore) UpdateLoopIn(ctx context.Context,
|
|||
String: htlcTimeoutSweepTxID,
|
||||
Valid: htlcTimeoutSweepTxID != "",
|
||||
},
|
||||
ConfirmedHtlcTxID: sql.NullString{
|
||||
String: htlcTxID,
|
||||
Valid: htlcTxID != "",
|
||||
},
|
||||
ConfirmedHtlcOutputIndex: sql.NullInt32{
|
||||
Int32: int32(loopIn.HtlcOutputIndex),
|
||||
Valid: htlcTxID != "",
|
||||
},
|
||||
ConfirmedHtlcOutputValue: sql.NullInt64{
|
||||
Int64: int64(loopIn.HtlcOutputValue),
|
||||
Valid: htlcTxID != "",
|
||||
},
|
||||
}
|
||||
|
||||
updateTime := sqlStoreUpdateTime(s.clock)
|
||||
|
|
@ -575,6 +604,16 @@ func toStaticAddressLoopIn(_ context.Context, network *chaincfg.Params,
|
|||
}
|
||||
}
|
||||
|
||||
var htlcTxHash *chainhash.Hash
|
||||
if swap.ConfirmedHtlcTxID.Valid {
|
||||
htlcTxHash, err = chainhash.NewHashFromStr(
|
||||
swap.ConfirmedHtlcTxID.String,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
var depositOutpoints []string
|
||||
if swap.DepositOutpoints != "" {
|
||||
depositOutpoints = strings.Split(
|
||||
|
|
@ -638,6 +677,11 @@ func toStaticAddressLoopIn(_ context.Context, network *chaincfg.Params,
|
|||
}
|
||||
depositList = orderDepositsBySnapshot(depositList, depositOutpoints)
|
||||
|
||||
changeAddressParams, err := toChangeAddressParameters(swap)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
loopIn := &StaticAddressLoopIn{
|
||||
SwapHash: swapHash,
|
||||
SwapPreimage: swapPreImage,
|
||||
|
|
@ -670,7 +714,13 @@ func toStaticAddressLoopIn(_ context.Context, network *chaincfg.Params,
|
|||
),
|
||||
HtlcTimeoutSweepAddress: timeoutAddress,
|
||||
HtlcTimeoutSweepTxHash: htlcTimeoutSweepTxHash,
|
||||
Deposits: depositList,
|
||||
HtlcTxHash: htlcTxHash,
|
||||
HtlcOutputIndex: uint32(swap.ConfirmedHtlcOutputIndex.Int32),
|
||||
HtlcOutputValue: btcutil.Amount(
|
||||
swap.ConfirmedHtlcOutputValue.Int64,
|
||||
),
|
||||
Deposits: depositList,
|
||||
ChangeAddressParams: changeAddressParams,
|
||||
}
|
||||
if swap.ConfirmationRiskDecisionTime.Valid {
|
||||
loopIn.ConfirmationRiskDecisionTime =
|
||||
|
|
@ -719,3 +769,41 @@ func orderDepositsBySnapshot(deposits []*deposit.Deposit,
|
|||
|
||||
return orderedDeposits
|
||||
}
|
||||
|
||||
// toChangeAddressParameters converts the optional joined static address row
|
||||
// into the change address parameters used to verify batched sweepless sweeps.
|
||||
func toChangeAddressParameters(row sqlc.GetStaticAddressLoopInSwapRow) (
|
||||
*address.Parameters, error) {
|
||||
|
||||
if !row.ChangeStaticAddressID.Valid {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
clientKey, err := btcec.ParsePubKey(row.ChangeClientPubkey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
serverKey, err := btcec.ParsePubKey(row.ChangeServerPubkey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &address.Parameters{
|
||||
ID: row.ChangeStaticAddressID.Int32,
|
||||
ClientPubkey: clientKey,
|
||||
ServerPubkey: serverKey,
|
||||
Expiry: uint32(row.ChangeExpiry.Int32),
|
||||
PkScript: row.ChangePkscript,
|
||||
KeyLocator: keychain.KeyLocator{
|
||||
Family: keychain.KeyFamily(
|
||||
row.ChangeClientKeyFamily.Int32,
|
||||
),
|
||||
Index: uint32(row.ChangeClientKeyIndex.Int32),
|
||||
},
|
||||
ProtocolVersion: version.AddressProtocolVersion(
|
||||
row.ChangeProtocolVersion.Int32,
|
||||
),
|
||||
InitiationHeight: row.ChangeInitiationHeight.Int32,
|
||||
}, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -544,6 +544,70 @@ func TestGetLoopInByHashOrdersDepositsBySnapshot(t *testing.T) {
|
|||
require.Equal(t, d1.ID, storedSwap.Deposits[1].ID)
|
||||
}
|
||||
|
||||
func TestUpdateLoopInPersistsConfirmedHtlcOutpoint(t *testing.T) {
|
||||
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,
|
||||
)
|
||||
|
||||
depositID, err := deposit.GetRandomDepositID()
|
||||
require.NoError(t, err)
|
||||
|
||||
d := &deposit.Deposit{
|
||||
ID: depositID,
|
||||
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,
|
||||
},
|
||||
}
|
||||
require.NoError(t, depositStore.CreateDeposit(ctxb, d))
|
||||
|
||||
d.SetState(deposit.LoopingIn)
|
||||
require.NoError(t, depositStore.UpdateDeposit(ctxb, d))
|
||||
|
||||
_, clientPubKey := test.CreateKey(1)
|
||||
_, serverPubKey := test.CreateKey(2)
|
||||
addr, err := btcutil.DecodeAddress(P2wkhAddr, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
swapHash := lntypes.Hash{0x4, 0x2, 0x3, 0x5}
|
||||
swap := StaticAddressLoopIn{
|
||||
SwapHash: swapHash,
|
||||
SwapPreimage: lntypes.Preimage{0x4, 0x2, 0x3, 0x5},
|
||||
DepositOutpoints: []string{d.OutPoint.String()},
|
||||
Deposits: []*deposit.Deposit{d},
|
||||
ClientPubkey: clientPubKey,
|
||||
ServerPubkey: serverPubKey,
|
||||
HtlcTimeoutSweepAddress: addr,
|
||||
}
|
||||
swap.SetState(MonitorInvoiceAndHtlcTx)
|
||||
require.NoError(t, swapStore.CreateLoopIn(ctxb, &swap))
|
||||
|
||||
confirmedHtlcTxHash := chainhash.Hash{0x55}
|
||||
swap.HtlcTxHash = &confirmedHtlcTxHash
|
||||
swap.HtlcOutputIndex = 2
|
||||
swap.HtlcOutputValue = 88_000
|
||||
require.NoError(t, swapStore.UpdateLoopIn(ctxb, &swap))
|
||||
|
||||
storedSwap, err := swapStore.GetLoopInByHash(ctxb, swapHash)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, storedSwap.HtlcTxHash)
|
||||
require.Equal(t, confirmedHtlcTxHash, *storedSwap.HtlcTxHash)
|
||||
require.EqualValues(t, 2, storedSwap.HtlcOutputIndex)
|
||||
require.EqualValues(t, 88_000, storedSwap.HtlcOutputValue)
|
||||
require.Equal(t, MonitorInvoiceAndHtlcTx, storedSwap.GetState())
|
||||
}
|
||||
|
||||
// TestGetLoopInByHashPreservesStoredDepositOutpoints ensures recovered loop-ins
|
||||
// keep the original outpoint snapshot stored when the swap was created.
|
||||
func TestGetLoopInByHashPreservesStoredDepositOutpoints(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import (
|
|||
"github.com/btcsuite/btcd/txscript"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/lightninglabs/lndclient"
|
||||
"github.com/lightninglabs/loop/staticaddr/address"
|
||||
"github.com/lightninglabs/loop/staticaddr/deposit"
|
||||
"github.com/lightninglabs/loop/swapserverrpc"
|
||||
"github.com/lightningnetwork/lnd/input"
|
||||
|
|
@ -96,6 +97,35 @@ func DepositClientPubkeys(deposits []*deposit.Deposit) (
|
|||
return clientPubkeys, nil
|
||||
}
|
||||
|
||||
// ChangeOutput converts a locally generated static address into the RPC change
|
||||
// descriptor sent to the server. The descriptor binds the expected script,
|
||||
// amount and client key so the server can derive and verify the same address.
|
||||
func ChangeOutput(params *address.Parameters,
|
||||
amount btcutil.Amount) (*swapserverrpc.StaticAddressChangeOutput, error) {
|
||||
|
||||
if amount <= 0 {
|
||||
return nil, nil
|
||||
}
|
||||
if params == nil {
|
||||
return nil, fmt.Errorf("missing static address change parameters")
|
||||
}
|
||||
if params.ClientPubkey == nil {
|
||||
return nil, fmt.Errorf("missing static address change client " +
|
||||
"pubkey")
|
||||
}
|
||||
if len(params.PkScript) == 0 {
|
||||
return nil, fmt.Errorf("missing static address change pkscript")
|
||||
}
|
||||
|
||||
return &swapserverrpc.StaticAddressChangeOutput{
|
||||
StaticAddress: &swapserverrpc.StaticAddressDescriptor{
|
||||
Pubkey: params.ClientPubkey.SerializeCompressed(),
|
||||
PkScript: params.PkScript,
|
||||
},
|
||||
Amount: int64(amount),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CreateMusig2Sessions creates a musig2 session for a number of deposits.
|
||||
func CreateMusig2Sessions(ctx context.Context,
|
||||
signer lndclient.SignerClient, deposits []*deposit.Deposit) (
|
||||
|
|
|
|||
|
|
@ -237,6 +237,45 @@ func TestDepositClientPubkeysRejectsInvalidDeposits(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestChangeOutput(t *testing.T) {
|
||||
clientKey, err := btcec.NewPrivateKey()
|
||||
require.NoError(t, err)
|
||||
|
||||
params := &address.Parameters{
|
||||
ClientPubkey: clientKey.PubKey(),
|
||||
PkScript: []byte{0x51, 0x20, 0x01},
|
||||
}
|
||||
amount := btcutil.Amount(12345)
|
||||
|
||||
changeOutput, err := ChangeOutput(params, amount)
|
||||
require.NoError(t, err)
|
||||
require.Equal(
|
||||
t, clientKey.PubKey().SerializeCompressed(),
|
||||
changeOutput.StaticAddress.GetPubkey(),
|
||||
)
|
||||
require.Equal(t, params.PkScript, changeOutput.StaticAddress.GetPkScript())
|
||||
require.EqualValues(t, amount, changeOutput.Amount)
|
||||
|
||||
changeOutput, err = ChangeOutput(params, 0)
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, changeOutput)
|
||||
}
|
||||
|
||||
func TestChangeOutputRejectsInvalidParams(t *testing.T) {
|
||||
_, err := ChangeOutput(nil, 100)
|
||||
require.ErrorContains(t, err, "missing static address change parameters")
|
||||
|
||||
_, err = ChangeOutput(&address.Parameters{}, 100)
|
||||
require.ErrorContains(t, err, "missing static address change client pubkey")
|
||||
|
||||
clientKey, err := btcec.NewPrivateKey()
|
||||
require.NoError(t, err)
|
||||
_, err = ChangeOutput(&address.Parameters{
|
||||
ClientPubkey: clientKey.PubKey(),
|
||||
}, 100)
|
||||
require.ErrorContains(t, err, "missing static address change pkscript")
|
||||
}
|
||||
|
||||
func TestGetPrevoutInfo_ConversionAndSorting(t *testing.T) {
|
||||
// Helper to create a hash from string.
|
||||
must := func(s string) chainhash.Hash {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue