feat(p2p): enforce peer build versions on handshake
This commit is contained in:
parent
7b65270c62
commit
af7109e83c
5 changed files with 226 additions and 29 deletions
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
72
p2p/peer_version.go
Normal 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
59
p2p/peer_version_test.go
Normal 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")
|
||||
}
|
||||
15
sync_loop.go
15
sync_loop.go
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue