Skip to content

Commit 60d0001

Browse files
refactor(handshake): keep only the latest signed address in the cache
1 parent 538218d commit 60d0001

6 files changed

Lines changed: 49 additions & 114 deletions

File tree

pkg/p2p/libp2p/internal/handshake/addresscache.go

Lines changed: 20 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -5,55 +5,37 @@
55
package handshake
66

77
import (
8-
"container/list"
98
"sync"
109

1110
"github.com/ethersphere/bee/v2/pkg/bzz"
1211
)
1312

14-
// addressCache stores the session-stable signed BzzAddress per cache key,
15-
// evicting the least recently used entry when capacity is exceeded.
13+
// addressCache stores the most recently minted session-stable signed
14+
// BzzAddress and the key (chequebook + underlay set) it was minted for. A key
15+
// change makes the previous record obsolete, so only the latest one is kept.
1616
//
17-
// The cache owns the mutex and runs mint inside it, which guarantees two
18-
// invariants no per-method locking could:
17+
// Minting runs inside the mutex, which guarantees:
1918
//
20-
// - single flight: concurrent lookups for the same key produce exactly
21-
// one signed record;
22-
// - monotonic timestamps: every minted record carries a strictly greater
23-
// timestamp than the previous one, across all keys, even when the wall
24-
// clock repeats a second or steps backwards.
19+
// - single flight: concurrent lookups for the same key mint exactly once;
20+
// - monotonic timestamps: each mint is strictly newer than the last, even
21+
// when the wall clock repeats a second or steps back. Peers reject
22+
// records older than the one they hold (see bzz.CheckTimestamp).
2523
type addressCache struct {
26-
mu sync.Mutex
27-
cap int
28-
entries map[string]*list.Element
29-
lru *list.List // *cacheEntry elements, most recently used in front
30-
lastTS int64 // timestamp of the most recently minted address
24+
mu sync.Mutex
25+
key string
26+
addr *bzz.Address
27+
lastTS int64 // timestamp of the most recently minted address
3128
}
3229

33-
// cacheEntry pairs the cache key with the signed address minted for it.
34-
type cacheEntry struct {
35-
key string
36-
addr *bzz.Address
37-
}
38-
39-
func newAddressCache(capacity int) *addressCache {
40-
return &addressCache{
41-
cap: capacity,
42-
entries: make(map[string]*list.Element),
43-
lru: list.New(),
44-
}
45-
}
46-
47-
// getOrMint returns the cached address for key, or calls mint exactly once
48-
// with the next monotonic timestamp, max(now, last+1), and caches the result.
49-
// A failed mint does not consume the timestamp.
30+
// getOrMint returns the cached address on a key match, otherwise mints with
31+
// the next monotonic timestamp, max(now, last+1), and caches the result. A
32+
// failed mint does not consume the timestamp.
5033
func (c *addressCache) getOrMint(key string, now int64, mint func(timestamp int64) (*bzz.Address, error)) (*bzz.Address, error) {
5134
c.mu.Lock()
5235
defer c.mu.Unlock()
5336

54-
if el, ok := c.entries[key]; ok {
55-
c.lru.MoveToFront(el)
56-
return el.Value.(*cacheEntry).addr, nil
37+
if c.addr != nil && c.key == key {
38+
return c.addr, nil
5739
}
5840

5941
timestamp := max(now, c.lastTS+1)
@@ -63,30 +45,15 @@ func (c *addressCache) getOrMint(key string, now int64, mint func(timestamp int6
6345
return nil, err
6446
}
6547
c.lastTS = timestamp
66-
67-
c.entries[key] = c.lru.PushFront(&cacheEntry{key: key, addr: addr})
68-
if c.lru.Len() > c.cap {
69-
oldest := c.lru.Back()
70-
c.lru.Remove(oldest)
71-
delete(c.entries, oldest.Value.(*cacheEntry).key)
72-
}
48+
c.key, c.addr = key, addr
7349

7450
return addr, nil
7551
}
7652

77-
// purge drops all cached addresses; the next getOrMint per key re-signs.
53+
// purge drops the cached address; the next getOrMint re-signs.
7854
func (c *addressCache) purge() {
7955
c.mu.Lock()
8056
defer c.mu.Unlock()
8157

82-
clear(c.entries)
83-
c.lru.Init()
84-
}
85-
86-
// size returns the number of cached addresses.
87-
func (c *addressCache) size() int {
88-
c.mu.Lock()
89-
defer c.mu.Unlock()
90-
91-
return c.lru.Len()
58+
c.key, c.addr = "", nil
9259
}

pkg/p2p/libp2p/internal/handshake/export_test.go

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,6 @@ import (
1313
ma "github.com/multiformats/go-multiaddr"
1414
)
1515

16-
const MaxCachedAddresses = maxCachedAddresses
17-
1816
func (s *Service) SetTime(f func() time.Time) {
1917
s.now = f
2018
}
@@ -26,7 +24,3 @@ func (s *Service) ParseCheckAck(ctx context.Context, ack *pb.Ack) (*bzz.Address,
2624
func (s *Service) SignedAddress(underlays []ma.Multiaddr) (*bzz.Address, error) {
2725
return s.signedAddress(underlays)
2826
}
29-
30-
func (s *Service) AddressCacheLen() int {
31-
return s.addrCache.size()
32-
}

pkg/p2p/libp2p/internal/handshake/handshake.go

Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,6 @@ const (
4141
// MaxWelcomeMessageLength is maximum number of characters allowed in the welcome message.
4242
MaxWelcomeMessageLength = 140
4343
handshakeTimeout = 15 * time.Second
44-
// maxCachedAddresses bounds the signed-address cache so that peers
45-
// reporting fabricated observed underlays cannot force unbounded growth
46-
// or evict the legitimate entries fast enough to defeat record reuse.
47-
maxCachedAddresses = 16
4844
)
4945

5046
var (
@@ -107,7 +103,7 @@ type Service struct {
107103
hostAddresser Addresser
108104
now func() time.Time
109105

110-
addrCache *addressCache // session-stable signed addresses, keyed by chequebook + underlays
106+
addrCache addressCache // session-stable signed address, keyed by chequebook + underlays
111107
}
112108

113109
// Info contains the information received from the handshake.
@@ -146,19 +142,17 @@ func New(signer crypto.Signer, advertisableAddresser AdvertisableAddressResolver
146142
addressbook: addrbook,
147143
chequebookVerifier: chequebookVerifier,
148144
now: time.Now,
149-
addrCache: newAddressCache(maxCachedAddresses),
150145
}
151146
svc.welcomeMessage.Store(welcomeMessage)
152147

153148
return svc, nil
154149
}
155150

156151
// SetChequebookAddress sets the local chequebook address included in
157-
// subsequent signed BzzAddress payloads. The zero value clears it. Cached
158-
// signed addresses are invalidated so the next handshake re-signs with the
159-
// new chequebook. An entry minted by a handshake in flight during this call
160-
// may survive the purge, but is keyed under the old chequebook and can no
161-
// longer be looked up; it ages out of the cache unused.
152+
// subsequent signed BzzAddress payloads; the zero value clears it. The cached
153+
// signed address is purged so the next handshake re-signs. A mint in flight
154+
// during this call may land after the purge, but is keyed under the old
155+
// chequebook and simply gets overwritten by the next handshake.
162156
func (s *Service) SetChequebookAddress(addr common.Address) {
163157
if (addr == common.Address{}) {
164158
s.chequebookAddr.Store(nil)
@@ -178,11 +172,10 @@ func (s *Service) chequebookAddress() common.Address {
178172
}
179173

180174
// signedAddress returns the session-stable signed BzzAddress advertising the
181-
// given canonical underlay set, minting and caching one on first use. Reusing
182-
// the timestamp and signature keeps the record byte-stable across handshakes,
183-
// so receiving peers see it as unchanged and skip redundant addressbook
184-
// writes and gossip updates. A new address is minted only when the underlay
185-
// set or the local chequebook changes.
175+
// given canonical underlay set. The record is minted on first use and reused
176+
// byte-stable (same timestamp and signature) across handshakes, so receiving
177+
// peers see it as unchanged and skip redundant addressbook writes and gossip
178+
// updates. It is re-minted when the underlay set or the chequebook changes.
186179
func (s *Service) signedAddress(underlays []ma.Multiaddr) (*bzz.Address, error) {
187180
underlaysBinary, err := bzz.SerializeUnderlays(underlays)
188181
if err != nil {

pkg/p2p/libp2p/internal/handshake/handshake_test.go

Lines changed: 15 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1055,8 +1055,8 @@ func testUnderlays(t *testing.T, port int) []ma.Multiaddr {
10551055
}
10561056

10571057
// TestSignedAddress_StableAcrossCalls verifies that the signed address is
1058-
// minted once per underlay set and reused verbatim on subsequent calls, even
1059-
// when the clock advances (issue #23).
1058+
// minted once and reused verbatim on subsequent calls, even when the clock
1059+
// advances.
10601060
func TestSignedAddress_StableAcrossCalls(t *testing.T) {
10611061
t.Parallel()
10621062

@@ -1152,57 +1152,39 @@ func TestSignedAddress_ClockRegression(t *testing.T) {
11521152
}
11531153
}
11541154

1155-
// TestSignedAddress_CacheEviction verifies the LRU semantics of the signed
1156-
// address cache: recently used entries survive an overflow, the least
1157-
// recently used entry is evicted and re-minted on next use.
1158-
func TestSignedAddress_CacheEviction(t *testing.T) {
1155+
// TestSignedAddress_LatestEntryOnly verifies the cache keeps only the most
1156+
// recently minted address: returning to a previously used underlay set
1157+
// re-mints with a strictly newer timestamp instead of reviving the old record.
1158+
func TestSignedAddress_LatestEntryOnly(t *testing.T) {
11591159
t.Parallel()
11601160

11611161
networkID := uint64(3)
11621162
now := time.Unix(1700000000, 0)
11631163
svc := newTimestampTestService(t, networkID, now)
11641164

1165-
sets := make([][]ma.Multiaddr, handshake.MaxCachedAddresses+1)
1166-
timestamps := make([]int64, len(sets))
1167-
for i := range sets {
1168-
sets[i] = testUnderlays(t, 2000+i)
1169-
}
1170-
1171-
for i := range handshake.MaxCachedAddresses {
1172-
addr, err := svc.SignedAddress(sets[i])
1173-
if err != nil {
1174-
t.Fatal(err)
1175-
}
1176-
timestamps[i] = addr.Timestamp
1177-
}
1178-
1179-
// Touch the oldest entry so the upcoming eviction targets sets[1].
1180-
if _, err := svc.SignedAddress(sets[0]); err != nil {
1165+
first, err := svc.SignedAddress(testUnderlays(t, 2000))
1166+
if err != nil {
11811167
t.Fatal(err)
11821168
}
11831169

1184-
if _, err := svc.SignedAddress(sets[handshake.MaxCachedAddresses]); err != nil {
1170+
if _, err := svc.SignedAddress(testUnderlays(t, 2001)); err != nil {
11851171
t.Fatal(err)
11861172
}
11871173

1188-
if got := svc.AddressCacheLen(); got != handshake.MaxCachedAddresses {
1189-
t.Fatalf("cache length: got %d, want %d", got, handshake.MaxCachedAddresses)
1190-
}
1191-
1192-
survived, err := svc.SignedAddress(sets[0])
1174+
reminted, err := svc.SignedAddress(testUnderlays(t, 2000))
11931175
if err != nil {
11941176
t.Fatal(err)
11951177
}
1196-
if survived.Timestamp != timestamps[0] {
1197-
t.Fatalf("touched entry re-minted: got %d, want cached %d", survived.Timestamp, timestamps[0])
1178+
if reminted.Timestamp != first.Timestamp+2 {
1179+
t.Fatalf("expected re-mint with newer timestamp: got %d, want %d", reminted.Timestamp, first.Timestamp+2)
11981180
}
11991181

1200-
evicted, err := svc.SignedAddress(sets[1])
1182+
cached, err := svc.SignedAddress(testUnderlays(t, 2000))
12011183
if err != nil {
12021184
t.Fatal(err)
12031185
}
1204-
if evicted.Timestamp <= timestamps[1] {
1205-
t.Fatalf("evicted entry not re-minted: got %d, cached was %d", evicted.Timestamp, timestamps[1])
1186+
if cached.Timestamp != reminted.Timestamp || !bytes.Equal(cached.Signature, reminted.Signature) {
1187+
t.Fatal("latest entry not reused verbatim")
12061188
}
12071189
}
12081190

pkg/p2p/libp2p/internal/handshake/metrics.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ func newMetrics() metrics {
8181
Namespace: m.Namespace,
8282
Subsystem: subsystem,
8383
Name: "address_minted_total",
84-
Help: "Number of session-stable signed addresses minted (signed-address cache misses). Should plateau at a small number per session; linear growth with handshakes indicates advertised-underlay churn.",
84+
Help: "Number of session-stable signed addresses minted. Plateaus per session; linear growth indicates advertised-underlay churn.",
8585
}),
8686
TimestampRejected: prometheus.NewCounterVec(
8787
prometheus.CounterOpts{

pkg/p2p/libp2p/libp2p.go

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -773,11 +773,10 @@ func (s *Service) handleIncoming(stream network.Stream) {
773773
s.logger.Debug("stream handler: successfully connected to peer (inbound)", "address", i.BzzAddress.Overlay, "light", i.LightString(), "user_agent", peerUserAgent)
774774
}
775775

776-
// putHandshakeAddress persists a peer's BzzAddress from the handshake.
777-
// Timestamp and chequebook validation have already run in the handshake; here
778-
// we only need to perform the atomic registry-plus-addressbook write. The
779-
// addressbook write is skipped when the stored record is identical and
780-
// already verified, so steady-state reconnects cost no disk write.
776+
// putHandshakeAddress persists a peer's BzzAddress, already validated by the
777+
// handshake, as an atomic registry-plus-addressbook write. The addressbook
778+
// write is skipped when the stored record is identical and verified, so
779+
// steady-state reconnects cost no disk write.
781780
func (s *Service) putHandshakeAddress(addr *bzz.Address) error {
782781
existing, verified, err := s.addressbook.Get(addr.Overlay)
783782
if err != nil && !errors.Is(err, addressbook.ErrNotFound) {

0 commit comments

Comments
 (0)