github.com/MetalBlockchain/metalgo@v1.11.9/tests/e2e/p/staking_rewards.go (about)

     1  // Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved.
     2  // See the file LICENSE for licensing terms.
     3  
     4  package p
     5  
     6  import (
     7  	"time"
     8  
     9  	"github.com/mitchellh/mapstructure"
    10  	"github.com/spf13/cast"
    11  	"github.com/stretchr/testify/require"
    12  
    13  	"github.com/MetalBlockchain/metalgo/api/admin"
    14  	"github.com/MetalBlockchain/metalgo/api/info"
    15  	"github.com/MetalBlockchain/metalgo/config"
    16  	"github.com/MetalBlockchain/metalgo/ids"
    17  	"github.com/MetalBlockchain/metalgo/tests"
    18  	"github.com/MetalBlockchain/metalgo/tests/fixture/e2e"
    19  	"github.com/MetalBlockchain/metalgo/tests/fixture/tmpnet"
    20  	"github.com/MetalBlockchain/metalgo/utils/constants"
    21  	"github.com/MetalBlockchain/metalgo/utils/crypto/secp256k1"
    22  	"github.com/MetalBlockchain/metalgo/utils/units"
    23  	"github.com/MetalBlockchain/metalgo/vms/platformvm"
    24  	"github.com/MetalBlockchain/metalgo/vms/platformvm/reward"
    25  	"github.com/MetalBlockchain/metalgo/vms/platformvm/txs"
    26  	"github.com/MetalBlockchain/metalgo/vms/secp256k1fx"
    27  
    28  	ginkgo "github.com/onsi/ginkgo/v2"
    29  )
    30  
    31  const (
    32  	targetDelegationPeriod = 15 * time.Second
    33  	targetValidationPeriod = 30 * time.Second
    34  )
    35  
    36  var _ = ginkgo.Describe("[Staking Rewards]", func() {
    37  	require := require.New(ginkgo.GinkgoT())
    38  
    39  	ginkgo.It("should ensure that validator node uptime determines whether a staking reward is issued", func() {
    40  		network := e2e.Env.GetNetwork()
    41  
    42  		ginkgo.By("checking that the network has a compatible minimum stake duration", func() {
    43  			minStakeDuration := cast.ToDuration(network.DefaultFlags[config.MinStakeDurationKey])
    44  			require.Equal(tmpnet.DefaultMinStakeDuration, minStakeDuration)
    45  		})
    46  
    47  		ginkgo.By("adding alpha node, whose uptime should result in a staking reward")
    48  		alphaNode := e2e.AddEphemeralNode(network, tmpnet.FlagsMap{})
    49  		ginkgo.By("adding beta node, whose uptime should not result in a staking reward")
    50  		betaNode := e2e.AddEphemeralNode(network, tmpnet.FlagsMap{})
    51  
    52  		// Wait to check health until both nodes have started to minimize the duration
    53  		// required for both nodes to report healthy.
    54  		ginkgo.By("waiting until alpha node is healthy")
    55  		e2e.WaitForHealthy(alphaNode)
    56  		ginkgo.By("waiting until beta node is healthy")
    57  		e2e.WaitForHealthy(betaNode)
    58  
    59  		ginkgo.By("retrieving alpha node id and pop")
    60  		alphaInfoClient := info.NewClient(alphaNode.URI)
    61  		alphaNodeID, alphaPOP, err := alphaInfoClient.GetNodeID(e2e.DefaultContext())
    62  		require.NoError(err)
    63  
    64  		ginkgo.By("retrieving beta node id and pop")
    65  		betaInfoClient := info.NewClient(betaNode.URI)
    66  		betaNodeID, betaPOP, err := betaInfoClient.GetNodeID(e2e.DefaultContext())
    67  		require.NoError(err)
    68  
    69  		ginkgo.By("generating reward keys")
    70  
    71  		alphaValidationRewardKey, err := secp256k1.NewPrivateKey()
    72  		require.NoError(err)
    73  		alphaDelegationRewardKey, err := secp256k1.NewPrivateKey()
    74  		require.NoError(err)
    75  
    76  		betaValidationRewardKey, err := secp256k1.NewPrivateKey()
    77  		require.NoError(err)
    78  		betaDelegationRewardKey, err := secp256k1.NewPrivateKey()
    79  		require.NoError(err)
    80  
    81  		gammaDelegationRewardKey, err := secp256k1.NewPrivateKey()
    82  		require.NoError(err)
    83  
    84  		deltaDelegationRewardKey, err := secp256k1.NewPrivateKey()
    85  		require.NoError(err)
    86  
    87  		rewardKeys := []*secp256k1.PrivateKey{
    88  			alphaValidationRewardKey,
    89  			alphaDelegationRewardKey,
    90  			betaValidationRewardKey,
    91  			betaDelegationRewardKey,
    92  			gammaDelegationRewardKey,
    93  			deltaDelegationRewardKey,
    94  		}
    95  
    96  		ginkgo.By("creating keychain and P-Chain wallet")
    97  		keychain := secp256k1fx.NewKeychain(rewardKeys...)
    98  		fundedKey := e2e.Env.AllocatePreFundedKey()
    99  		keychain.Add(fundedKey)
   100  		nodeURI := tmpnet.NodeURI{
   101  			NodeID: alphaNodeID,
   102  			URI:    alphaNode.URI,
   103  		}
   104  		baseWallet := e2e.NewWallet(keychain, nodeURI)
   105  		pWallet := baseWallet.P()
   106  
   107  		pBuilder := pWallet.Builder()
   108  		pContext := pBuilder.Context()
   109  
   110  		const (
   111  			delegationPercent = 0.10 // 10%
   112  			delegationShare   = reward.PercentDenominator * delegationPercent
   113  			weight            = 2_000 * units.Avax
   114  		)
   115  
   116  		pvmClient := platformvm.NewClient(alphaNode.URI)
   117  
   118  		ginkgo.By("retrieving supply before inserting validators")
   119  		supplyAtValidatorsStart, _, err := pvmClient.GetCurrentSupply(e2e.DefaultContext(), constants.PrimaryNetworkID)
   120  		require.NoError(err)
   121  
   122  		alphaValidatorsEndTime := time.Now().Add(targetValidationPeriod)
   123  		tests.Outf("alpha node validation period ending at: %v\n", alphaValidatorsEndTime)
   124  
   125  		ginkgo.By("adding alpha node as a validator", func() {
   126  			_, err := pWallet.IssueAddPermissionlessValidatorTx(
   127  				&txs.SubnetValidator{
   128  					Validator: txs.Validator{
   129  						NodeID: alphaNodeID,
   130  						End:    uint64(alphaValidatorsEndTime.Unix()),
   131  						Wght:   weight,
   132  					},
   133  					Subnet: constants.PrimaryNetworkID,
   134  				},
   135  				alphaPOP,
   136  				pContext.AVAXAssetID,
   137  				&secp256k1fx.OutputOwners{
   138  					Threshold: 1,
   139  					Addrs:     []ids.ShortID{alphaValidationRewardKey.Address()},
   140  				},
   141  				&secp256k1fx.OutputOwners{
   142  					Threshold: 1,
   143  					Addrs:     []ids.ShortID{alphaDelegationRewardKey.Address()},
   144  				},
   145  				delegationShare,
   146  				e2e.WithDefaultContext(),
   147  			)
   148  			require.NoError(err)
   149  		})
   150  
   151  		betaValidatorEndTime := time.Now().Add(targetValidationPeriod)
   152  		tests.Outf("beta node validation period ending at: %v\n", betaValidatorEndTime)
   153  
   154  		ginkgo.By("adding beta node as a validator", func() {
   155  			_, err := pWallet.IssueAddPermissionlessValidatorTx(
   156  				&txs.SubnetValidator{
   157  					Validator: txs.Validator{
   158  						NodeID: betaNodeID,
   159  						End:    uint64(betaValidatorEndTime.Unix()),
   160  						Wght:   weight,
   161  					},
   162  					Subnet: constants.PrimaryNetworkID,
   163  				},
   164  				betaPOP,
   165  				pContext.AVAXAssetID,
   166  				&secp256k1fx.OutputOwners{
   167  					Threshold: 1,
   168  					Addrs:     []ids.ShortID{betaValidationRewardKey.Address()},
   169  				},
   170  				&secp256k1fx.OutputOwners{
   171  					Threshold: 1,
   172  					Addrs:     []ids.ShortID{betaDelegationRewardKey.Address()},
   173  				},
   174  				delegationShare,
   175  				e2e.WithDefaultContext(),
   176  			)
   177  			require.NoError(err)
   178  		})
   179  
   180  		ginkgo.By("retrieving supply before inserting delegators")
   181  		supplyAtDelegatorsStart, _, err := pvmClient.GetCurrentSupply(e2e.DefaultContext(), constants.PrimaryNetworkID)
   182  		require.NoError(err)
   183  
   184  		gammaDelegatorEndTime := time.Now().Add(targetDelegationPeriod)
   185  		tests.Outf("gamma delegation period ending at: %v\n", gammaDelegatorEndTime)
   186  
   187  		ginkgo.By("adding gamma as delegator to the alpha node", func() {
   188  			_, err := pWallet.IssueAddPermissionlessDelegatorTx(
   189  				&txs.SubnetValidator{
   190  					Validator: txs.Validator{
   191  						NodeID: alphaNodeID,
   192  						End:    uint64(gammaDelegatorEndTime.Unix()),
   193  						Wght:   weight,
   194  					},
   195  					Subnet: constants.PrimaryNetworkID,
   196  				},
   197  				pContext.AVAXAssetID,
   198  				&secp256k1fx.OutputOwners{
   199  					Threshold: 1,
   200  					Addrs:     []ids.ShortID{gammaDelegationRewardKey.Address()},
   201  				},
   202  				e2e.WithDefaultContext(),
   203  			)
   204  			require.NoError(err)
   205  		})
   206  
   207  		deltaDelegatorEndTime := time.Now().Add(targetDelegationPeriod)
   208  		tests.Outf("delta delegation period ending at: %v\n", deltaDelegatorEndTime)
   209  
   210  		ginkgo.By("adding delta as delegator to the beta node", func() {
   211  			_, err := pWallet.IssueAddPermissionlessDelegatorTx(
   212  				&txs.SubnetValidator{
   213  					Validator: txs.Validator{
   214  						NodeID: betaNodeID,
   215  						End:    uint64(deltaDelegatorEndTime.Unix()),
   216  						Wght:   weight,
   217  					},
   218  					Subnet: constants.PrimaryNetworkID,
   219  				},
   220  				pContext.AVAXAssetID,
   221  				&secp256k1fx.OutputOwners{
   222  					Threshold: 1,
   223  					Addrs:     []ids.ShortID{deltaDelegationRewardKey.Address()},
   224  				},
   225  				e2e.WithDefaultContext(),
   226  			)
   227  			require.NoError(err)
   228  		})
   229  
   230  		ginkgo.By("stopping beta node to prevent it and its delegator from receiving a validation reward")
   231  		require.NoError(betaNode.Stop(e2e.DefaultContext()))
   232  
   233  		ginkgo.By("retrieving staking periods from the chain")
   234  		data, err := pvmClient.GetCurrentValidators(e2e.DefaultContext(), constants.PlatformChainID, []ids.NodeID{alphaNodeID})
   235  		require.NoError(err)
   236  		require.Len(data, 1)
   237  		actualAlphaValidationPeriod := time.Duration(data[0].EndTime-data[0].StartTime) * time.Second
   238  		delegatorData := data[0].Delegators[0]
   239  		actualGammaDelegationPeriod := time.Duration(delegatorData.EndTime-delegatorData.StartTime) * time.Second
   240  
   241  		ginkgo.By("waiting until all validation periods are over")
   242  		// The beta validator was the last added and so has the latest end time. The
   243  		// delegation periods are shorter than the validation periods.
   244  		time.Sleep(time.Until(betaValidatorEndTime))
   245  
   246  		ginkgo.By("waiting until the alpha and beta nodes are no longer validators")
   247  		e2e.Eventually(func() bool {
   248  			validators, err := pvmClient.GetCurrentValidators(e2e.DefaultContext(), constants.PrimaryNetworkID, nil)
   249  			require.NoError(err)
   250  			for _, validator := range validators {
   251  				if validator.NodeID == alphaNodeID || validator.NodeID == betaNodeID {
   252  					return false
   253  				}
   254  			}
   255  			return true
   256  		}, e2e.DefaultTimeout, e2e.DefaultPollingInterval, "nodes failed to stop validating before timeout ")
   257  
   258  		ginkgo.By("retrieving reward configuration for the network")
   259  		// TODO(marun) Enable GetConfig to return *node.Config
   260  		// directly. Currently, due to a circular dependency issue, a
   261  		// map-based equivalent is used for which manual unmarshaling
   262  		// is required.
   263  		adminClient := admin.NewClient(e2e.Env.GetRandomNodeURI().URI)
   264  		rawNodeConfigMap, err := adminClient.GetConfig(e2e.DefaultContext())
   265  		require.NoError(err)
   266  		nodeConfigMap, ok := rawNodeConfigMap.(map[string]interface{})
   267  		require.True(ok)
   268  		stakingConfigMap, ok := nodeConfigMap["stakingConfig"].(map[string]interface{})
   269  		require.True(ok)
   270  		rawRewardConfig := stakingConfigMap["rewardConfig"]
   271  		rewardConfig := reward.Config{}
   272  		require.NoError(mapstructure.Decode(rawRewardConfig, &rewardConfig))
   273  
   274  		ginkgo.By("retrieving reward address balances")
   275  		rewardBalances := make(map[ids.ShortID]uint64, len(rewardKeys))
   276  		for _, rewardKey := range rewardKeys {
   277  			keychain := secp256k1fx.NewKeychain(rewardKey)
   278  			baseWallet := e2e.NewWallet(keychain, nodeURI)
   279  			pWallet := baseWallet.P()
   280  			balances, err := pWallet.Builder().GetBalance()
   281  			require.NoError(err)
   282  			rewardBalances[rewardKey.Address()] = balances[pContext.AVAXAssetID]
   283  		}
   284  		require.Len(rewardBalances, len(rewardKeys))
   285  
   286  		ginkgo.By("determining expected validation and delegation rewards")
   287  		calculator := reward.NewCalculator(rewardConfig)
   288  		expectedValidationReward := calculator.Calculate(actualAlphaValidationPeriod, weight, supplyAtValidatorsStart)
   289  		potentialDelegationReward := calculator.Calculate(actualGammaDelegationPeriod, weight, supplyAtDelegatorsStart)
   290  		expectedDelegationFee, expectedDelegatorReward := reward.Split(potentialDelegationReward, delegationShare)
   291  
   292  		ginkgo.By("checking expected rewards against actual rewards")
   293  		expectedRewardBalances := map[ids.ShortID]uint64{
   294  			alphaValidationRewardKey.Address(): expectedValidationReward,
   295  			alphaDelegationRewardKey.Address(): expectedDelegationFee,
   296  			betaValidationRewardKey.Address():  0, // Validator didn't meet uptime requirement
   297  			betaDelegationRewardKey.Address():  0, // Validator didn't meet uptime requirement
   298  			gammaDelegationRewardKey.Address(): expectedDelegatorReward,
   299  			deltaDelegationRewardKey.Address(): 0, // Validator didn't meet uptime requirement
   300  		}
   301  		for address := range expectedRewardBalances {
   302  			require.Equal(expectedRewardBalances[address], rewardBalances[address])
   303  		}
   304  
   305  		ginkgo.By("stopping alpha to free up resources for a bootstrap check")
   306  		require.NoError(alphaNode.Stop(e2e.DefaultContext()))
   307  
   308  		e2e.CheckBootstrapIsPossible(network)
   309  	})
   310  })