Skip to content

Commit 06e915e

Browse files
committed
e2e: Migrate staking rewards e2e from kurtosis
1 parent 62cadd8 commit 06e915e

File tree

4 files changed

+280
-7
lines changed

4 files changed

+280
-7
lines changed

tests/e2e/p/staking_rewards.go

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

tests/fixture/testnet/config.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ const (
4040

4141
// Arbitrarily large amount of AVAX to fund keys on the X-Chain for testing
4242
DefaultFundedKeyXChainAmount = 30 * units.MegaAvax
43+
44+
// A short min stake duration enables testing of staking logic.
45+
DefaultMinStakeDuration = time.Second
4346
)
4447

4548
var (

tests/fixture/testnet/local/config.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ func LocalFlags() testnet.FlagsMap {
3737
config.IndexEnabledKey: true,
3838
config.LogDisplayLevelKey: "INFO",
3939
config.LogLevelKey: "DEBUG",
40+
config.MinStakeDurationKey: testnet.DefaultMinStakeDuration.String(),
4041
}
4142
}
4243

wallet/chain/p/wallet.go

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
package p
55

66
import (
7-
"errors"
7+
"fmt"
88
"time"
99

1010
"github.com/ava-labs/avalanchego/ids"
@@ -17,11 +17,7 @@ import (
1717
"github.com/ava-labs/avalanchego/wallet/subnet/primary/common"
1818
)
1919

20-
var (
21-
errNotCommitted = errors.New("not committed")
22-
23-
_ Wallet = (*wallet)(nil)
24-
)
20+
var _ Wallet = (*wallet)(nil)
2521

2622
type Wallet interface {
2723
Context
@@ -503,7 +499,7 @@ func (w *wallet) IssueTx(
503499
}
504500

505501
if txStatus.Status != status.Committed {
506-
return errNotCommitted
502+
return fmt.Errorf("not committed: %s", txStatus.Reason)
507503
}
508504
return nil
509505
}

0 commit comments

Comments
 (0)