mirror of
https://github.com/lightningnetwork/lnd.git
synced 2026-08-13 12:32:48 +02:00
Onion message forwarding is an unpaid side channel. Without any peer qualification the byte-bucket limiters added in the previous commits are our only defense against a Sybil attacker: an attacker that can cheaply spin up N identities and burn a full per-peer byte budget on each one saturates the global bucket and converts the aggregate cap into a service-denial primitive against legitimate channel peers. This was raised on PR review — the per-peer cap is good, but the global cap on its own is a Sybil multiplier if peer identity is free. The proper fix is to make new identities cost real capital, which is what requiring a funded channel does. This commit adds a channel-presence gate as the first check in allowOnionMessage, ahead of both the per-peer and the global rate limiters. Messages from peers that do not have at least one fully open channel with us are dropped with a new dropReasonNoChannel sentinel and never allocate any rate limiter state — the gate runs before either limiter is consulted, so no-channel peers cannot burn tokens on any bucket. Pending channels are deliberately excluded from the check: they are represented as nil values in the activeChannels map, are cheap to open and prone to getting stuck, and so do not provide the capital-cost guarantee the Sybil defense depends on. Existing Brontide cleanup paths (StopOnionActorIfExists, OnionPeerLimiter.Forget) already handle teardown on peer disconnect; nothing new is needed there because the gate keeps no-channel peers from ever allocating per-peer state in the first place. For the hot path we cannot afford to iterate the activeChannels registry on every incoming onion message, so Brontide now carries a numActiveChans atomic.Int32 that shadows the count of non-pending entries in activeChannels. hasActiveChannels is a single atomic Load and is therefore O(1). The counter is maintained in lockstep with activeChannels at every mutation site: loadActiveChannels increments it as it populates the registry during Start(); addActiveChannel uses a new lnutils.SyncMap.Swap method (a thin typed wrapper around sync.Map.Swap) to atomically replace any prior entry so that both brand-new channels and pending-to-active promotions bump the counter by exactly one; WipeChannel and handleRemovePendingChannel both use LoadAndDelete so they can inspect the prior value and only decrement when the removed entry was non-nil. Under race, this keeps the counter and the map consistent even when RPC WipeChannel races with the channelManager goroutine. The accompanying unit tests cover: the no-channel drop path at the allowOnionMessage level, asserting that neither the global stub counter nor the per-peer limiter's dropped counter move when the gate fires; the subsequent channel-gained path on the same peer, asserting the same message is accepted once hasChannel flips; and a focused Brontide-level test that walks the counter through initial emptiness, a pending-only state (counter must stay at zero), a pending-to-active promotion via direct Store + Add, the pending delete path through handleRemovePendingChannel (must not underflow), and the active delete path through LoadAndDelete + Add(-1) that WipeChannel uses internally. Running with -race confirms the Swap/LoadAndDelete patterns keep the counter and the map in sync under concurrent access.
247 lines
5.9 KiB
Go
247 lines
5.9 KiB
Go
package lnutils_test
|
|
|
|
import (
|
|
"errors"
|
|
"testing"
|
|
|
|
"github.com/lightningnetwork/lnd/lnutils"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// TestSyncMapStore tests the Store method of the SyncMap type.
|
|
func TestSyncMapStore(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Create a new SyncMap of string keys and integer values.
|
|
m := &lnutils.SyncMap[string, int]{}
|
|
|
|
// Test storing a new key-value pair.
|
|
m.Store("foo", 42)
|
|
value, ok := m.Load("foo")
|
|
require.True(t, ok)
|
|
require.Equal(t, 42, value)
|
|
|
|
// Test overwriting an existing key-value pair.
|
|
m.Store("foo", 99)
|
|
value, ok = m.Load("foo")
|
|
require.True(t, ok)
|
|
require.Equal(t, 99, value)
|
|
}
|
|
|
|
// TestSyncMapLoad tests the Load method of the SyncMap type.
|
|
func TestSyncMapLoad(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Create a new SyncMap of string keys and integer values.
|
|
m := &lnutils.SyncMap[string, int]{}
|
|
|
|
// Add some key-value pairs to the map.
|
|
m.Store("foo", 42)
|
|
m.Store("bar", 99)
|
|
|
|
// Test loading an existing key-value pair.
|
|
value, ok := m.Load("foo")
|
|
require.True(t, ok)
|
|
require.Equal(t, 42, value)
|
|
|
|
// Test loading a non-existing key-value pair.
|
|
value, ok = m.Load("baz")
|
|
require.False(t, ok)
|
|
require.Equal(t, 0, value)
|
|
}
|
|
|
|
// TestSyncMapDelete tests the Delete method of the SyncMap type.
|
|
func TestSyncMapDelete(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Create a new SyncMap of string keys and integer values.
|
|
m := &lnutils.SyncMap[string, int]{}
|
|
|
|
// Add some key-value pairs to the map.
|
|
m.Store("foo", 42)
|
|
m.Store("bar", 99)
|
|
|
|
// Test deleting an existing key-value pair.
|
|
m.Delete("foo")
|
|
_, ok := m.Load("foo")
|
|
require.False(t, ok)
|
|
|
|
// Test deleting a non-existing key-value pair.
|
|
m.Delete("baz")
|
|
_, ok = m.Load("baz")
|
|
require.False(t, ok)
|
|
}
|
|
|
|
// TestSyncMapLoadAndDelete tests the LoadAndDelete method of the SyncMap type.
|
|
func TestSyncMapLoadAndDelete(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Create a new SyncMap of string keys and integer values.
|
|
m := &lnutils.SyncMap[string, int]{}
|
|
|
|
// Add some key-value pairs to the map.
|
|
m.Store("foo", 42)
|
|
m.Store("bar", 99)
|
|
|
|
// Test loading and deleting an existing key-value pair.
|
|
value, ok := m.LoadAndDelete("foo")
|
|
require.True(t, ok)
|
|
require.Equal(t, 42, value)
|
|
|
|
// Verify that the pair was deleted from the map.
|
|
_, ok = m.Load("foo")
|
|
require.False(t, ok)
|
|
|
|
// Test loading and deleting a non-existing key-value pair.
|
|
value, ok = m.LoadAndDelete("baz")
|
|
require.False(t, ok)
|
|
require.Equal(t, 0, value)
|
|
|
|
// Verify that the map is unchanged.
|
|
require.Equal(t, 1, m.Len())
|
|
}
|
|
|
|
// TestSyncMapRange tests the Range method of the SyncMap type.
|
|
func TestSyncMapRange(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Create a new SyncMap and populate it with some values.
|
|
m := &lnutils.SyncMap[int, string]{}
|
|
m.Store(1, "one")
|
|
m.Store(2, "two")
|
|
m.Store(3, "three")
|
|
|
|
// Use Range to iterate over the map.
|
|
visited := 0
|
|
m.Range(func(key int, value string) bool {
|
|
visited++
|
|
|
|
return visited != 2
|
|
})
|
|
|
|
// Check we've only visited twice.
|
|
require.Equal(t, 2, visited)
|
|
}
|
|
|
|
// TestSyncMapRange tests the ForEach method of the SyncMap.
|
|
func TestSyncMapForEach(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Create a new SyncMap and add some items to it.
|
|
m := &lnutils.SyncMap[int, string]{}
|
|
m.Store(1, "one")
|
|
m.Store(2, "two")
|
|
m.Store(3, "three")
|
|
|
|
// Define the visitor function that will be applied to each item.
|
|
visited := 0
|
|
visitor := func(key int, value string) error {
|
|
visited++
|
|
if visited == 2 {
|
|
// Return an error to stop the iteration.
|
|
return errors.New("stop iteration")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Apply the visitor function to each item in the SyncMap.
|
|
m.ForEach(visitor)
|
|
|
|
// Verify that the iteration was stopped because of the error returned
|
|
// by the visitor function.
|
|
require.Equal(t, 2, visited)
|
|
}
|
|
|
|
// TestSyncMapRange tests the Len method of the SyncMap.
|
|
func TestSyncMapLen(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
require := require.New(t)
|
|
|
|
// Create a new SyncMap instance.
|
|
m := &lnutils.SyncMap[int, string]{}
|
|
|
|
// Add a few items to the map.
|
|
m.Store(1, "foo")
|
|
m.Store(2, "bar")
|
|
m.Store(3, "baz")
|
|
|
|
// Check that the map length is correct.
|
|
require.Equal(3, m.Len())
|
|
|
|
// Remove an item from the map.
|
|
m.Delete(2)
|
|
|
|
// Check that the map length is updated.
|
|
require.Equal(2, m.Len())
|
|
}
|
|
|
|
// TestSyncMapRange tests the LoadOrStore method of the SyncMap.
|
|
func TestSyncMapLoadOrStore(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Create a new SyncMap.
|
|
sm := &lnutils.SyncMap[int, string]{}
|
|
|
|
// Test loading non-existent items.
|
|
item, loaded := sm.LoadOrStore(1, "one")
|
|
require.False(t, loaded)
|
|
require.Equal(t, "one", item)
|
|
|
|
item, loaded = sm.LoadOrStore(2, "two")
|
|
require.False(t, loaded)
|
|
require.Equal(t, "two", item)
|
|
|
|
// Test loading existing items.
|
|
item, loaded = sm.LoadOrStore(1, "new one")
|
|
require.True(t, loaded)
|
|
require.Equal(t, "one", item)
|
|
|
|
item, loaded = sm.LoadOrStore(2, "new two")
|
|
require.True(t, loaded)
|
|
require.Equal(t, "two", item)
|
|
}
|
|
|
|
// TestSyncMapSwap tests the Swap method of the SyncMap type.
|
|
func TestSyncMapSwap(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Create a new SyncMap of string keys and integer values.
|
|
m := &lnutils.SyncMap[string, int]{}
|
|
|
|
// Swapping into an empty key should store the value and report no
|
|
// previous entry.
|
|
prev, loaded := m.Swap("foo", 42)
|
|
require.False(t, loaded)
|
|
require.Equal(t, 0, prev)
|
|
|
|
// The value should now be retrievable via Load.
|
|
value, ok := m.Load("foo")
|
|
require.True(t, ok)
|
|
require.Equal(t, 42, value)
|
|
|
|
// Swapping an existing key should return the previous value and
|
|
// report that it was present.
|
|
prev, loaded = m.Swap("foo", 99)
|
|
require.True(t, loaded)
|
|
require.Equal(t, 42, prev)
|
|
|
|
// Load should now return the new value.
|
|
value, ok = m.Load("foo")
|
|
require.True(t, ok)
|
|
require.Equal(t, 99, value)
|
|
|
|
// Swapping a second key should not affect the first.
|
|
prev, loaded = m.Swap("bar", 7)
|
|
require.False(t, loaded)
|
|
require.Equal(t, 0, prev)
|
|
|
|
value, ok = m.Load("foo")
|
|
require.True(t, ok)
|
|
require.Equal(t, 99, value)
|
|
|
|
value, ok = m.Load("bar")
|
|
require.True(t, ok)
|
|
require.Equal(t, 7, value)
|
|
}
|