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 })