Skip to content

Commit 5786184

Browse files
committed
e2e: Migrate staking rewards e2e from kurtosis
1 parent b1c94ab commit 5786184

File tree

4 files changed

+287
-7
lines changed

4 files changed

+287
-7
lines changed

tests/e2e/e2e.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,3 +142,21 @@ func WaitForHealthy(node testnet.Node) {
142142
defer cancel()
143143
require.NoError(ginkgo.GinkgoT(), node.WaitForHealthy(ctx))
144144
}
145+
146+
// Re-implementation of testify/require.Eventually that is compatible with ginkgo. testify's
147+
// version calls the condition function with a goroutine and ginkgo assertions don't work
148+
// properly in goroutines.
149+
func Eventually(condition func() bool, waitFor time.Duration, tick time.Duration, msg string) {
150+
ticker := time.NewTicker(tick)
151+
defer ticker.Stop()
152+
153+
ctx, cancel := context.WithTimeout(context.Background(), waitFor)
154+
defer cancel()
155+
for !condition() {
156+
select {
157+
case <-ctx.Done():
158+
require.Fail(ginkgo.GinkgoT(), msg)
159+
case <-ticker.C:
160+
}
161+
}
162+
}

tests/e2e/p/staking_rewards.go

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

tests/fixture/testnet/local/config.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ func LocalFlags() testnet.FlagsMap {
3838
config.IndexEnabledKey: true,
3939
config.LogDisplayLevelKey: "INFO",
4040
config.LogLevelKey: "DEBUG",
41+
42+
// A short min stake duration enables testing of staking logic.
43+
config.MinStakeDurationKey: "1s",
4144
}
4245
}
4346

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)