mirror of
https://github.com/lightninglabs/loop.git
synced 2026-08-13 12:33:03 +02:00
staticaddr: checkChange method for sweep signing
This commit is contained in:
parent
221372b5ef
commit
a877cfdc4c
3 changed files with 443 additions and 23 deletions
|
|
@ -79,6 +79,11 @@ type StaticAddressLoopInStore interface {
|
|||
// GetLoopInByHash returns the loop-in swap with the given hash.
|
||||
GetLoopInByHash(ctx context.Context, swapHash lntypes.Hash) (
|
||||
*StaticAddressLoopIn, error)
|
||||
|
||||
// SwapHashesForDepositIDs returns a map of swap hashes to deposit IDs
|
||||
// for the given deposit IDs.
|
||||
SwapHashesForDepositIDs(ctx context.Context,
|
||||
depositIDs []deposit.ID) (map[lntypes.Hash][]deposit.ID, error)
|
||||
}
|
||||
|
||||
type QuoteGetter interface {
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import (
|
|||
"github.com/lightninglabs/loop"
|
||||
"github.com/lightninglabs/loop/fsm"
|
||||
"github.com/lightninglabs/loop/labels"
|
||||
"github.com/lightninglabs/loop/staticaddr/address"
|
||||
"github.com/lightninglabs/loop/staticaddr/deposit"
|
||||
"github.com/lightninglabs/loop/swapserverrpc"
|
||||
"github.com/lightningnetwork/lnd/input"
|
||||
|
|
@ -324,29 +325,9 @@ 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.
|
||||
totalDepositAmount := loopIn.TotalDepositAmount()
|
||||
changeAmt := totalDepositAmount - loopIn.SelectedAmount
|
||||
if changeAmt > 0 && changeAmt < totalDepositAmount {
|
||||
var foundChange bool
|
||||
changePkScript := loopIn.AddressParams.PkScript
|
||||
|
||||
for _, out := range sweepTx.TxOut {
|
||||
if out.Value == int64(changeAmt) &&
|
||||
bytes.Equal(out.PkScript, changePkScript) {
|
||||
|
||||
foundChange = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !foundChange {
|
||||
return fmt.Errorf("expected change output to our "+
|
||||
"static address, total_deposit_amount=%v, "+
|
||||
"selected_amount=%v, "+
|
||||
"expected_change_amount=%v ",
|
||||
totalDepositAmount, loopIn.SelectedAmount,
|
||||
changeAmt)
|
||||
}
|
||||
err = m.checkChange(ctx, sweepTx, loopIn.AddressParams)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check if all the deposits requested are part of the loop-in and
|
||||
|
|
@ -465,6 +446,73 @@ func (m *Manager) handleLoopInSweepReq(ctx context.Context,
|
|||
return err
|
||||
}
|
||||
|
||||
// checkChange ensures that the server sends us the correct change amount
|
||||
// back to our static address. An edge case arises if a batch contains two
|
||||
// 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 *address.Parameters) error {
|
||||
|
||||
prevOuts := make([]string, len(sweepTx.TxIn))
|
||||
for i, in := range sweepTx.TxIn {
|
||||
prevOuts[i] = in.PreviousOutPoint.String()
|
||||
}
|
||||
|
||||
deposits, err := m.cfg.DepositManager.DepositsForOutpoints(
|
||||
ctx, prevOuts,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
depositIDs := make([]deposit.ID, len(deposits))
|
||||
for i, d := range deposits {
|
||||
depositIDs[i] = d.ID
|
||||
}
|
||||
|
||||
swapHashes, err := m.cfg.Store.SwapHashesForDepositIDs(ctx, depositIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var expectedChange btcutil.Amount
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
if expectedChange == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, out := range sweepTx.TxOut {
|
||||
if out.Value == int64(expectedChange) &&
|
||||
bytes.Equal(out.PkScript, changeAddr.PkScript) {
|
||||
|
||||
// We found the expected change output.
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("couldn't find expected change of %v "+
|
||||
"satoshis sent to our static address", expectedChange)
|
||||
}
|
||||
|
||||
// recover stars a loop-in state machine for each non-final loop-in to pick up
|
||||
// work where it was left off before the restart.
|
||||
func (m *Manager) recoverLoopIns(ctx context.Context) error {
|
||||
|
|
|
|||
|
|
@ -1,11 +1,16 @@
|
|||
package loopin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"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/staticaddr/address"
|
||||
"github.com/lightninglabs/loop/staticaddr/deposit"
|
||||
"github.com/lightningnetwork/lnd/lntypes"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
|
|
@ -133,3 +138,365 @@ func TestSelectDeposits(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
// mockDepositManager implements DepositManager for tests.
|
||||
type mockDepositManager struct {
|
||||
byOutpoint map[string]*deposit.Deposit
|
||||
}
|
||||
|
||||
func (m *mockDepositManager) GetAllDeposits(_ context.Context) (
|
||||
[]*deposit.Deposit, error) {
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *mockDepositManager) AllStringOutpointsActiveDeposits(_ []string,
|
||||
_ fsm.StateType) ([]*deposit.Deposit, bool) {
|
||||
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (m *mockDepositManager) TransitionDeposits(_ context.Context,
|
||||
_ []*deposit.Deposit, _ fsm.EventType, _ fsm.StateType) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockDepositManager) DepositsForOutpoints(_ context.Context,
|
||||
outpoints []string) ([]*deposit.Deposit, error) {
|
||||
|
||||
res := make([]*deposit.Deposit, 0, len(outpoints))
|
||||
for _, op := range outpoints {
|
||||
if d, ok := m.byOutpoint[op]; ok {
|
||||
res = append(res, d)
|
||||
}
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (m *mockDepositManager) GetActiveDepositsInState(_ fsm.StateType) (
|
||||
[]*deposit.Deposit, error) {
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// mockStore implements StaticAddressLoopInStore for tests.
|
||||
type mockStore struct {
|
||||
loopIns map[lntypes.Hash]*StaticAddressLoopIn
|
||||
mapIDs map[lntypes.Hash][]deposit.ID
|
||||
}
|
||||
|
||||
func (s *mockStore) CreateLoopIn(_ context.Context,
|
||||
_ *StaticAddressLoopIn) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *mockStore) UpdateLoopIn(_ context.Context,
|
||||
_ *StaticAddressLoopIn) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *mockStore) GetStaticAddressLoopInSwapsByStates(_ context.Context,
|
||||
_ []fsm.StateType) ([]*StaticAddressLoopIn, error) {
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
func (s *mockStore) IsStored(_ context.Context, _ lntypes.Hash) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (s *mockStore) GetLoopInByHash(_ context.Context,
|
||||
swapHash lntypes.Hash) (*StaticAddressLoopIn, error) {
|
||||
|
||||
li, ok := s.loopIns[swapHash]
|
||||
if !ok {
|
||||
return nil, nil
|
||||
}
|
||||
return li, nil
|
||||
}
|
||||
func (s *mockStore) SwapHashesForDepositIDs(_ context.Context,
|
||||
depositIDs []deposit.ID) (map[lntypes.Hash][]deposit.ID, error) {
|
||||
|
||||
// Filter the prepared mapping to only include hashes that reference
|
||||
// any of the provided deposit IDs.
|
||||
idSet := make(map[deposit.ID]struct{}, len(depositIDs))
|
||||
for _, id := range depositIDs {
|
||||
idSet[id] = struct{}{}
|
||||
}
|
||||
res := make(map[lntypes.Hash][]deposit.ID)
|
||||
for h, ids := range s.mapIDs {
|
||||
for _, id := range ids {
|
||||
if _, ok := idSet[id]; ok {
|
||||
res[h] = ids
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// helper to create a deposit with specific outpoint and value.
|
||||
func makeDeposit(h byte, index uint32, value btcutil.Amount) *deposit.Deposit {
|
||||
d := &deposit.Deposit{Value: value}
|
||||
d.Hash = chainhash.Hash{h}
|
||||
d.Index = index
|
||||
var id deposit.ID
|
||||
id[0] = h
|
||||
d.ID = id
|
||||
|
||||
return d
|
||||
}
|
||||
|
||||
// helper to outpoint string as used by txin.PreviousOutPoint.String().
|
||||
func outpointString(d *deposit.Deposit) string {
|
||||
return wire.OutPoint{Hash: d.Hash, Index: d.Index}.String()
|
||||
}
|
||||
|
||||
// build a sweep tx with given inputs and outputs.
|
||||
func makeSweepTx(inputs []wire.OutPoint, outputs []*wire.TxOut) *wire.MsgTx {
|
||||
tx := wire.NewMsgTx(2)
|
||||
for _, in := range inputs {
|
||||
tx.AddTxIn(&wire.TxIn{PreviousOutPoint: in})
|
||||
}
|
||||
for _, out := range outputs {
|
||||
tx.AddTxOut(out)
|
||||
}
|
||||
|
||||
return tx
|
||||
}
|
||||
|
||||
// TestCheckChange exercises all relevant scenarios for checkChange.
|
||||
func TestCheckChange(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
// Prepare a common change address and an alternate address.
|
||||
changeAddr := &address.Parameters{PkScript: []byte{0xaa, 0xbb}}
|
||||
otherAddr := &address.Parameters{PkScript: []byte{0xcc, 0xdd}}
|
||||
serverAddr := &address.Parameters{PkScript: []byte{0xee, 0xff}}
|
||||
|
||||
// Prepare swaps (loop-ins) with varying deposit totals and selections.
|
||||
// Helper to make a swap with deposits and selected amount.
|
||||
makeSwap := func(h byte, deposits []*deposit.Deposit,
|
||||
selected btcutil.Amount) (lntypes.Hash, *StaticAddressLoopIn) {
|
||||
|
||||
var hash lntypes.Hash
|
||||
hash[0] = h
|
||||
li := &StaticAddressLoopIn{
|
||||
Deposits: deposits,
|
||||
SelectedAmount: selected,
|
||||
AddressParams: changeAddr,
|
||||
}
|
||||
return hash, li
|
||||
}
|
||||
|
||||
// Deposits belonging to different swaps.
|
||||
s1d1 := makeDeposit(1, 0, 1000)
|
||||
s1d2 := makeDeposit(1, 1, 2000)
|
||||
s2d1 := makeDeposit(2, 0, 1500)
|
||||
s3d1 := makeDeposit(3, 0, 800)
|
||||
s4d1 := makeDeposit(4, 0, 900)
|
||||
|
||||
// Swaps:
|
||||
// A: total 3000, selected 3000 => no change.
|
||||
hA, liA := makeSwap(10, []*deposit.Deposit{s1d1, s1d2}, 3000)
|
||||
// B: total 1500, selected 1000 => change 500.
|
||||
hB, liB := makeSwap(11, []*deposit.Deposit{s2d1}, 1000)
|
||||
// C: total 800, selected 400 => change 400.
|
||||
hC, liC := makeSwap(12, []*deposit.Deposit{s3d1}, 400)
|
||||
// D: total 900, selected 500 => change 400.
|
||||
hD, liD := makeSwap(13, []*deposit.Deposit{s4d1}, 500)
|
||||
|
||||
// Mapping deposits -> swaps (by deposit IDs).
|
||||
mapIDs := map[lntypes.Hash][]deposit.ID{
|
||||
hA: {s1d1.ID, s1d2.ID},
|
||||
hB: {s2d1.ID},
|
||||
hC: {s3d1.ID},
|
||||
hD: {s4d1.ID},
|
||||
}
|
||||
|
||||
loopIns := map[lntypes.Hash]*StaticAddressLoopIn{
|
||||
hA: liA,
|
||||
hB: liB,
|
||||
hC: liC,
|
||||
hD: liD,
|
||||
}
|
||||
|
||||
// Common manager with mocked dependencies; will change inputs per test.
|
||||
mgr := &Manager{
|
||||
cfg: &Config{
|
||||
DepositManager: &mockDepositManager{
|
||||
byOutpoint: map[string]*deposit.Deposit{},
|
||||
},
|
||||
Store: &mockStore{
|
||||
loopIns: loopIns,
|
||||
mapIDs: mapIDs,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
type testCase struct {
|
||||
name string
|
||||
inDeps []*deposit.Deposit // deposits referenced by tx inputs
|
||||
outputs []*wire.TxOut // outputs in sweep tx
|
||||
addr *address.Parameters
|
||||
expectErr bool
|
||||
expectedErrMsg string
|
||||
}
|
||||
|
||||
cases := []testCase{
|
||||
{
|
||||
name: "no change expected (selected == total)",
|
||||
inDeps: []*deposit.Deposit{s1d1, s1d2},
|
||||
// No change output required.
|
||||
outputs: []*wire.TxOut{
|
||||
{
|
||||
Value: 1337,
|
||||
PkScript: serverAddr.PkScript,
|
||||
},
|
||||
},
|
||||
addr: changeAddr,
|
||||
},
|
||||
{
|
||||
name: "single swap change present",
|
||||
inDeps: []*deposit.Deposit{s2d1}, // B -> change 500
|
||||
outputs: []*wire.TxOut{
|
||||
{
|
||||
Value: 1337,
|
||||
PkScript: serverAddr.PkScript,
|
||||
},
|
||||
{
|
||||
Value: 500,
|
||||
PkScript: changeAddr.PkScript,
|
||||
},
|
||||
},
|
||||
addr: changeAddr,
|
||||
},
|
||||
{
|
||||
name: "multiple swaps different change amounts",
|
||||
inDeps: []*deposit.Deposit{s2d1, s3d1}, // B(500)+C(400)=900
|
||||
outputs: []*wire.TxOut{
|
||||
{
|
||||
Value: 1337,
|
||||
PkScript: serverAddr.PkScript,
|
||||
},
|
||||
{
|
||||
Value: 900,
|
||||
PkScript: changeAddr.PkScript,
|
||||
},
|
||||
},
|
||||
addr: changeAddr,
|
||||
},
|
||||
{
|
||||
name: "two swaps with identical change values sum correctly",
|
||||
inDeps: []*deposit.Deposit{s3d1, s4d1}, // C(400)+D(400)=800
|
||||
outputs: []*wire.TxOut{
|
||||
{
|
||||
Value: 1337,
|
||||
PkScript: serverAddr.PkScript,
|
||||
},
|
||||
{
|
||||
Value: 800,
|
||||
PkScript: changeAddr.PkScript,
|
||||
},
|
||||
},
|
||||
addr: changeAddr,
|
||||
},
|
||||
{
|
||||
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",
|
||||
},
|
||||
{
|
||||
name: "wrong address for change output",
|
||||
inDeps: []*deposit.Deposit{s2d1}, // expect 500
|
||||
outputs: []*wire.TxOut{
|
||||
{
|
||||
Value: 1337,
|
||||
PkScript: serverAddr.PkScript,
|
||||
},
|
||||
{
|
||||
Value: 500,
|
||||
PkScript: otherAddr.PkScript,
|
||||
},
|
||||
},
|
||||
addr: changeAddr,
|
||||
expectErr: true,
|
||||
expectedErrMsg: "couldn't find expected change",
|
||||
},
|
||||
{
|
||||
name: "wrong amount for change output",
|
||||
inDeps: []*deposit.Deposit{s2d1}, // expect 500
|
||||
outputs: []*wire.TxOut{
|
||||
{
|
||||
Value: 1337,
|
||||
PkScript: serverAddr.PkScript,
|
||||
},
|
||||
{
|
||||
Value: 400,
|
||||
PkScript: changeAddr.PkScript,
|
||||
},
|
||||
},
|
||||
addr: changeAddr,
|
||||
expectErr: true,
|
||||
expectedErrMsg: "couldn't find expected change",
|
||||
},
|
||||
{
|
||||
name: "mixed swaps some with change some without",
|
||||
inDeps: []*deposit.Deposit{s1d1, s1d2, s3d1}, // A(0)+C(400)=400
|
||||
outputs: []*wire.TxOut{
|
||||
{
|
||||
Value: 1337,
|
||||
PkScript: serverAddr.PkScript,
|
||||
},
|
||||
{
|
||||
Value: 400,
|
||||
PkScript: changeAddr.PkScript,
|
||||
},
|
||||
{
|
||||
Value: 1000,
|
||||
PkScript: otherAddr.PkScript,
|
||||
},
|
||||
},
|
||||
addr: changeAddr,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
// Prepare inputs mapping for deposit manager.
|
||||
mdm := &mockDepositManager{
|
||||
byOutpoint: map[string]*deposit.Deposit{},
|
||||
}
|
||||
inputs := make([]wire.OutPoint, 0, len(tc.inDeps))
|
||||
for _, d := range tc.inDeps {
|
||||
mdm.byOutpoint[outpointString(d)] = d
|
||||
inputs = append(
|
||||
inputs, wire.OutPoint{
|
||||
Hash: d.Hash,
|
||||
Index: d.Index,
|
||||
},
|
||||
)
|
||||
}
|
||||
mgr.cfg.DepositManager = mdm
|
||||
|
||||
tx := makeSweepTx(inputs, tc.outputs)
|
||||
err := mgr.checkChange(ctx, tx, tc.addr)
|
||||
if tc.expectErr {
|
||||
require.Error(t, err)
|
||||
if tc.expectedErrMsg != "" {
|
||||
require.ErrorContains(
|
||||
t, err, tc.expectedErrMsg,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue