feat(p2p): enforce peer build versions on handshake

This commit is contained in:
Virgil 2026-04-04 13:15:18 +00:00
parent 7b65270c62
commit af7109e83c
5 changed files with 226 additions and 29 deletions

View file

@ -17,6 +17,7 @@ import (
"dappco.re/go/core/blockchain/chain"
"dappco.re/go/core/blockchain/config"
"dappco.re/go/core/blockchain/p2p"
"github.com/spf13/cobra"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@ -341,3 +342,46 @@ func TestRunChainSyncOnce_Bad_ReportsHeightLookupError(t *testing.T) {
assert.ErrorContains(t, err, "read local height")
assert.ErrorContains(t, err, "height failed")
}
func TestValidateHandshakePeer_Good_AcceptsMatchingNetworkAndVersion(t *testing.T) {
resp := &p2p.HandshakeResponse{
NodeData: p2p.NodeData{
NetworkID: config.NetworkIDTestnet,
},
PayloadData: p2p.CoreSyncData{
ClientVersion: config.ClientVersion,
},
}
require.NoError(t, validateHandshakePeer(&config.Testnet, resp))
}
func TestValidateHandshakePeer_Bad_RejectsNetworkMismatch(t *testing.T) {
resp := &p2p.HandshakeResponse{
NodeData: p2p.NodeData{
NetworkID: config.NetworkIDMainnet,
},
PayloadData: p2p.CoreSyncData{
ClientVersion: config.ClientVersion,
},
}
err := validateHandshakePeer(&config.Testnet, resp)
require.Error(t, err)
assert.ErrorContains(t, err, "network ID")
}
func TestValidateHandshakePeer_Bad_RejectsStalePeerVersion(t *testing.T) {
resp := &p2p.HandshakeResponse{
NodeData: p2p.NodeData{
NetworkID: config.NetworkIDMainnet,
},
PayloadData: p2p.CoreSyncData{
ClientVersion: "5.9.9",
},
}
err := validateHandshakePeer(&config.Mainnet, resp)
require.Error(t, err)
assert.ErrorContains(t, err, "below minimum")
}

View file

@ -215,14 +215,14 @@ const (
// ---------------------------------------------------------------------------
const (
BlockMajorVersionGenesis uint8 = 1
BlockMinorVersionGenesis uint8 = 0
BlockMajorVersionInitial uint8 = 0
HF1BlockMajorVersion uint8 = 1
HF3BlockMajorVersion uint8 = 2
HF3BlockMinorVersion uint8 = 0
CurrentBlockMajorVersion uint8 = 3
CurrentBlockMinorVersion uint8 = 0
BlockMajorVersionGenesis uint8 = 1
BlockMinorVersionGenesis uint8 = 0
BlockMajorVersionInitial uint8 = 0
HF1BlockMajorVersion uint8 = 1
HF3BlockMajorVersion uint8 = 2
HF3BlockMinorVersion uint8 = 0
CurrentBlockMajorVersion uint8 = 3
CurrentBlockMinorVersion uint8 = 0
)
// ---------------------------------------------------------------------------
@ -230,11 +230,11 @@ const (
// ---------------------------------------------------------------------------
const (
TransactionVersionInitial uint8 = 0
TransactionVersionPreHF4 uint8 = 1
TransactionVersionPostHF4 uint8 = 2
TransactionVersionPostHF5 uint8 = 3
CurrentTransactionVersion uint8 = 3
TransactionVersionInitial uint8 = 0
TransactionVersionPreHF4 uint8 = 1
TransactionVersionPostHF4 uint8 = 2
TransactionVersionPostHF5 uint8 = 3
CurrentTransactionVersion uint8 = 3
)
// ---------------------------------------------------------------------------
@ -242,12 +242,12 @@ const (
// ---------------------------------------------------------------------------
const (
PosScanWindow uint64 = 60 * 10 // 10 minutes in seconds
PosScanStep uint64 = 15 // seconds
PosModifierInterval uint64 = 10
PosMinimumCoinstakeAge uint64 = 10 // blocks
PosStrictSequenceLimit uint64 = 20
PosStarterKernelHash = "00000000000000000006382a8d8f94588ce93a1351924f6ccb9e07dd287c6e4b"
PosScanWindow uint64 = 60 * 10 // 10 minutes in seconds
PosScanStep uint64 = 15 // seconds
PosModifierInterval uint64 = 10
PosMinimumCoinstakeAge uint64 = 10 // blocks
PosStrictSequenceLimit uint64 = 20
PosStarterKernelHash = "00000000000000000006382a8d8f94588ce93a1351924f6ccb9e07dd287c6e4b"
)
// ---------------------------------------------------------------------------
@ -255,13 +255,21 @@ const (
// ---------------------------------------------------------------------------
const (
P2PLocalWhitePeerlistLimit uint64 = 1000
P2PLocalGrayPeerlistLimit uint64 = 5000
P2PDefaultConnectionsCount uint64 = 8
P2PDefaultHandshakeInterval uint64 = 60 // seconds
P2PDefaultPacketMaxSize uint64 = 50_000_000
P2PIPBlockTime uint64 = 60 * 60 * 24 // 24 hours
P2PIPFailsBeforeBlock uint64 = 10
P2PLocalWhitePeerlistLimit uint64 = 1000
P2PLocalGrayPeerlistLimit uint64 = 5000
P2PDefaultConnectionsCount uint64 = 8
P2PDefaultHandshakeInterval uint64 = 60 // seconds
P2PDefaultPacketMaxSize uint64 = 50_000_000
P2PIPBlockTime uint64 = 60 * 60 * 24 // 24 hours
P2PIPFailsBeforeBlock uint64 = 10
// MinimumPeerBuildVersionMainnet is the minimum accepted peer build
// version on mainnet after the HF5-era client version enforcement.
MinimumPeerBuildVersionMainnet uint64 = 601
// MinimumPeerBuildVersionTestnet is the minimum accepted peer build
// version on testnet after the HF5-era client version enforcement.
MinimumPeerBuildVersionTestnet uint64 = 2
)
// ---------------------------------------------------------------------------
@ -304,7 +312,6 @@ var NetworkIDTestnet = [16]byte{
// is below the minimum for the current hard-fork era.
const ClientVersion = "6.0.1.2[go-blockchain]"
// ---------------------------------------------------------------------------
// Currency identity
// ---------------------------------------------------------------------------
@ -465,7 +472,7 @@ var Mainnet = ChainConfig{
HF4MandatoryDecoySetSize: HF4MandatoryDecoySetSize,
MinedMoneyUnlockWindow: MinedMoneyUnlockWindow,
P2PMaintainersPubKey: "8f138bb73f6d663a3746a542770781a09579a7b84cb4125249e95530824ee607",
NetworkID: NetworkIDMainnet,
NetworkID: NetworkIDMainnet,
}
// Testnet holds the chain configuration for the Lethean testnet.
@ -501,5 +508,5 @@ var Testnet = ChainConfig{
HF4MandatoryDecoySetSize: HF4MandatoryDecoySetSize,
MinedMoneyUnlockWindow: MinedMoneyUnlockWindow,
P2PMaintainersPubKey: "8f138bb73f6d663a3746a542770781a09579a7b84cb4125249e95530824ee607",
NetworkID: NetworkIDTestnet,
NetworkID: NetworkIDTestnet,
}

72
p2p/peer_version.go Normal file
View file

@ -0,0 +1,72 @@
// Copyright (c) 2017-2026 Lethean (https://lt.hn)
//
// Licensed under the European Union Public Licence (EUPL) version 1.2.
// SPDX-License-Identifier: EUPL-1.2
package p2p
import (
"fmt"
"regexp"
"strconv"
"dappco.re/go/core/blockchain/config"
)
var clientVersionPattern = regexp.MustCompile(`(\d+)(?:\.(\d+))?(?:\.(\d+))?`)
// ParseBuildVersion extracts the C++-style build version from a peer client
// version string. The first three numeric components are interpreted as
// major.minor.revision and encoded as major*100 + minor*10 + revision.
func ParseBuildVersion(clientVersion string) (uint64, error) {
match := clientVersionPattern.FindStringSubmatch(clientVersion)
if len(match) == 0 {
return 0, fmt.Errorf("parse peer client version %q: no numeric version found", clientVersion)
}
parts := [3]uint64{}
for i := 1; i <= 3; i++ {
if match[i] == "" {
continue
}
n, err := strconv.ParseUint(match[i], 10, 64)
if err != nil {
return 0, fmt.Errorf("parse peer client version %q: %w", clientVersion, err)
}
parts[i-1] = n
}
return parts[0]*100 + parts[1]*10 + parts[2], nil
}
// MinimumPeerBuildVersion returns the minimum allowed peer build version for
// the given network ID.
func MinimumPeerBuildVersion(networkID [16]byte) (uint64, error) {
switch networkID {
case config.NetworkIDMainnet:
return config.MinimumPeerBuildVersionMainnet, nil
case config.NetworkIDTestnet:
return config.MinimumPeerBuildVersionTestnet, nil
default:
return 0, fmt.Errorf("unknown network ID %x", networkID)
}
}
// ValidatePeerClientVersion rejects peers whose advertised client version is
// below the minimum build required for the active network.
func ValidatePeerClientVersion(networkID [16]byte, clientVersion string) error {
minBuild, err := MinimumPeerBuildVersion(networkID)
if err != nil {
return err
}
build, err := ParseBuildVersion(clientVersion)
if err != nil {
return err
}
if build < minBuild {
return fmt.Errorf("peer build version %d from %q is below minimum %d", build, clientVersion, minBuild)
}
return nil
}

59
p2p/peer_version_test.go Normal file
View file

@ -0,0 +1,59 @@
// Copyright (c) 2017-2026 Lethean (https://lt.hn)
//
// Licensed under the European Union Public Licence (EUPL) version 1.2.
// SPDX-License-Identifier: EUPL-1.2
package p2p
import (
"testing"
"dappco.re/go/core/blockchain/config"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestParseBuildVersion_Good_CurrentClientVersion(t *testing.T) {
build, err := ParseBuildVersion(config.ClientVersion)
require.NoError(t, err)
assert.Equal(t, uint64(601), build)
}
func TestParseBuildVersion_Good_EmbeddedPrefix(t *testing.T) {
build, err := ParseBuildVersion("Zano/6.0.1.2[commit]")
require.NoError(t, err)
assert.Equal(t, uint64(601), build)
}
func TestParseBuildVersion_Bad_RejectsNonNumericVersion(t *testing.T) {
_, err := ParseBuildVersion("go-blockchain")
require.Error(t, err)
assert.ErrorContains(t, err, "no numeric version found")
}
func TestMinimumPeerBuildVersion_Good(t *testing.T) {
build, err := MinimumPeerBuildVersion(config.NetworkIDMainnet)
require.NoError(t, err)
assert.Equal(t, config.MinimumPeerBuildVersionMainnet, build)
build, err = MinimumPeerBuildVersion(config.NetworkIDTestnet)
require.NoError(t, err)
assert.Equal(t, config.MinimumPeerBuildVersionTestnet, build)
}
func TestValidatePeerClientVersion_Good_MainnetAndTestnet(t *testing.T) {
require.NoError(t, ValidatePeerClientVersion(config.NetworkIDMainnet, config.ClientVersion))
require.NoError(t, ValidatePeerClientVersion(config.NetworkIDTestnet, "test/0.2"))
}
func TestValidatePeerClientVersion_Bad_RejectsBelowMinimum(t *testing.T) {
err := ValidatePeerClientVersion(config.NetworkIDMainnet, "5.9.9")
require.Error(t, err)
assert.ErrorContains(t, err, "below minimum")
}
func TestValidatePeerClientVersion_Bad_RejectsUnknownNetwork(t *testing.T) {
err := ValidatePeerClientVersion([16]byte{}, config.ClientVersion)
require.Error(t, err)
assert.ErrorContains(t, err, "unknown network ID")
}

View file

@ -126,6 +126,9 @@ func runChainSyncOnce(ctx context.Context, blockchain *chain.Chain, chainConfig
if err := handshakeResp.Decode(data); err != nil {
return coreerr.E("runChainSyncOnce", "decode handshake", err)
}
if err := validateHandshakePeer(chainConfig, &handshakeResp); err != nil {
return coreerr.E("runChainSyncOnce", "validate handshake", err)
}
if err := tcpConn.SetDeadline(time.Time{}); err != nil {
return coreerr.E("runChainSyncOnce", "clear handshake deadline", err)
}
@ -139,3 +142,15 @@ func runChainSyncOnce(ctx context.Context, blockchain *chain.Chain, chainConfig
return blockchain.P2PSync(ctx, p2pConn, opts)
}
func validateHandshakePeer(chainConfig *config.ChainConfig, handshakeResp *p2p.HandshakeResponse) error {
if handshakeResp.NodeData.NetworkID != chainConfig.NetworkID {
return fmt.Errorf("peer network ID %x does not match expected %x", handshakeResp.NodeData.NetworkID, chainConfig.NetworkID)
}
if err := p2p.ValidatePeerClientVersion(chainConfig.NetworkID, handshakeResp.PayloadData.ClientVersion); err != nil {
return err
}
return nil
}