From 502ad63164988c2cf4654f5a12387e25c1d9957d Mon Sep 17 00:00:00 2001 From: Maru Newby Date: Thu, 13 Jul 2023 21:55:59 -0700 Subject: [PATCH] e2e: Migrate staking rewards e2e from kurtosis --- tests/e2e/e2e.go | 18 ++ tests/e2e/p/staking_rewards.go | 267 ++++++++++++++++++++++++++ tests/fixture/testnet/config.go | 3 + tests/fixture/testnet/local/config.go | 1 + wallet/chain/p/wallet.go | 10 +- 5 files changed, 292 insertions(+), 7 deletions(-) create mode 100644 tests/e2e/p/staking_rewards.go diff --git a/tests/e2e/e2e.go b/tests/e2e/e2e.go index ffcdef0ac4fe..bb5e60b9b56d 100644 --- a/tests/e2e/e2e.go +++ b/tests/e2e/e2e.go @@ -141,3 +141,21 @@ func WaitForHealthy(node testnet.Node) { defer cancel() require.NoError(ginkgo.GinkgoT(), node.WaitForHealthy(ctx)) } + +// Re-implementation of testify/require.Eventually that is compatible with ginkgo. testify's +// version calls the condition function with a goroutine and ginkgo assertions don't work +// properly in goroutines. +func Eventually(condition func() bool, waitFor time.Duration, tick time.Duration, msg string) { + ticker := time.NewTicker(tick) + defer ticker.Stop() + + ctx, cancel := context.WithTimeout(context.Background(), waitFor) + defer cancel() + for !condition() { + select { + case <-ctx.Done(): + require.Fail(ginkgo.GinkgoT(), msg) + case <-ticker.C: + } + } +} diff --git a/tests/e2e/p/staking_rewards.go b/tests/e2e/p/staking_rewards.go new file mode 100644 index 000000000000..2988d72129e6 --- /dev/null +++ b/tests/e2e/p/staking_rewards.go @@ -0,0 +1,267 @@ +// Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package p + +import ( + "context" + "math" + "time" + + "github.com/mitchellh/mapstructure" + + ginkgo "github.com/onsi/ginkgo/v2" + + "github.com/spf13/cast" + + "github.com/stretchr/testify/require" + + "github.com/ava-labs/avalanchego/api/admin" + "github.com/ava-labs/avalanchego/api/info" + "github.com/ava-labs/avalanchego/config" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/tests" + "github.com/ava-labs/avalanchego/tests/e2e" + "github.com/ava-labs/avalanchego/tests/fixture/testnet" + "github.com/ava-labs/avalanchego/utils/constants" + "github.com/ava-labs/avalanchego/utils/crypto/secp256k1" + "github.com/ava-labs/avalanchego/utils/units" + "github.com/ava-labs/avalanchego/vms/platformvm" + "github.com/ava-labs/avalanchego/vms/platformvm/reward" + "github.com/ava-labs/avalanchego/vms/platformvm/txs" + "github.com/ava-labs/avalanchego/vms/secp256k1fx" +) + +const ( + validatorStartTimeDiff = 20 * time.Second + delegationPeriod = 15 * time.Second + validationPeriod = 30 * time.Second +) + +var _ = ginkgo.Describe("[Staking Rewards]", func() { + require := require.New(ginkgo.GinkgoT()) + + ginkgo.It("should ensure that validator node uptime determines whether a staking reward is issued", func() { + network := e2e.Env.GetNetwork() + + ginkgo.By("checking that the network has a compatible minimum stake duration", func() { + minStakeDuration := cast.ToDuration(network.GetConfig().DefaultFlags[config.MinStakeDurationKey]) + require.Equal(testnet.DefaultMinStakeDuration, minStakeDuration) + }) + ginkgo.By("adding alpha node, whose uptime should result in a staking reward") + alphaNode := e2e.AddEphemeralNode(network, testnet.FlagsMap{}) + ginkgo.By("adding beta node, whose uptime should not result in a staking reward") + betaNode := e2e.AddEphemeralNode(network, testnet.FlagsMap{}) + + // Wait to check health until both nodes have started to minimize the duration + // required for both nodes to report healthy. + ginkgo.By("waiting until alpha node is healthy") + e2e.WaitForHealthy(alphaNode) + ginkgo.By("waiting until beta node is healthy") + e2e.WaitForHealthy(betaNode) + + ginkgo.By("generating reward keys") + factory := secp256k1.Factory{} + alphaValidationRewardKey, err := factory.NewPrivateKey() + require.NoError(err) + alphaDelegationRewardKey, err := factory.NewPrivateKey() + require.NoError(err) + betaValidationRewardKey, err := factory.NewPrivateKey() + require.NoError(err) + betaDelegationRewardKey, err := factory.NewPrivateKey() + require.NoError(err) + gammaDelegationRewardKey, err := factory.NewPrivateKey() + require.NoError(err) + deltaDelegationRewardKey, err := factory.NewPrivateKey() + require.NoError(err) + rewardKeys := []*secp256k1.PrivateKey{ + alphaValidationRewardKey, + alphaDelegationRewardKey, + betaValidationRewardKey, + betaDelegationRewardKey, + gammaDelegationRewardKey, + deltaDelegationRewardKey, + } + + ginkgo.By("creating keychain and P-Chain wallet") + keychain := secp256k1fx.NewKeychain(rewardKeys...) + fundedKey := e2e.Env.AllocateFundedKey() + keychain.Add(fundedKey) + baseWallet := e2e.Env.NewWallet(keychain) + pWallet := baseWallet.P() + + ginkgo.By("retrieving alpha node id and pop") + alphaInfoClient := info.NewClient(alphaNode.GetProcessContext().URI) + alphaNodeID, alphaPOP, err := alphaInfoClient.GetNodeID(context.Background()) + require.NoError(err) + ginkgo.By("retrieving beta node id and pop") + betaInfoClient := info.NewClient(betaNode.GetProcessContext().URI) + betaNodeID, betaPOP, err := betaInfoClient.GetNodeID(context.Background()) + require.NoError(err) + + delegationPercent := 0.10 // 10% + delegationFee := uint32(reward.PercentDenominator * delegationPercent) + weight := 2_000 * units.Avax + + alphaValidatorStartTime := time.Now().Add(validatorStartTimeDiff) + alphaValidatorEndTime := alphaValidatorStartTime.Add(validationPeriod) + tests.Outf("alpha node validation period starting at: %v\n", alphaValidatorStartTime) + + ginkgo.By("adding alpha node as a validator") + _, err = pWallet.IssueAddPermissionlessValidatorTx( + &txs.SubnetValidator{Validator: txs.Validator{ + NodeID: alphaNodeID, + Start: uint64(alphaValidatorStartTime.Unix()), + End: uint64(alphaValidatorEndTime.Unix()), + Wght: weight, + }}, + alphaPOP, + pWallet.AVAXAssetID(), + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{alphaValidationRewardKey.Address()}, + }, + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{alphaDelegationRewardKey.Address()}, + }, + delegationFee, + ) + require.NoError(err) + + betaValidatorStartTime := time.Now().Add(validatorStartTimeDiff) + betaValidatorEndTime := betaValidatorStartTime.Add(validationPeriod) + tests.Outf("beta node validation period starting at: %v\n", betaValidatorStartTime) + + ginkgo.By("adding beta node as a validator") + _, err = pWallet.IssueAddPermissionlessValidatorTx( + &txs.SubnetValidator{Validator: txs.Validator{ + NodeID: betaNodeID, + Start: uint64(betaValidatorStartTime.Unix()), + End: uint64(betaValidatorEndTime.Unix()), + Wght: weight, + }}, + betaPOP, + pWallet.AVAXAssetID(), + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{betaValidationRewardKey.Address()}, + }, + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{betaDelegationRewardKey.Address()}, + }, + delegationFee, + ) + require.NoError(err) + + gammaDelegatorStartTime := time.Now().Add(validatorStartTimeDiff) + tests.Outf("gamma delegation period starting at: %v\n", gammaDelegatorStartTime) + + ginkgo.By("adding gamma as delegator to the alpha node") + _, err = pWallet.IssueAddPermissionlessDelegatorTx( + &txs.SubnetValidator{Validator: txs.Validator{ + NodeID: alphaNodeID, + Start: uint64(gammaDelegatorStartTime.Unix()), + End: uint64(gammaDelegatorStartTime.Add(delegationPeriod).Unix()), + Wght: weight, + }}, + pWallet.AVAXAssetID(), + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{gammaDelegationRewardKey.Address()}, + }, + ) + require.NoError(err) + + deltaDelegatorStartTime := time.Now().Add(validatorStartTimeDiff) + tests.Outf("delta delegation period starting at: %v\n", deltaDelegatorStartTime) + + ginkgo.By("adding delta as delegator to the beta node") + _, err = pWallet.IssueAddPermissionlessDelegatorTx( + &txs.SubnetValidator{Validator: txs.Validator{ + NodeID: betaNodeID, + Start: uint64(deltaDelegatorStartTime.Unix()), + End: uint64(deltaDelegatorStartTime.Add(delegationPeriod).Unix()), + Wght: weight, + }}, + pWallet.AVAXAssetID(), + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{deltaDelegationRewardKey.Address()}, + }, + ) + require.NoError(err) + + ginkgo.By("stopping beta node to prevent it and its delegator from receiving a validation reward") + require.NoError(betaNode.Stop()) + + ginkgo.By("waiting until all validation periods are over") + // The beta validator was the last added and so has the latest end time. The + // delegation periods are shorter than the validation periods. + time.Sleep(time.Until(betaValidatorEndTime)) + + pvmClient := platformvm.NewClient(alphaNode.GetProcessContext().URI) + + ginkgo.By("waiting until the alpha and beta nodes are no longer validators") + e2e.Eventually(func() bool { + validators, err := pvmClient.GetCurrentValidators(context.Background(), ids.Empty, nil) + require.NoError(err) + for _, validator := range validators { + if validator.NodeID == alphaNodeID || validator.NodeID == betaNodeID { + return false + } + } + return true + }, e2e.DefaultTimeout, e2e.DefaultPollingInterval, "nodes failed to stop validating before timeout ") + + ginkgo.By("retrieving reward configuration for the network") + // TODO(marun) Enable GetConfig to return *node.Config + // directly. Currently, due to a circular dependency issue, a + // map-based equivalent is used for which manual unmarshaling + // is required. + adminClient := admin.NewClient(e2e.Env.GetRandomNodeURI()) + rawNodeConfigMap, err := adminClient.GetConfig(context.Background()) + require.NoError(err) + nodeConfigMap, ok := rawNodeConfigMap.(map[string]interface{}) + require.True(ok) + stakingConfigMap, ok := nodeConfigMap["stakingConfig"].(map[string]interface{}) + require.True(ok) + rawRewardConfig := stakingConfigMap["rewardConfig"] + rewardConfig := reward.Config{} + require.NoError(mapstructure.Decode(rawRewardConfig, &rewardConfig)) + + ginkgo.By("retrieving reward address balances") + rewardBalances := make(map[ids.ShortID]uint64, len(rewardKeys)) + for _, rewardKey := range rewardKeys { + keychain := secp256k1fx.NewKeychain(rewardKey) + baseWallet := e2e.Env.NewWallet(keychain) + pWallet := baseWallet.P() + balances, err := pWallet.Builder().GetBalance() + require.NoError(err) + rewardBalances[rewardKey.Address()] = balances[pWallet.AVAXAssetID()] + } + require.Len(rewardBalances, len(rewardKeys)) + + ginkgo.By("determining expected validation and delegation rewards") + currentSupply, err := pvmClient.GetCurrentSupply(context.Background(), constants.PrimaryNetworkID) + require.NoError(err) + calculator := reward.NewCalculator(rewardConfig) + expectedValidationReward := calculator.Calculate(validationPeriod, weight, currentSupply) + expectedDelegationReward := calculator.Calculate(delegationPeriod, weight, currentSupply) + expectedDelegationFee := uint64(math.Round(float64(expectedDelegationReward) * delegationPercent)) + + ginkgo.By("checking expected rewards against actual rewards") + expectedRewardBalances := map[ids.ShortID]uint64{ + alphaValidationRewardKey.Address(): expectedValidationReward, + alphaDelegationRewardKey.Address(): expectedDelegationFee, + betaValidationRewardKey.Address(): 0, // Validator didn't meet uptime requirement + betaDelegationRewardKey.Address(): 0, // Validator didn't meet uptime requirement + gammaDelegationRewardKey.Address(): expectedDelegationReward - expectedDelegationFee, + deltaDelegationRewardKey.Address(): 0, // Validator didn't meet uptime requirement + } + for address := range expectedRewardBalances { + require.Equal(expectedRewardBalances[address], rewardBalances[address]) + } + }) +}) diff --git a/tests/fixture/testnet/config.go b/tests/fixture/testnet/config.go index 2c08b5dc9db4..8f66622194ac 100644 --- a/tests/fixture/testnet/config.go +++ b/tests/fixture/testnet/config.go @@ -30,6 +30,9 @@ import ( const ( DefaultNodeCount = 5 DefaultFundedKeyCount = 50 + + // A short min stake duration enables testing of staking logic. + DefaultMinStakeDuration = time.Second ) var ( diff --git a/tests/fixture/testnet/local/config.go b/tests/fixture/testnet/local/config.go index 8da3e3564ab2..5cb7079f307c 100644 --- a/tests/fixture/testnet/local/config.go +++ b/tests/fixture/testnet/local/config.go @@ -38,6 +38,7 @@ func LocalFlags() testnet.FlagsMap { config.IndexEnabledKey: true, config.LogDisplayLevelKey: "INFO", config.LogLevelKey: "DEBUG", + config.MinStakeDurationKey: testnet.DefaultMinStakeDuration, } } diff --git a/wallet/chain/p/wallet.go b/wallet/chain/p/wallet.go index ec180cc90963..eaa128059ef1 100644 --- a/wallet/chain/p/wallet.go +++ b/wallet/chain/p/wallet.go @@ -4,7 +4,7 @@ package p import ( - "errors" + "fmt" "time" "github.com/ava-labs/avalanchego/ids" @@ -17,11 +17,7 @@ import ( "github.com/ava-labs/avalanchego/wallet/subnet/primary/common" ) -var ( - errNotCommitted = errors.New("not committed") - - _ Wallet = (*wallet)(nil) -) +var _ Wallet = (*wallet)(nil) type Wallet interface { Context @@ -503,7 +499,7 @@ func (w *wallet) IssueTx( } if txStatus.Status != status.Committed { - return errNotCommitted + return fmt.Errorf("not committed: %s", txStatus.Reason) } return nil }