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:
Slyghtning 2026-07-01 07:16:34 +02:00
parent 0611832030
commit ee5d84b323
No known key found for this signature in database
GPG key ID: F82D456EA023C9BF
6 changed files with 138 additions and 16 deletions

View file

@ -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)

View 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())
}

View 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
}

View 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))
}

View file

@ -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 =

View file

@ -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
}