mirror of
https://github.com/lightninglabs/loop.git
synced 2026-08-13 12:33:03 +02:00
staticaddr/deposit: reject duplicate outpoints
Reject duplicate static-address deposit outpoints before creating withdrawal, loop-in, or channel-open requests. Use the shared outpoint duplicate helper so each flow reports the same input validation failure.
This commit is contained in:
parent
0611832030
commit
ee5d84b323
6 changed files with 138 additions and 16 deletions
|
|
@ -439,6 +439,10 @@ func (m *Manager) GetActiveDepositsInState(stateFilter fsm.StateType) (
|
|||
func (m *Manager) AllOutpointsActiveDeposits(outpoints []wire.OutPoint,
|
||||
targetState fsm.StateType) ([]*Deposit, bool) {
|
||||
|
||||
if CheckDuplicates(outpoints) != nil {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
|
|
@ -497,6 +501,9 @@ func (m *Manager) TransitionDeposits(ctx context.Context, deposits []*Deposit,
|
|||
for i, d := range deposits {
|
||||
outpoints[i] = d.OutPoint
|
||||
}
|
||||
if err := CheckDuplicates(outpoints); err != nil {
|
||||
return fmt.Errorf("duplicate deposit outpoint: %w", err)
|
||||
}
|
||||
|
||||
m.mu.Lock()
|
||||
stateMachines, _ := m.toActiveDeposits(&outpoints)
|
||||
|
|
|
|||
58
staticaddr/deposit/manager_reconcile_test.go
Normal file
58
staticaddr/deposit/manager_reconcile_test.go
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
package deposit
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestAllOutpointsActiveDepositsRejectsDuplicateOutpoints verifies that a
|
||||
// duplicated selection is rejected before the manager tries to lock the same
|
||||
// deposit twice.
|
||||
func TestAllOutpointsActiveDepositsRejectsDuplicateOutpoints(t *testing.T) {
|
||||
outpoint := wire.OutPoint{
|
||||
Hash: chainhash.Hash{12},
|
||||
Index: 6,
|
||||
}
|
||||
|
||||
deposit := &Deposit{
|
||||
OutPoint: outpoint,
|
||||
}
|
||||
deposit.SetState(Deposited)
|
||||
|
||||
manager := NewManager(&ManagerConfig{})
|
||||
manager.deposits[outpoint] = deposit
|
||||
manager.activeDeposits[outpoint] = &FSM{
|
||||
deposit: deposit,
|
||||
}
|
||||
|
||||
deposits, ok := manager.AllOutpointsActiveDeposits(
|
||||
[]wire.OutPoint{outpoint, outpoint}, Deposited,
|
||||
)
|
||||
require.False(t, ok)
|
||||
require.Nil(t, deposits)
|
||||
}
|
||||
|
||||
// TestTransitionDepositsRejectsDuplicateOutpoints verifies that transition
|
||||
// callers cannot deadlock the manager by passing the same deposit twice.
|
||||
func TestTransitionDepositsRejectsDuplicateOutpoints(t *testing.T) {
|
||||
outpoint := wire.OutPoint{
|
||||
Hash: chainhash.Hash{13},
|
||||
Index: 6,
|
||||
}
|
||||
|
||||
deposit := &Deposit{
|
||||
OutPoint: outpoint,
|
||||
}
|
||||
deposit.SetState(Deposited)
|
||||
|
||||
manager := NewManager(&ManagerConfig{})
|
||||
err := manager.TransitionDeposits(
|
||||
t.Context(), []*Deposit{deposit, deposit}, OnLoopInInitiated,
|
||||
LoopingIn,
|
||||
)
|
||||
require.ErrorContains(t, err, "duplicate deposit outpoint")
|
||||
require.Equal(t, Deposited, deposit.GetState())
|
||||
}
|
||||
21
staticaddr/deposit/outpoint.go
Normal file
21
staticaddr/deposit/outpoint.go
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
package deposit
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
)
|
||||
|
||||
// CheckDuplicates returns an error if the outpoint list contains duplicates.
|
||||
func CheckDuplicates(outpoints []wire.OutPoint) error {
|
||||
seen := make(map[wire.OutPoint]struct{}, len(outpoints))
|
||||
for _, outpoint := range outpoints {
|
||||
if _, ok := seen[outpoint]; ok {
|
||||
return fmt.Errorf("duplicate outpoint %v", outpoint)
|
||||
}
|
||||
|
||||
seen[outpoint] = struct{}{}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
40
staticaddr/deposit/outpoint_test.go
Normal file
40
staticaddr/deposit/outpoint_test.go
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
package deposit
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestCheckDuplicates(t *testing.T) {
|
||||
duplicate := wire.OutPoint{
|
||||
Hash: chainhash.Hash{1},
|
||||
Index: 2,
|
||||
}
|
||||
|
||||
outpoints := []wire.OutPoint{{
|
||||
Hash: chainhash.Hash{3},
|
||||
Index: 4,
|
||||
}, duplicate, {
|
||||
Hash: chainhash.Hash{5},
|
||||
Index: 6,
|
||||
}, duplicate}
|
||||
|
||||
err := CheckDuplicates(outpoints)
|
||||
require.ErrorContains(t, err, "duplicate outpoint")
|
||||
require.ErrorContains(t, err, duplicate.String())
|
||||
}
|
||||
|
||||
func TestCheckDuplicatesNoDuplicate(t *testing.T) {
|
||||
outpoints := []wire.OutPoint{{
|
||||
Hash: chainhash.Hash{1},
|
||||
Index: 2,
|
||||
}, {
|
||||
Hash: chainhash.Hash{3},
|
||||
Index: 4,
|
||||
}}
|
||||
|
||||
require.NoError(t, CheckDuplicates(outpoints))
|
||||
}
|
||||
|
|
@ -284,13 +284,8 @@ func (m *Manager) OpenChannel(ctx context.Context,
|
|||
// Check for duplicate outpoints which would lead to fee
|
||||
// miscalculation and an invalid PSBT with the same input
|
||||
// listed twice.
|
||||
seen := make(map[wire.OutPoint]struct{}, len(outpoints))
|
||||
for _, op := range outpoints {
|
||||
if _, ok := seen[op]; ok {
|
||||
return nil, fmt.Errorf("duplicate outpoint "+
|
||||
"%v in request", op)
|
||||
}
|
||||
seen[op] = struct{}{}
|
||||
if err := deposit.CheckDuplicates(outpoints); err != nil {
|
||||
return nil, fmt.Errorf("%w in request", err)
|
||||
}
|
||||
|
||||
deposits, allActive =
|
||||
|
|
|
|||
|
|
@ -24,20 +24,21 @@ import (
|
|||
func ToPrevOuts(deposits []*deposit.Deposit,
|
||||
pkScript []byte) (map[wire.OutPoint]*wire.TxOut, error) {
|
||||
|
||||
outpoints := make([]wire.OutPoint, len(deposits))
|
||||
for i, d := range deposits {
|
||||
outpoints[i] = d.OutPoint
|
||||
}
|
||||
if err := deposit.CheckDuplicates(outpoints); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
prevOuts := make(map[wire.OutPoint]*wire.TxOut, len(deposits))
|
||||
for _, d := range deposits {
|
||||
outpoint := wire.OutPoint{
|
||||
Hash: d.Hash,
|
||||
Index: d.Index,
|
||||
}
|
||||
for i, d := range deposits {
|
||||
outpoint := outpoints[i]
|
||||
txOut := &wire.TxOut{
|
||||
Value: int64(d.Value),
|
||||
PkScript: pkScript,
|
||||
}
|
||||
if _, ok := prevOuts[outpoint]; ok {
|
||||
return nil, fmt.Errorf("duplicate outpoint %v",
|
||||
outpoint)
|
||||
}
|
||||
prevOuts[outpoint] = txOut
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue