You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

437 lines
12 KiB

package main
import (
"fmt"
"math/rand"
"sort"
"strings"
e2e "github.com/tendermint/tendermint/test/e2e/pkg"
"github.com/tendermint/tendermint/types"
)
var (
// testnetCombinations defines global testnet options, where we generate a
// separate testnet for each combination (Cartesian product) of options.
testnetCombinations = map[string][]interface{}{
"topology": {"single", "quad", "large"},
"p2p": {NewP2PMode, LegacyP2PMode, HybridP2PMode},
"queueType": {"priority"}, // "fifo", "wdrr"
"initialHeight": {0, 1000},
"initialState": {
map[string]string{},
map[string]string{"initial01": "a", "initial02": "b", "initial03": "c"},
},
"validators": {"genesis", "initchain"},
}
// The following specify randomly chosen values for testnet nodes.
nodeDatabases = weightedChoice{
"goleveldb": 35,
"badgerdb": 35,
"boltdb": 15,
"rocksdb": 10,
"cleveldb": 5,
}
nodeABCIProtocols = weightedChoice{
"builtin": 50,
"tcp": 20,
"grpc": 20,
"unix": 10,
}
nodePrivvalProtocols = weightedChoice{
"file": 50,
"grpc": 20,
"tcp": 20,
"unix": 10,
}
// FIXME: v2 disabled due to flake
nodeBlockSyncs = uniformChoice{"v0"} // "v2"
nodeMempools = uniformChoice{"v0", "v1"}
nodeStateSyncs = weightedChoice{
e2e.StateSyncDisabled: 10,
e2e.StateSyncP2P: 45,
e2e.StateSyncRPC: 45,
}
nodePersistIntervals = uniformChoice{0, 1, 5}
nodeSnapshotIntervals = uniformChoice{0, 5}
nodeRetainBlocks = uniformChoice{0, 2 * int(e2e.EvidenceAgeHeight), 4 * int(e2e.EvidenceAgeHeight)}
nodePerturbations = probSetChoice{
"disconnect": 0.1,
"pause": 0.1,
"kill": 0.1,
"restart": 0.1,
}
evidence = uniformChoice{0, 1, 10}
txSize = uniformChoice{1024, 4096} // either 1kb or 4kb
ipv6 = uniformChoice{false, true}
keyType = uniformChoice{types.ABCIPubKeyTypeEd25519, types.ABCIPubKeyTypeSecp256k1}
)
// Generate generates random testnets using the given RNG.
func Generate(r *rand.Rand, opts Options) ([]e2e.Manifest, error) {
manifests := []e2e.Manifest{}
switch opts.P2P {
case NewP2PMode, LegacyP2PMode, HybridP2PMode:
defer func() {
// avoid modifying the global state.
original := make([]interface{}, len(testnetCombinations["p2p"]))
copy(original, testnetCombinations["p2p"])
testnetCombinations["p2p"] = original
}()
testnetCombinations["p2p"] = []interface{}{opts.P2P}
case MixedP2PMode:
testnetCombinations["p2p"] = []interface{}{NewP2PMode, LegacyP2PMode, HybridP2PMode}
}
for _, opt := range combinations(testnetCombinations) {
manifest, err := generateTestnet(r, opt)
if err != nil {
return nil, err
}
if len(manifest.Nodes) < opts.MinNetworkSize {
continue
}
if len(manifest.Nodes) == 1 {
if opt["p2p"] == HybridP2PMode {
continue
}
}
if opts.MaxNetworkSize > 0 && len(manifest.Nodes) >= opts.MaxNetworkSize {
continue
}
manifests = append(manifests, manifest)
}
return manifests, nil
}
type Options struct {
MinNetworkSize int
MaxNetworkSize int
NumGroups int
Directory string
P2P P2PMode
Reverse bool
}
type P2PMode string
const (
NewP2PMode P2PMode = "new"
LegacyP2PMode P2PMode = "legacy"
HybridP2PMode P2PMode = "hybrid"
// mixed means that all combination are generated
MixedP2PMode P2PMode = "mixed"
)
// generateTestnet generates a single testnet with the given options.
func generateTestnet(r *rand.Rand, opt map[string]interface{}) (e2e.Manifest, error) {
manifest := e2e.Manifest{
IPv6: ipv6.Choose(r).(bool),
ABCIProtocol: nodeABCIProtocols.Choose(r),
InitialHeight: int64(opt["initialHeight"].(int)),
InitialState: opt["initialState"].(map[string]string),
Validators: &map[string]int64{},
ValidatorUpdates: map[string]map[string]int64{},
Nodes: map[string]*e2e.ManifestNode{},
KeyType: keyType.Choose(r).(string),
Evidence: evidence.Choose(r).(int),
QueueType: opt["queueType"].(string),
TxSize: int64(txSize.Choose(r).(int)),
}
p2pMode := opt["p2p"].(P2PMode)
switch p2pMode {
case NewP2PMode, LegacyP2PMode, HybridP2PMode:
default:
return manifest, fmt.Errorf("unknown p2p mode %s", p2pMode)
}
var numSeeds, numValidators, numFulls, numLightClients int
switch opt["topology"].(string) {
case "single":
numValidators = 1
case "quad":
numValidators = 4
case "large":
// FIXME Networks are kept small since large ones use too much CPU.
numSeeds = r.Intn(1)
numLightClients = r.Intn(2)
numValidators = 4 + r.Intn(4)
numFulls = r.Intn(4)
default:
return manifest, fmt.Errorf("unknown topology %q", opt["topology"])
}
const legacyP2PFactor float64 = 0.5
// First we generate seed nodes, starting at the initial height.
for i := 1; i <= numSeeds; i++ {
node := generateNode(r, manifest, e2e.ModeSeed, 0, false)
switch p2pMode {
case LegacyP2PMode:
node.UseLegacyP2P = true
case HybridP2PMode:
node.UseLegacyP2P = r.Float64() < legacyP2PFactor
}
manifest.Nodes[fmt.Sprintf("seed%02d", i)] = node
}
var (
numSyncingNodes = 0
hybridNumNew = 0
hybridNumLegacy = 0
)
// Next, we generate validators. We make sure a BFT quorum of validators start
// at the initial height, and that we have two archive nodes. We also set up
// the initial validator set, and validator set updates for delayed nodes.
nextStartAt := manifest.InitialHeight + 5
quorum := numValidators*2/3 + 1
for i := 1; i <= numValidators; i++ {
startAt := int64(0)
if i > quorum && numSyncingNodes < 2 && r.Float64() >= 0.25 {
numSyncingNodes++
startAt = nextStartAt
nextStartAt += 5
}
name := fmt.Sprintf("validator%02d", i)
node := generateNode(r, manifest, e2e.ModeValidator, startAt, i <= 2)
switch p2pMode {
case LegacyP2PMode:
node.UseLegacyP2P = true
case HybridP2PMode:
node.UseLegacyP2P = r.Float64() < legacyP2PFactor
if node.UseLegacyP2P {
hybridNumLegacy++
if hybridNumNew == 0 {
hybridNumNew++
hybridNumLegacy--
node.UseLegacyP2P = false
}
} else {
hybridNumNew++
if hybridNumLegacy == 0 {
hybridNumNew--
hybridNumLegacy++
node.UseLegacyP2P = true
}
}
}
manifest.Nodes[name] = node
if startAt == 0 {
(*manifest.Validators)[name] = int64(30 + r.Intn(71))
} else {
manifest.ValidatorUpdates[fmt.Sprint(startAt+5)] = map[string]int64{
name: int64(30 + r.Intn(71)),
}
}
}
// Move validators to InitChain if specified.
switch opt["validators"].(string) {
case "genesis":
case "initchain":
manifest.ValidatorUpdates["0"] = *manifest.Validators
manifest.Validators = &map[string]int64{}
default:
return manifest, fmt.Errorf("invalid validators option %q", opt["validators"])
}
// Finally, we generate random full nodes.
for i := 1; i <= numFulls; i++ {
startAt := int64(0)
if numSyncingNodes < 2 && r.Float64() >= 0.5 {
numSyncingNodes++
startAt = nextStartAt
nextStartAt += 5
}
node := generateNode(r, manifest, e2e.ModeFull, startAt, false)
switch p2pMode {
case LegacyP2PMode:
node.UseLegacyP2P = true
case HybridP2PMode:
node.UseLegacyP2P = r.Float64() > legacyP2PFactor
}
manifest.Nodes[fmt.Sprintf("full%02d", i)] = node
}
// We now set up peer discovery for nodes. Seed nodes are fully meshed with
// each other, while non-seed nodes either use a set of random seeds or a
// set of random peers that start before themselves.
var seedNames, peerNames, lightProviders []string
for name, node := range manifest.Nodes {
if node.Mode == string(e2e.ModeSeed) {
seedNames = append(seedNames, name)
} else {
// if the full node or validator is an ideal candidate, it is added as a light provider.
// There are at least two archive nodes so there should be at least two ideal candidates
if (node.StartAt == 0 || node.StartAt == manifest.InitialHeight) && node.RetainBlocks == 0 {
lightProviders = append(lightProviders, name)
}
peerNames = append(peerNames, name)
}
}
for _, name := range seedNames {
for _, otherName := range seedNames {
if name != otherName {
manifest.Nodes[name].Seeds = append(manifest.Nodes[name].Seeds, otherName)
}
}
}
sort.Slice(peerNames, func(i, j int) bool {
iName, jName := peerNames[i], peerNames[j]
switch {
case manifest.Nodes[iName].StartAt < manifest.Nodes[jName].StartAt:
return true
case manifest.Nodes[iName].StartAt > manifest.Nodes[jName].StartAt:
return false
default:
return strings.Compare(iName, jName) == -1
}
})
for i, name := range peerNames {
// there are seeds, statesync is disabled, and it's
// either the first peer by the sort order, and
// (randomly half of the remaining peers use a seed
// node; otherwise, choose some remaining set of the
// peers.
if len(seedNames) > 0 &&
manifest.Nodes[name].StateSync == e2e.StateSyncDisabled &&
(i == 0 || r.Float64() >= 0.5) {
// choose one of the seeds
manifest.Nodes[name].Seeds = uniformSetChoice(seedNames).Choose(r)
} else if i > 0 {
peers := uniformSetChoice(peerNames[:i])
if manifest.Nodes[name].StateSync == e2e.StateSyncP2P {
manifest.Nodes[name].PersistentPeers = peers.ChooseAtLeast(r, 2)
} else {
manifest.Nodes[name].PersistentPeers = peers.Choose(r)
}
}
}
// lastly, set up the light clients
for i := 1; i <= numLightClients; i++ {
startAt := manifest.InitialHeight + 5
node := generateLightNode(
r, startAt+(5*int64(i)), lightProviders,
)
switch p2pMode {
case LegacyP2PMode:
node.UseLegacyP2P = true
case HybridP2PMode:
node.UseLegacyP2P = r.Float64() < legacyP2PFactor
}
manifest.Nodes[fmt.Sprintf("light%02d", i)] = node
}
return manifest, nil
}
// generateNode randomly generates a node, with some constraints to avoid
// generating invalid configurations. We do not set Seeds or PersistentPeers
// here, since we need to know the overall network topology and startup
// sequencing.
func generateNode(
r *rand.Rand,
manifest e2e.Manifest,
mode e2e.Mode,
startAt int64,
forceArchive bool,
) *e2e.ManifestNode {
node := e2e.ManifestNode{
Mode: string(mode),
StartAt: startAt,
Database: nodeDatabases.Choose(r),
PrivvalProtocol: nodePrivvalProtocols.Choose(r),
BlockSync: nodeBlockSyncs.Choose(r).(string),
Mempool: nodeMempools.Choose(r).(string),
StateSync: e2e.StateSyncDisabled,
PersistInterval: ptrUint64(uint64(nodePersistIntervals.Choose(r).(int))),
SnapshotInterval: uint64(nodeSnapshotIntervals.Choose(r).(int)),
RetainBlocks: uint64(nodeRetainBlocks.Choose(r).(int)),
Perturb: nodePerturbations.Choose(r),
}
if startAt > 0 {
node.StateSync = nodeStateSyncs.Choose(r)
if manifest.InitialHeight-startAt <= 5 && node.StateSync == e2e.StateSyncDisabled {
// avoid needing to blocsync more than five total blocks.
node.StateSync = uniformSetChoice([]string{
e2e.StateSyncP2P,
e2e.StateSyncRPC,
}).Choose(r)[0]
}
}
// If this node is forced to be an archive node, retain all blocks and
// enable state sync snapshotting.
if forceArchive {
node.RetainBlocks = 0
node.SnapshotInterval = 3
}
// If a node which does not persist state also does not retain blocks, randomly
// choose to either persist state or retain all blocks.
if node.PersistInterval != nil && *node.PersistInterval == 0 && node.RetainBlocks > 0 {
if r.Float64() > 0.5 {
node.RetainBlocks = 0
} else {
node.PersistInterval = ptrUint64(node.RetainBlocks)
}
}
// If either PersistInterval or SnapshotInterval are greater than RetainBlocks,
// expand the block retention time.
if node.RetainBlocks > 0 {
if node.PersistInterval != nil && node.RetainBlocks < *node.PersistInterval {
node.RetainBlocks = *node.PersistInterval
}
if node.RetainBlocks < node.SnapshotInterval {
node.RetainBlocks = node.SnapshotInterval
}
}
if node.StateSync != e2e.StateSyncDisabled {
node.BlockSync = "v0"
}
return &node
}
func generateLightNode(r *rand.Rand, startAt int64, providers []string) *e2e.ManifestNode {
return &e2e.ManifestNode{
Mode: string(e2e.ModeLight),
StartAt: startAt,
Database: nodeDatabases.Choose(r),
PersistInterval: ptrUint64(0),
PersistentPeers: providers,
}
}
func ptrUint64(i uint64) *uint64 {
return &i
}