Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 15 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 @@ -1484,7 +1488,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)
}
})
}
}
Loading