Skip to content

Commit e8c32b3

Browse files
e2e: Migrate staking rewards test from kurtosis (#1767)
Co-authored-by: Stephen Buttolph <stephen@avalabs.org>
1 parent f87964e commit e8c32b3

File tree

1 file changed

+295
-0
lines changed

1 file changed

+295
-0
lines changed

tests/e2e/p/staking_rewards.go

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

0 commit comments

Comments
 (0)