Skip to content
6 changes: 4 additions & 2 deletions pkg/hive/hive.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ var (
)

type Service struct {
streamer p2p.Streamer
streamer p2p.Bee260CompatibilityStreamer
addressBook addressbook.GetPutter
addPeersHandler func(...swarm.Address)
networkID uint64
Expand All @@ -67,7 +67,7 @@ type Service struct {
overlay swarm.Address
}

func New(streamer p2p.Streamer, addressbook addressbook.GetPutter, networkID uint64, bootnode bool, allowPrivateCIDRs bool, overlay swarm.Address, logger log.Logger) *Service {
func New(streamer p2p.Bee260CompatibilityStreamer, addressbook addressbook.GetPutter, networkID uint64, bootnode bool, allowPrivateCIDRs bool, overlay swarm.Address, logger log.Logger) *Service {
svc := &Service{
streamer: streamer,
logger: logger.WithName(loggerName).Register(),
Expand Down Expand Up @@ -196,6 +196,8 @@ func (s *Service) sendPeers(ctx context.Context, peer swarm.Address, peers []swa
continue
}

advertisableUnderlays = p2p.FilterBee260CompatibleUnderlays(s.streamer.IsBee260(peer), advertisableUnderlays)

peersRequest.Peers = append(peersRequest.Peers, &pb.BzzAddress{
Overlay: addr.Overlay.Bytes(),
Underlay: bzz.SerializeUnderlays(advertisableUnderlays),
Expand Down
23 changes: 4 additions & 19 deletions pkg/p2p/libp2p/internal/handshake/handshake.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ func (s *Service) Handshake(ctx context.Context, stream p2p.Stream, peerMultiadd

w, r := protobuf.NewWriterAndReader(stream)

peerMultiaddrs = filterBee260CompatibleUnderlays(o.bee260compatibility, peerMultiaddrs)
peerMultiaddrs = p2p.FilterBee260CompatibleUnderlays(o.bee260compatibility, peerMultiaddrs)

if err := w.WriteMsgWithContext(ctx, &pb.Syn{
ObservedUnderlay: bzz.SerializeUnderlays(peerMultiaddrs),
Expand Down Expand Up @@ -208,7 +208,7 @@ func (s *Service) Handshake(ctx context.Context, stream p2p.Stream, peerMultiadd
return a.Equal(b)
})

advertisableUnderlays = filterBee260CompatibleUnderlays(o.bee260compatibility, advertisableUnderlays)
advertisableUnderlays = p2p.FilterBee260CompatibleUnderlays(o.bee260compatibility, advertisableUnderlays)

bzzAddress, err := bzz.NewAddress(s.signer, advertisableUnderlays, s.overlay, s.networkID, s.nonce)
if err != nil {
Expand Down Expand Up @@ -306,7 +306,7 @@ func (s *Service) Handle(ctx context.Context, stream p2p.Stream, peerMultiaddrs
return a.Equal(b)
})

advertisableUnderlays = filterBee260CompatibleUnderlays(o.bee260compatibility, advertisableUnderlays)
advertisableUnderlays = p2p.FilterBee260CompatibleUnderlays(o.bee260compatibility, advertisableUnderlays)

bzzAddress, err := bzz.NewAddress(s.signer, advertisableUnderlays, s.overlay, s.networkID, s.nonce)
if err != nil {
Expand All @@ -315,7 +315,7 @@ func (s *Service) Handle(ctx context.Context, stream p2p.Stream, peerMultiaddrs

welcomeMessage := s.GetWelcomeMessage()

peerMultiaddrs = filterBee260CompatibleUnderlays(o.bee260compatibility, peerMultiaddrs)
peerMultiaddrs = p2p.FilterBee260CompatibleUnderlays(o.bee260compatibility, peerMultiaddrs)

if err := w.WriteMsgWithContext(ctx, &pb.SynAck{
Syn: &pb.Syn{
Expand Down Expand Up @@ -395,18 +395,3 @@ func (s *Service) parseCheckAck(ack *pb.Ack) (*bzz.Address, error) {

return bzzAddress, nil
}

// filterBee260CompatibleUnderlays select a single underlay to pass if
// bee260compatibility is true. Otherwise it passes the unmodified underlays
// slice. This function can be safely removed when bee version 2.6.0 is
// deprecated.
func filterBee260CompatibleUnderlays(bee260compatibility bool, underlays []ma.Multiaddr) []ma.Multiaddr {
if !bee260compatibility {
return underlays
}
underlay := bzz.SelectBestAdvertisedAddress(underlays, nil)
if underlay == nil {
return underlays
}
return []ma.Multiaddr{underlay}
}
28 changes: 25 additions & 3 deletions pkg/p2p/libp2p/libp2p.go
Original file line number Diff line number Diff line change
Expand Up @@ -655,11 +655,13 @@ func (s *Service) handleIncoming(stream network.Stream) {
return
}

bee260Compat := s.bee260BackwardCompatibility(peerID)

i, err := s.handshakeService.Handle(
s.ctx,
handshakeStream,
peerMultiaddrs,
handshake.WithBee260Compatibility(s.bee260BackwardCompatibility(peerID)),
handshake.WithBee260Compatibility(bee260Compat),
)
if err != nil {
s.logger.Debug("stream handler: handshake: handle failed", "peer_id", peerID, "error", err)
Expand Down Expand Up @@ -1070,11 +1072,13 @@ func (s *Service) Connect(ctx context.Context, addrs []ma.Multiaddr) (address *b
return nil, fmt.Errorf("build peer multiaddrs: %w", err)
}

bee260Compat := s.bee260BackwardCompatibility(peerID)

i, err := s.handshakeService.Handshake(
s.ctx,
handshakeStream,
peerMultiaddrs,
handshake.WithBee260Compatibility(s.bee260BackwardCompatibility(peerID)),
handshake.WithBee260Compatibility(bee260Compat),
)
if err != nil {
_ = handshakeStream.Reset()
Expand Down Expand Up @@ -1471,6 +1475,16 @@ func (s *Service) peerMultiaddrs(ctx context.Context, peerID libp2ppeer.ID) ([]m
return buildFullMAs(waitPeerAddrs(waitPeersCtx, s.host.Peerstore(), peerID), peerID)
}

// IsBee260 implements p2p.Bee260CompatibilityStreamer interface.
// It checks if a peer is running Bee version older than 2.7.0.
func (s *Service) IsBee260(overlay swarm.Address) bool {
peerID, found := s.peers.peerID(overlay)
if !found {
return false
}
return s.bee260BackwardCompatibility(peerID)
}

var version270 = *semver.Must(semver.NewVersion("2.7.0"))

func (s *Service) bee260BackwardCompatibility(peerID libp2ppeer.ID) bool {
Expand All @@ -1484,7 +1498,15 @@ func (s *Service) bee260BackwardCompatibility(peerID libp2ppeer.ID) bool {
if err != nil {
return false
}
return v.LessThan(version270)

// Compare major.minor.patch only (ignore pre-release)
// This way 2.7.0-rc12 is treated as >= 2.7.0
vCore, err := semver.NewVersion(fmt.Sprintf("%d.%d.%d", v.Major, v.Minor, v.Patch))
if err != nil {
return false
}
result := vCore.LessThan(version270)
return result
}

// appendSpace adds a leading space character if the string is not empty.
Expand Down
165 changes: 165 additions & 0 deletions pkg/p2p/libp2p/version_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
// Copyright 2026 The Swarm Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package libp2p

import (
"context"
"testing"

"github.com/ethersphere/bee/v2/pkg/crypto"
"github.com/ethersphere/bee/v2/pkg/log"
"github.com/ethersphere/bee/v2/pkg/statestore/mock"
"github.com/ethersphere/bee/v2/pkg/swarm"
libp2ppeer "github.com/libp2p/go-libp2p/core/peer"
)

func TestBee260BackwardCompatibility(t *testing.T) {
t.Parallel()

tests := []struct {
name string
userAgent string
want bool
}{
// Versions < 2.7.0 should require backward compatibility
{
name: "version 2.6.0",
userAgent: "bee/2.6.0 go1.22.0 linux/amd64",
want: true,
},
{
name: "version 2.6.5",
userAgent: "bee/2.6.5 go1.22.0 linux/amd64",
want: true,
},
{
name: "version 2.5.0",
userAgent: "bee/2.5.0 go1.21.0 linux/amd64",
want: true,
},
{
name: "version 2.6.0-beta1",
userAgent: "bee/2.6.0-beta1 go1.22.0 linux/amd64",
want: true,
},
// Versions >= 2.7.0 should NOT require backward compatibility
{
name: "version 2.7.0",
userAgent: "bee/2.7.0 go1.23.0 linux/amd64",
want: false,
},
{
name: "version 2.8.0",
userAgent: "bee/2.8.0 go1.23.0 linux/amd64",
want: false,
},
{
name: "version 3.0.0",
userAgent: "bee/3.0.0 go1.25.0 linux/amd64",
want: false,
},
// Pre-release versions >= 2.7.0 should NOT require backward compatibility
// This is the critical fix: 2.7.0-rcX should be treated as >= 2.7.0
{
name: "version 2.7.0-rc1",
userAgent: "bee/2.7.0-rc1 go1.23.0 linux/amd64",
want: false,
},
{
name: "version 2.7.0-rc12",
userAgent: "bee/2.7.0-rc12-b39629d5-dirty go1.25.6 linux/amd64",
want: false,
},
{
name: "version 2.7.0-beta1",
userAgent: "bee/2.7.0-beta1 go1.23.0 linux/amd64",
want: false,
},
{
name: "version 2.8.0-rc1",
userAgent: "bee/2.8.0-rc1 go1.24.0 linux/amd64",
want: false,
},
{
name: "version 2.9.0-beta1",
userAgent: "bee/2.9.0-beta1 go1.24.0 linux/amd64",
want: false,
},
// Edge cases that should return false (not requiring backward compat)
{
name: "empty user agent",
userAgent: "",
want: false,
},
{
name: "malformed user agent missing space",
userAgent: "bee/2.6.0",
want: false,
},
{
name: "non-bee user agent",
userAgent: "other/1.0.0 go1.22.0 linux/amd64",
want: false,
},
{
name: "invalid version format",
userAgent: "bee/invalid go1.22.0 linux/amd64",
want: false,
},
{
name: "default libp2p user agent",
userAgent: "github.com/libp2p/go-libp2p",
want: false,
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()

// Create a service with minimal configuration
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

swarmKey, err := crypto.GenerateSecp256k1Key()
if err != nil {
t.Fatal(err)
}

overlay := swarm.RandAddress(t)
addr := ":0"
networkID := uint64(1)

statestore := mock.NewStateStore()
defer statestore.Close()

s, err := New(ctx, crypto.NewDefaultSigner(swarmKey), networkID, overlay, addr, nil, statestore, nil, log.Noop, nil, Options{})
if err != nil {
t.Fatal(err)
}
defer s.Close()

// Create a random test peer ID - we only need any valid libp2p peer ID
// The peerstore lookup will be mocked by setting the AgentVersion directly
libp2pPeerID, err := libp2ppeer.Decode("16Uiu2HAm3g4hXfCWTDhPBq3KkqpV3wGkPVgMJY3Jt8gGTYWiTWNZ")
if err != nil {
t.Fatal(err)
}

// Set the user agent in the peerstore if provided
if tc.userAgent != "" {
if err := s.host.Peerstore().Put(libp2pPeerID, "AgentVersion", tc.userAgent); err != nil {
t.Fatal(err)
}
}

// Test the backward compatibility check
got := s.bee260BackwardCompatibility(libp2pPeerID)
if got != tc.want {
t.Errorf("bee260BackwardCompatibility() = %v, want %v (userAgent: %q)", got, tc.want, tc.userAgent)
}
})
}
}
21 changes: 21 additions & 0 deletions pkg/p2p/p2p.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,12 @@ type Streamer interface {
NewStream(ctx context.Context, address swarm.Address, h Headers, protocol, version, stream string) (Stream, error)
}

// Bee260CompatibilityStreamer is able to create a new Stream and check if a peer is running Bee 2.6.0.
type Bee260CompatibilityStreamer interface {
NewStream(ctx context.Context, address swarm.Address, h Headers, protocol, version, stream string) (Stream, error)
IsBee260(address swarm.Address) bool
}

type StreamerDisconnecter interface {
Streamer
Disconnecter
Expand Down Expand Up @@ -237,3 +243,18 @@ func (e *ChunkDeliveryError) Error() string {
func NewChunkDeliveryError(msg string) error {
return &ChunkDeliveryError{msg: msg}
}

// FilterBee260CompatibleUnderlays select a single underlay to pass if
// bee260compatibility is true. Otherwise it passes the unmodified underlays
// slice. This function can be safely removed when bee version 2.6.0 is
// deprecated.
func FilterBee260CompatibleUnderlays(bee260compatibility bool, underlays []ma.Multiaddr) []ma.Multiaddr {
if !bee260compatibility {
return underlays
}
underlay := bzz.SelectBestAdvertisedAddress(underlays, nil)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here we could only return TCP. It is prefered, but 2.6 doesn't support other transports, so we can send TCP only.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

makes sense

if underlay == nil {
return underlays
}
return []ma.Multiaddr{underlay}
}
6 changes: 6 additions & 0 deletions pkg/p2p/streamtest/streamtest.go
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,12 @@ func (r *Recorder) WaitRecords(t *testing.T, addr swarm.Address, proto, version,
return recs
}

// IsBee260 implements p2p.Bee260CompatibilityStreamer interface.
// It always returns false.
func (r *Recorder) IsBee260(overlay swarm.Address) bool {
return false
}

type Record struct {
in *record
out *record
Expand Down
Loading