Skip to content

Commit

Permalink
switchrpc: protect from attemptID reuse
Browse files Browse the repository at this point in the history
Add a memory optimized store for SendOnion/TrackOnion duplication/safe ordering
protection. This ensures that if TrackOnion returns PAYMENT_ID_NOT_FOUND or
SendOnion initiates HTLC creation for a given attempt ID, SendOnion cannot
subsequently succeed with the same attempt ID. This mechanism safeguards against
overpayment in scenarios where network requests are reordered. If an attempt ID
has already been used by either SendOnion or TrackOnion, SendOnion will return
DUPLICATE_HTLC for that attempt ID.

Used https://github.com/RoaringBitmap/roaring as a store for attemp IDs.
  • Loading branch information
starius committed Jan 13, 2025
1 parent 3676dcd commit 49ed723
Show file tree
Hide file tree
Showing 3 changed files with 63 additions and 1 deletion.
6 changes: 6 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ module github.com/lightningnetwork/lnd

require (
github.com/NebulousLabs/go-upnp v0.0.0-20180202185039-29b680b06c82
github.com/RoaringBitmap/roaring/v2 v2.4.2
github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344
github.com/andybalholm/brotli v1.0.4
github.com/btcsuite/btcd v0.24.3-0.20241210095828-e646d437e95b
Expand Down Expand Up @@ -67,6 +68,11 @@ require (
pgregory.net/rapid v1.1.0
)

require (
github.com/bits-and-blooms/bitset v1.12.0 // indirect
github.com/mschoch/smat v0.2.0 // indirect
)

require (
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
github.com/Microsoft/go-winio v0.6.1 // indirect
Expand Down
7 changes: 7 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ github.com/NebulousLabs/go-upnp v0.0.0-20180202185039-29b680b06c82/go.mod h1:Gbu
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw=
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/RoaringBitmap/roaring/v2 v2.4.2 h1:ew/INI7HLRyYK+dCbF6FcUwoe2Q0q5HCV7WafY9ljBk=
github.com/RoaringBitmap/roaring/v2 v2.4.2/go.mod h1:FiJcsfkGje/nZBZgCu0ZxCPOKD/hVXDS2dXi7/eUFE0=
github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344 h1:cDVUiFo+npB0ZASqnw4q90ylaVAbnYyx0JYqK4YcGok=
github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344/go.mod h1:9pIqrY6SXNL8vjRQE5Hd/OL5GyK/9MrGUWs87z/eFfk=
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY=
Expand All @@ -70,6 +72,8 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bits-and-blooms/bitset v1.12.0 h1:U/q1fAF7xXRhFCrhROzIfffYnu+dlS38vCZtmFVPHmA=
github.com/bits-and-blooms/bitset v1.12.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ=
github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M=
github.com/btcsuite/btcd v0.23.5-0.20231215221805-96c9fd8078fd/go.mod h1:nm3Bko6zh6bWP60UxwoT5LzdGJsQJaPo6HjduXq9p6A=
Expand Down Expand Up @@ -503,6 +507,8 @@ github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/mschoch/smat v0.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM=
github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
Expand Down Expand Up @@ -1039,6 +1045,7 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.3.0 h1:MfDY1b1/0xN1CyMlQDac0ziEy9zJQd9CXBRRDHw2jJo=
Expand Down
51 changes: 50 additions & 1 deletion lnrpc/switchrpc/switch_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ import (
"math/big"
"os"
"path/filepath"
"sync"

"github.com/RoaringBitmap/roaring/v2/roaring64"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
sphinx "github.com/lightningnetwork/lightning-onion"
Expand Down Expand Up @@ -89,12 +91,43 @@ type ServerShell struct {
type Server struct {
cfg *Config

// usedAttemptIDs maintains the set of attempt IDs that have been used
// in either SendOnion or TrackOnion. This ensures that if TrackOnion
// returns PAYMENT_ID_NOT_FOUND or SendOnion initiates HTLC creation for
// a given attempt ID, SendOnion cannot subsequently succeed with the
// same attempt ID. This mechanism safeguards against overpayment in
// scenarios where network requests are reordered. If an attempt ID has
// already been used by either SendOnion or TrackOnion, SendOnion will
// return DUPLICATE_HTLC for that attempt ID.
usedAttemptIDs *roaring64.Bitmap

// usedAttemptIDsMu is a mutex which protects accesses to
// usedAttemptIDs.
usedAttemptIDsMu sync.Mutex

// Required by the grpc-gateway/v2 library for forward compatibility.
// Must be after the atomically used variables to not break struct
// alignment.
UnimplementedSwitchServer
}

// tryAddAttemptID tries to add attemptID to usedAttemptIDs under the mutex and
// returns if the attemptID is new.
func (s *Server) tryAddAttemptID(attemptID uint64) bool {
s.usedAttemptIDsMu.Lock()
defer s.usedAttemptIDsMu.Unlock()

return s.usedAttemptIDs.CheckedAdd(attemptID)
}

// tryAddAttemptID adds attemptID to usedAttemptIDs under the mutex.
func (s *Server) addAttemptID(attemptID uint64) {
s.usedAttemptIDsMu.Lock()
defer s.usedAttemptIDsMu.Unlock()

s.usedAttemptIDs.Add(attemptID)
}

// New creates a new instance of the SwitchServer given a configuration struct
// that contains all external dependencies. If the target macaroon exists, and
// we're unable to create it, then an error will be returned. We also return
Expand Down Expand Up @@ -141,7 +174,8 @@ func New(cfg *Config) (*Server, lnrpc.MacaroonPerms, error) {
}

switchServer := &Server{
cfg: cfg,
cfg: cfg,
usedAttemptIDs: roaring64.New(),
// quit: make(chan struct{}),
}

Expand Down Expand Up @@ -217,6 +251,7 @@ func (r *ServerShell) CreateSubServer(configRegistry lnrpc.SubServerConfigDispat
}

r.SwitchServer = subServer

return subServer, macPermissions, nil
}

Expand Down Expand Up @@ -294,6 +329,16 @@ func (s *Server) SendOnion(_ context.Context,
OnionBlob: [lnwire.OnionPacketSize]byte(req.OnionBlob),
}

// Make sure that SendOnion and TrackOnion have not been called with
// this AttemptID.
if !s.tryAddAttemptID(req.AttemptId) {
return &SendOnionResponse{
Success: false,
ErrorMessage: "potential AttemptID reuse detected",
ErrorCode: ErrorCode_ERROR_CODE_DUPLICATE_HTLC,
}, nil
}

log.Debugf("Dispatching HTLC attempt(id=%v, amt=%v) for payment=%v via "+
"first_hop=%x over channel=%s", req.AttemptId, req.Amount,
hash, req.FirstHopPubkey, chanID)
Expand Down Expand Up @@ -376,6 +421,10 @@ func (s *Server) TrackOnion(ctx context.Context,
"unable to process shared secrets")
}

// Mark this AttemptID as used so SendOnion can't reuse it later.
// this AttemptId.
s.addAttemptID(req.AttemptId)

// NOTE(calvin): In order to decrypt errors server side we require
// either the combination of session key and hop public keys from which
// we can construct the shared secrets used to build the onion or,
Expand Down

0 comments on commit 49ed723

Please sign in to comment.