Skip to content

Commit b73bdf0

Browse files
committed
e2e: Migrate staking rewards e2e from kurtosis
1 parent 0cf3d29 commit b73bdf0

File tree

5 files changed

+308
-7
lines changed

5 files changed

+308
-7
lines changed

tests/e2e/e2e.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"fmt"
1212
"math/big"
1313
"math/rand"
14+
"os"
1415
"strings"
1516
"time"
1617

@@ -53,6 +54,11 @@ const (
5354
// Interval appropriate for network operations that should be
5455
// retried periodically but not too often.
5556
DefaultPollingInterval = 500 * time.Millisecond
57+
58+
// Setting this env will disable post-test bootstrap
59+
// checks. Useful for speeding up iteration during test
60+
// development.
61+
SkipBootstrapChecksEnvName = "E2E_SKIP_BOOTSTRAP_CHECKS"
5662
)
5763

5864
// Env is used to access shared test fixture. Intended to be
@@ -247,3 +253,14 @@ func WithSuggestedGasPrice(ethClient ethclient.Client) common.Option {
247253
baseFee := SuggestGasPrice(ethClient)
248254
return common.WithBaseFee(baseFee)
249255
}
256+
257+
// Verifies that the state of the network is compatible with bootstrapping a new node.
258+
func CheckBootstrapIsPossible(network testnet.Network) {
259+
if len(os.Getenv(SkipBootstrapChecksEnvName)) > 0 {
260+
tests.Outf("{{yellow}}Skipping bootstrap check due to the %s env var being set", SkipBootstrapChecksEnvName)
261+
return
262+
}
263+
ginkgo.By("checking if bootstrap is possible with the current network state")
264+
node := AddEphemeralNode(network, testnet.FlagsMap{})
265+
WaitForHealthy(node)
266+
}

tests/e2e/p/staking_rewards.go

Lines changed: 284 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,284 @@
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+
nodeURI := e2e.Env.GetRandomNodeURI()
96+
baseWallet := e2e.Env.NewWallet(keychain, nodeURI)
97+
pWallet := baseWallet.P()
98+
99+
ginkgo.By("retrieving alpha node id and pop")
100+
alphaInfoClient := info.NewClient(alphaNode.GetProcessContext().URI)
101+
alphaNodeID, alphaPOP, err := alphaInfoClient.GetNodeID(e2e.DefaultContext())
102+
require.NoError(err)
103+
104+
ginkgo.By("retrieving beta node id and pop")
105+
betaInfoClient := info.NewClient(betaNode.GetProcessContext().URI)
106+
betaNodeID, betaPOP, err := betaInfoClient.GetNodeID(e2e.DefaultContext())
107+
require.NoError(err)
108+
109+
delegationPercent := 0.10 // 10%
110+
delegationFee := uint32(reward.PercentDenominator * delegationPercent)
111+
weight := 2_000 * units.Avax
112+
113+
alphaValidatorStartTime := time.Now().Add(validatorStartTimeDiff)
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{Validator: txs.Validator{
120+
NodeID: alphaNodeID,
121+
Start: uint64(alphaValidatorStartTime.Unix()),
122+
End: uint64(alphaValidatorEndTime.Unix()),
123+
Wght: weight,
124+
}},
125+
alphaPOP,
126+
pWallet.AVAXAssetID(),
127+
&secp256k1fx.OutputOwners{
128+
Threshold: 1,
129+
Addrs: []ids.ShortID{alphaValidationRewardKey.Address()},
130+
},
131+
&secp256k1fx.OutputOwners{
132+
Threshold: 1,
133+
Addrs: []ids.ShortID{alphaDelegationRewardKey.Address()},
134+
},
135+
delegationFee,
136+
)
137+
require.NoError(err)
138+
})
139+
140+
betaValidatorStartTime := time.Now().Add(validatorStartTimeDiff)
141+
betaValidatorEndTime := betaValidatorStartTime.Add(validationPeriod)
142+
tests.Outf("beta node validation period starting at: %v\n", betaValidatorStartTime)
143+
144+
ginkgo.By("adding beta node as a validator", func() {
145+
_, err := pWallet.IssueAddPermissionlessValidatorTx(
146+
&txs.SubnetValidator{Validator: txs.Validator{
147+
NodeID: betaNodeID,
148+
Start: uint64(betaValidatorStartTime.Unix()),
149+
End: uint64(betaValidatorEndTime.Unix()),
150+
Wght: weight,
151+
}},
152+
betaPOP,
153+
pWallet.AVAXAssetID(),
154+
&secp256k1fx.OutputOwners{
155+
Threshold: 1,
156+
Addrs: []ids.ShortID{betaValidationRewardKey.Address()},
157+
},
158+
&secp256k1fx.OutputOwners{
159+
Threshold: 1,
160+
Addrs: []ids.ShortID{betaDelegationRewardKey.Address()},
161+
},
162+
delegationFee,
163+
)
164+
require.NoError(err)
165+
})
166+
167+
gammaDelegatorStartTime := time.Now().Add(validatorStartTimeDiff)
168+
tests.Outf("gamma delegation period starting at: %v\n", gammaDelegatorStartTime)
169+
170+
ginkgo.By("adding gamma as delegator to the alpha node", func() {
171+
_, err := pWallet.IssueAddPermissionlessDelegatorTx(
172+
&txs.SubnetValidator{Validator: txs.Validator{
173+
NodeID: alphaNodeID,
174+
Start: uint64(gammaDelegatorStartTime.Unix()),
175+
End: uint64(gammaDelegatorStartTime.Add(delegationPeriod).Unix()),
176+
Wght: weight,
177+
}},
178+
pWallet.AVAXAssetID(),
179+
&secp256k1fx.OutputOwners{
180+
Threshold: 1,
181+
Addrs: []ids.ShortID{gammaDelegationRewardKey.Address()},
182+
},
183+
)
184+
require.NoError(err)
185+
})
186+
187+
deltaDelegatorStartTime := time.Now().Add(validatorStartTimeDiff)
188+
tests.Outf("delta delegation period starting at: %v\n", deltaDelegatorStartTime)
189+
190+
ginkgo.By("adding delta as delegator to the beta node", func() {
191+
_, err := pWallet.IssueAddPermissionlessDelegatorTx(
192+
&txs.SubnetValidator{Validator: txs.Validator{
193+
NodeID: betaNodeID,
194+
Start: uint64(deltaDelegatorStartTime.Unix()),
195+
End: uint64(deltaDelegatorStartTime.Add(delegationPeriod).Unix()),
196+
Wght: weight,
197+
}},
198+
pWallet.AVAXAssetID(),
199+
&secp256k1fx.OutputOwners{
200+
Threshold: 1,
201+
Addrs: []ids.ShortID{deltaDelegationRewardKey.Address()},
202+
},
203+
)
204+
require.NoError(err)
205+
})
206+
207+
ginkgo.By("stopping beta node to prevent it and its delegator from receiving a validation reward")
208+
require.NoError(betaNode.Stop())
209+
210+
ginkgo.By("waiting until all validation periods are over")
211+
// The beta validator was the last added and so has the latest end time. The
212+
// delegation periods are shorter than the validation periods.
213+
time.Sleep(time.Until(betaValidatorEndTime))
214+
215+
pvmClient := platformvm.NewClient(alphaNode.GetProcessContext().URI)
216+
217+
ginkgo.By("waiting until the alpha and beta nodes are no longer validators")
218+
e2e.Eventually(func() bool {
219+
validators, err := pvmClient.GetCurrentValidators(e2e.DefaultContext(), ids.Empty, nil)
220+
require.NoError(err)
221+
for _, validator := range validators {
222+
if validator.NodeID == alphaNodeID || validator.NodeID == betaNodeID {
223+
return false
224+
}
225+
}
226+
return true
227+
}, e2e.DefaultTimeout, e2e.DefaultPollingInterval, "nodes failed to stop validating before timeout ")
228+
229+
ginkgo.By("retrieving reward configuration for the network")
230+
// TODO(marun) Enable GetConfig to return *node.Config
231+
// directly. Currently, due to a circular dependency issue, a
232+
// map-based equivalent is used for which manual unmarshaling
233+
// is required.
234+
adminClient := admin.NewClient(e2e.Env.GetRandomNodeURI().URI)
235+
rawNodeConfigMap, err := adminClient.GetConfig(e2e.DefaultContext())
236+
require.NoError(err)
237+
nodeConfigMap, ok := rawNodeConfigMap.(map[string]interface{})
238+
require.True(ok)
239+
stakingConfigMap, ok := nodeConfigMap["stakingConfig"].(map[string]interface{})
240+
require.True(ok)
241+
rawRewardConfig := stakingConfigMap["rewardConfig"]
242+
rewardConfig := reward.Config{}
243+
require.NoError(mapstructure.Decode(rawRewardConfig, &rewardConfig))
244+
245+
ginkgo.By("retrieving reward address balances")
246+
rewardBalances := make(map[ids.ShortID]uint64, len(rewardKeys))
247+
for _, rewardKey := range rewardKeys {
248+
keychain := secp256k1fx.NewKeychain(rewardKey)
249+
baseWallet := e2e.Env.NewWallet(keychain, nodeURI)
250+
pWallet := baseWallet.P()
251+
balances, err := pWallet.Builder().GetBalance()
252+
require.NoError(err)
253+
rewardBalances[rewardKey.Address()] = balances[pWallet.AVAXAssetID()]
254+
}
255+
require.Len(rewardBalances, len(rewardKeys))
256+
257+
ginkgo.By("determining expected validation and delegation rewards")
258+
currentSupply, err := pvmClient.GetCurrentSupply(e2e.DefaultContext(), constants.PrimaryNetworkID)
259+
require.NoError(err)
260+
calculator := reward.NewCalculator(rewardConfig)
261+
expectedValidationReward := calculator.Calculate(validationPeriod, weight, currentSupply)
262+
expectedDelegationReward := calculator.Calculate(delegationPeriod, weight, currentSupply)
263+
expectedDelegationFee := uint64(math.Round(float64(expectedDelegationReward) * delegationPercent))
264+
265+
ginkgo.By("checking expected rewards against actual rewards")
266+
expectedRewardBalances := map[ids.ShortID]uint64{
267+
alphaValidationRewardKey.Address(): expectedValidationReward,
268+
alphaDelegationRewardKey.Address(): expectedDelegationFee,
269+
betaValidationRewardKey.Address(): 0, // Validator didn't meet uptime requirement
270+
betaDelegationRewardKey.Address(): 0, // Validator didn't meet uptime requirement
271+
gammaDelegationRewardKey.Address(): expectedDelegationReward - expectedDelegationFee,
272+
deltaDelegationRewardKey.Address(): 0, // Validator didn't meet uptime requirement
273+
}
274+
for address := range expectedRewardBalances {
275+
require.Equal(expectedRewardBalances[address], rewardBalances[address])
276+
}
277+
278+
ginkgo.By("stopping alpha and beta nodes to free up resources for a bootstrap check")
279+
require.NoError(alphaNode.Stop())
280+
require.NoError(betaNode.Stop())
281+
282+
e2e.CheckBootstrapIsPossible(network)
283+
})
284+
})

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
@@ -507,7 +503,7 @@ func (w *wallet) IssueTx(
507503
}
508504

509505
if txStatus.Status != status.Committed {
510-
return errNotCommitted
506+
return fmt.Errorf("not committed: %s", txStatus.Reason)
511507
}
512508
return nil
513509
}

0 commit comments

Comments
 (0)