From 8101f7fe4dc053598f09f3aee7cb410b73073e57 Mon Sep 17 00:00:00 2001 From: Lukasz Klimek <842586+lklimek@users.noreply.github.com> Date: Mon, 23 Sep 2024 16:24:33 +0200 Subject: [PATCH] test(e2e): support consensus version update testing --- test/e2e/networks/rotate.toml | 23 +++++++++ test/e2e/node/config.go | 2 + test/e2e/pkg/manifest.go | 9 ++++ test/e2e/pkg/testnet.go | 11 +++++ test/e2e/runner/setup.go | 8 +++ test/e2e/tests/e2e_test.go | 4 ++ test/e2e/tests/validator_test.go | 85 ++++++++++++++++++++++++++++---- 7 files changed, 132 insertions(+), 10 deletions(-) diff --git a/test/e2e/networks/rotate.toml b/test/e2e/networks/rotate.toml index 14190da099..d386beb898 100644 --- a/test/e2e/networks/rotate.toml +++ b/test/e2e/networks/rotate.toml @@ -80,6 +80,29 @@ validator04 = 100 validator05 = 100 validator11 = 100 + +[validator_update.1070] +validator01 = 100 +validator02 = 100 +validator03 = 100 +validator04 = 100 +validator05 = 100 + + +[validator_update.1077] +validator01 = 100 +validator07 = 100 +validator08 = 100 +validator10 = 100 +validator11 = 100 + + +[consensus_version_updates] + +1070 = 1 +1076 = 0 +1079 = 1 + [node.seed01] mode = "seed" perturb = ["restart"] diff --git a/test/e2e/node/config.go b/test/e2e/node/config.go index edca4e9cb6..c64740de2f 100644 --- a/test/e2e/node/config.go +++ b/test/e2e/node/config.go @@ -30,6 +30,7 @@ type Config struct { QuorumHashUpdate map[string]string `toml:"quorum_hash_update"` ChainLockUpdates map[string]string `toml:"chainlock_updates"` PrivValServerType string `toml:"privval_server_type"` + ConsenusVersionUpdates map[string]int32 `toml:"consensus_version_updates"` } // App extracts out the application specific configuration parameters @@ -46,6 +47,7 @@ func (cfg *Config) App() *kvstore.Config { QuorumHashUpdate: cfg.QuorumHashUpdate, ChainLockUpdates: cfg.ChainLockUpdates, PrivValServerType: cfg.PrivValServerType, + ConsenusVersionUpdates: cfg.ConsenusVersionUpdates, } } diff --git a/test/e2e/pkg/manifest.go b/test/e2e/pkg/manifest.go index a070b2b398..ec1de91c1c 100644 --- a/test/e2e/pkg/manifest.go +++ b/test/e2e/pkg/manifest.go @@ -56,6 +56,15 @@ type Manifest struct { // not specified are not changed. ValidatorUpdates map[string]map[string]int64 `toml:"validator_update"` + // ConsensusVersionUpdates is a map of heights to consensus versions, and + // will be sent by the ABCI application as a consensus params update. + // For example, the following sets the consensus version to 1 at height 1000: + // + // [consensus_version_updates] + // 1000 = 1 + // + + ConsensusVersionUpdates map[string]int32 `toml:"consensus_version_updates"` // ChainLockUpdates is a map of heights at which a new chain lock should be proposed // The first number is the tendermint height, and the second is the // diff --git a/test/e2e/pkg/testnet.go b/test/e2e/pkg/testnet.go index 44ca245d28..61ad677410 100644 --- a/test/e2e/pkg/testnet.go +++ b/test/e2e/pkg/testnet.go @@ -102,6 +102,9 @@ type Testnet struct { QuorumType btcjson.LLMQType QuorumHash crypto.QuorumHash QuorumHashUpdates map[int64]crypto.QuorumHash + // ConsensusVersionUpdates is maps height where consensus params updates shall be generated + // to ConsensusParams.Version.ProtocolVersion. + ConsensusVersionUpdates map[int64]int32 } // Node represents a Tenderdash node in a testnet. @@ -216,6 +219,7 @@ func LoadTestnet(file string) (*Testnet, error) { QuorumType: btcjson.LLMQType(quorumType), QuorumHash: quorumHash, QuorumHashUpdates: map[int64]crypto.QuorumHash{}, + ConsensusVersionUpdates: map[int64]int32{}, } if len(manifest.KeyType) != 0 { testnet.KeyType = manifest.KeyType @@ -425,6 +429,13 @@ func LoadTestnet(file string) (*Testnet, error) { testnet.ValidatorUpdates[int64(height)] = valUpdate testnet.ThresholdPublicKeyUpdates[int64(height)] = ld.ThresholdPubKey testnet.QuorumHashUpdates[int64(height)] = quorumHash + if cpUpdate, ok := manifest.ConsensusVersionUpdates[strconv.Itoa(height)]; ok { + h, err := strconv.Atoi(heightStr) + if err != nil { + return nil, fmt.Errorf("invalid consensus version update height %q: %w", height, err) + } + testnet.ConsensusVersionUpdates[int64(h)] = cpUpdate + } } chainLockSetHeights := make([]int, 0, len(manifest.ChainLockUpdates)) diff --git a/test/e2e/runner/setup.go b/test/e2e/runner/setup.go index a5395ba8fd..b15302408a 100644 --- a/test/e2e/runner/setup.go +++ b/test/e2e/runner/setup.go @@ -438,6 +438,14 @@ func MakeAppConfig(node *e2e.Node) ([]byte, error) { cfg["chainlock_updates"] = chainLockUpdates } + if len(node.Testnet.ConsensusVersionUpdates) > 0 { + consensusVersionUpdates := map[string]int32{} + for height, version := range node.Testnet.ConsensusVersionUpdates { + consensusVersionUpdates[strconv.Itoa(int(height))] = version //#nosec:G115 + } + cfg["consensus_version_updates"] = consensusVersionUpdates + } + var buf bytes.Buffer err := toml.NewEncoder(&buf).Encode(cfg) if err != nil { diff --git a/test/e2e/tests/e2e_test.go b/test/e2e/tests/e2e_test.go index c8e72d0b68..2cb25ade2b 100644 --- a/test/e2e/tests/e2e_test.go +++ b/test/e2e/tests/e2e_test.go @@ -122,6 +122,10 @@ func fetchBlockChain(ctx context.Context, t *testing.T) []*types.Block { from := status.SyncInfo.EarliestBlockHeight to := status.SyncInfo.LatestBlockHeight + // limit blocks to 100 to avoid long test times + if to-from > 100 { + to = from + 100 + } blocks, ok := blocksCache[testnet.Name] if !ok { blocks = make([]*types.Block, 0, to-from+1) diff --git a/test/e2e/tests/validator_test.go b/test/e2e/tests/validator_test.go index a13a19935f..b816440861 100644 --- a/test/e2e/tests/validator_test.go +++ b/test/e2e/tests/validator_test.go @@ -3,6 +3,7 @@ package e2e_test import ( "bytes" "context" + "fmt" "testing" "github.com/dashpay/dashd-go/btcjson" @@ -11,6 +12,7 @@ import ( "github.com/dashpay/tenderdash/crypto" cryptoenc "github.com/dashpay/tenderdash/crypto/encoding" + "github.com/dashpay/tenderdash/internal/consensus/versioned/selectproposer" tmbytes "github.com/dashpay/tenderdash/libs/bytes" e2e "github.com/dashpay/tenderdash/test/e2e/pkg" "github.com/dashpay/tenderdash/types" @@ -35,6 +37,10 @@ func TestValidator_Sets(t *testing.T) { } last := status.SyncInfo.LatestBlockHeight + // limit the test to 100 blocks, to avoid long test times + if last > first+100 { + last = first + 100 + } // skip first block if node is pruning blocks, to avoid race conditions if node.RetainBlocks > 0 { @@ -62,7 +68,7 @@ func TestValidator_Sets(t *testing.T) { } // fmt.Printf("node %s(%X) validator set for height %d is %v\n", // node.Name, node.ProTxHash, h, valSchedule.Set) - for i, valScheduleValidator := range valSchedule.Set.Validators { + for i, valScheduleValidator := range valSchedule.ValidatorProposer.ValidatorSet().Validators { validator := validators[i] require.Equal(t, valScheduleValidator.ProTxHash, validator.ProTxHash, "mismatching validator proTxHashes at height %v (%X <=> %X", h, @@ -74,9 +80,9 @@ func TestValidator_Sets(t *testing.T) { // Validators in the schedule don't contain addresses validator.NodeAddress = types.ValidatorAddress{} } - require.Equal(t, valSchedule.Set.Validators, validators, + require.Equal(t, valSchedule.ValidatorProposer.ValidatorSet().Validators, validators, "incorrect validator set at height %v", h) - require.Equal(t, valSchedule.Set.ThresholdPublicKey, thresholdPublicKey, + require.Equal(t, valSchedule.ValidatorProposer.ValidatorSet().ThresholdPublicKey, thresholdPublicKey, "incorrect thresholdPublicKey at height %v", h) require.NoError(t, valSchedule.Increment(1)) } @@ -122,8 +128,13 @@ func TestValidator_Propose(t *testing.T) { expectCount := 0 proposeCount := 0 - for _, block := range blocks { - if bytes.Equal(valSchedule.Set.Proposer().ProTxHash, proTxHash) { + for i, block := range blocks { + round := int32(0) + if i+1 < len(blocks) { // we might be missing the last commit, we'll assume round 0 + round = blocks[i+1].LastCommit.Round + } + expectedProposer := valSchedule.ValidatorProposer.MustGetProposer(block.Height, round).ProTxHash + if bytes.Equal(expectedProposer, proTxHash) { expectCount++ if bytes.Equal(block.ProposerProTxHash, proTxHash) { proposeCount++ @@ -146,11 +157,14 @@ func TestValidator_Propose(t *testing.T) { // validatorSchedule is a validator set iterator, which takes into account // validator set updates. type validatorSchedule struct { - Set *types.ValidatorSet + ValidatorProposer selectproposer.ProposerSelector height int64 updates map[int64]e2e.ValidatorsMap thresholdPublicKeyUpdates map[int64]crypto.PubKey quorumHashUpdates map[int64]crypto.QuorumHash + consensusVersionUpdates map[int64]int32 + + consensusVersions map[int64]int32 } func newValidatorSchedule(testnet e2e.Testnet) *validatorSchedule { @@ -173,36 +187,87 @@ func newValidatorSchedule(testnet e2e.Testnet) *validatorSchedule { } } + vs := types.NewValidatorSet(makeVals(valMap), thresholdPublicKey, quorumType, quorumHash, true) + ps, err := selectproposer.NewProposerSelector(*types.DefaultConsensusParams(), vs, testnet.InitialHeight, 0, nil, nil) + if err != nil { + panic(err) + } + return &validatorSchedule{ height: testnet.InitialHeight, - Set: types.NewValidatorSet(makeVals(valMap), thresholdPublicKey, quorumType, quorumHash, true), + ValidatorProposer: ps, updates: testnet.ValidatorUpdates, thresholdPublicKeyUpdates: testnet.ThresholdPublicKeyUpdates, quorumHashUpdates: testnet.QuorumHashUpdates, + consensusVersions: make(map[int64]int32), + consensusVersionUpdates: testnet.ConsensusVersionUpdates, } } +func (s *validatorSchedule) consensusVersionUpdate() { + ver, ok := s.consensusVersionUpdates[s.height] + if !ok && s.height-1 > 0 { + ver = s.consensusVersions[s.height-1] + } + + s.consensusVersions[s.height] = ver +} + +func (s *validatorSchedule) ConsensusParams() types.ConsensusParams { + ver, ok := s.consensusVersions[s.height] + if !ok { + panic(fmt.Errorf("consensus version not set for height %d", s.height)) + } + + cp := *types.DefaultConsensusParams() + cp.Version.ConsensusVersion = ver + + return cp +} func (s *validatorSchedule) Increment(heights int64) error { for i := int64(0); i < heights; i++ { s.height++ + + // consensus params update - for now, we only support consensus version updates + s.consensusVersionUpdate() + cp := s.ConsensusParams() + + // validator set update if s.height > 1 { // validator set updates are offset by 1, since they only take effect // 1 block after they're returned. if update, ok := s.updates[s.height-1]; ok { if thresholdPublicKeyUpdate, ok := s.thresholdPublicKeyUpdates[s.height-1]; ok { if quorumHashUpdate, ok := s.quorumHashUpdates[s.height-1]; ok { - if bytes.Equal(quorumHashUpdate, s.Set.QuorumHash) { - if err := s.Set.UpdateWithChangeSet(makeVals(update), thresholdPublicKeyUpdate, quorumHashUpdate); err != nil { + currentQuorumHash := s.ValidatorProposer.ValidatorSet().QuorumHash + if bytes.Equal(quorumHashUpdate, currentQuorumHash) { + vs := s.ValidatorProposer.ValidatorSet() + + if err := vs.UpdateWithChangeSet(makeVals(update), thresholdPublicKeyUpdate, quorumHashUpdate); err != nil { return err } } else { - s.Set = types.NewValidatorSet(makeVals(update), thresholdPublicKeyUpdate, btcjson.LLMQType_5_60, + vs := types.NewValidatorSet(makeVals(update), thresholdPublicKeyUpdate, btcjson.LLMQType_5_60, quorumHashUpdate, true) + + ps, err := selectproposer.NewProposerSelector(cp, vs, s.height, 0, nil, nil) + if err != nil { + return err + } + if cp.Version.ConsensusVersion == 0 { + //consensus version 0 had an issue where first proposer didn't propose + // TODO: double check if this is really needed + ps.ValidatorSet().IncProposerIndex(1) + } + s.ValidatorProposer = ps } } } } } + if err := s.ValidatorProposer.UpdateHeightRound(s.height, 0); err != nil { + return err + } } return nil }