Skip to content

Commit

Permalink
e2e: Migrate staking rewards e2e from kurtosis
Browse files Browse the repository at this point in the history
  • Loading branch information
marun committed Aug 22, 2023
1 parent 09480f9 commit 7e81149
Show file tree
Hide file tree
Showing 5 changed files with 292 additions and 7 deletions.
18 changes: 18 additions & 0 deletions tests/e2e/e2e.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,3 +141,21 @@ func WaitForHealthy(node testnet.Node) {
defer cancel()
require.NoError(ginkgo.GinkgoT(), node.WaitForHealthy(ctx))
}

// Re-implementation of testify/require.Eventually that is compatible with ginkgo. testify's
// version calls the condition function with a goroutine and ginkgo assertions don't work
// properly in goroutines.
func Eventually(condition func() bool, waitFor time.Duration, tick time.Duration, msg string) {
ticker := time.NewTicker(tick)
defer ticker.Stop()

ctx, cancel := context.WithTimeout(context.Background(), waitFor)
defer cancel()
for !condition() {
select {
case <-ctx.Done():
require.Fail(ginkgo.GinkgoT(), msg)
case <-ticker.C:
}
}
}
267 changes: 267 additions & 0 deletions tests/e2e/p/staking_rewards.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,267 @@
// Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved.
// See the file LICENSE for licensing terms.

package p

import (
"context"
"math"
"time"

"github.com/mitchellh/mapstructure"

ginkgo "github.com/onsi/ginkgo/v2"

"github.com/spf13/cast"

"github.com/stretchr/testify/require"

"github.com/ava-labs/avalanchego/api/admin"
"github.com/ava-labs/avalanchego/api/info"
"github.com/ava-labs/avalanchego/config"
"github.com/ava-labs/avalanchego/ids"
"github.com/ava-labs/avalanchego/tests"
"github.com/ava-labs/avalanchego/tests/e2e"
"github.com/ava-labs/avalanchego/tests/fixture/testnet"
"github.com/ava-labs/avalanchego/utils/constants"
"github.com/ava-labs/avalanchego/utils/crypto/secp256k1"
"github.com/ava-labs/avalanchego/utils/units"
"github.com/ava-labs/avalanchego/vms/platformvm"
"github.com/ava-labs/avalanchego/vms/platformvm/reward"
"github.com/ava-labs/avalanchego/vms/platformvm/txs"
"github.com/ava-labs/avalanchego/vms/secp256k1fx"
)

const (
validatorStartTimeDiff = 20 * time.Second
delegationPeriod = 15 * time.Second
validationPeriod = 30 * time.Second
)

var _ = ginkgo.Describe("[Staking Rewards]", func() {
require := require.New(ginkgo.GinkgoT())

ginkgo.It("should ensure that validator node uptime determines whether a staking reward is issued", func() {
network := e2e.Env.GetNetwork()

ginkgo.By("checking that the network has a compatible minimum stake duration", func() {
minStakeDuration := cast.ToDuration(network.GetConfig().DefaultFlags[config.MinStakeDurationKey])
require.Equal(testnet.DefaultMinStakeDuration, minStakeDuration)
})
ginkgo.By("adding alpha node, whose uptime should result in a staking reward")
alphaNode := e2e.AddEphemeralNode(network, testnet.FlagsMap{})
ginkgo.By("adding beta node, whose uptime should not result in a staking reward")
betaNode := e2e.AddEphemeralNode(network, testnet.FlagsMap{})

// Wait to check health until both nodes have started to minimize the duration
// required for both nodes to report healthy.
ginkgo.By("waiting until alpha node is healthy")
e2e.WaitForHealthy(alphaNode)
ginkgo.By("waiting until beta node is healthy")
e2e.WaitForHealthy(betaNode)

ginkgo.By("generating reward keys")
factory := secp256k1.Factory{}
alphaValidationRewardKey, err := factory.NewPrivateKey()
require.NoError(err)
alphaDelegationRewardKey, err := factory.NewPrivateKey()
require.NoError(err)
betaValidationRewardKey, err := factory.NewPrivateKey()
require.NoError(err)
betaDelegationRewardKey, err := factory.NewPrivateKey()
require.NoError(err)
gammaDelegationRewardKey, err := factory.NewPrivateKey()
require.NoError(err)
deltaDelegationRewardKey, err := factory.NewPrivateKey()
require.NoError(err)
rewardKeys := []*secp256k1.PrivateKey{
alphaValidationRewardKey,
alphaDelegationRewardKey,
betaValidationRewardKey,
betaDelegationRewardKey,
gammaDelegationRewardKey,
deltaDelegationRewardKey,
}

ginkgo.By("creating keychain and P-Chain wallet")
keychain := secp256k1fx.NewKeychain(rewardKeys...)
fundedKey := e2e.Env.AllocateFundedKey()
keychain.Add(fundedKey)
baseWallet := e2e.Env.NewWallet(keychain)
pWallet := baseWallet.P()

ginkgo.By("retrieving alpha node id and pop")
alphaInfoClient := info.NewClient(alphaNode.GetProcessContext().URI)
alphaNodeID, alphaPOP, err := alphaInfoClient.GetNodeID(context.Background())
require.NoError(err)
ginkgo.By("retrieving beta node id and pop")
betaInfoClient := info.NewClient(betaNode.GetProcessContext().URI)
betaNodeID, betaPOP, err := betaInfoClient.GetNodeID(context.Background())
require.NoError(err)

delegationPercent := 0.10 // 10%
delegationFee := uint32(reward.PercentDenominator * delegationPercent)
weight := 2_000 * units.Avax

alphaValidatorStartTime := time.Now().Add(validatorStartTimeDiff)
alphaValidatorEndTime := alphaValidatorStartTime.Add(validationPeriod)
tests.Outf("alpha node validation period starting at: %v\n", alphaValidatorStartTime)

ginkgo.By("adding alpha node as a validator")
_, err = pWallet.IssueAddPermissionlessValidatorTx(
&txs.SubnetValidator{Validator: txs.Validator{
NodeID: alphaNodeID,
Start: uint64(alphaValidatorStartTime.Unix()),
End: uint64(alphaValidatorEndTime.Unix()),
Wght: weight,
}},
alphaPOP,
pWallet.AVAXAssetID(),
&secp256k1fx.OutputOwners{
Threshold: 1,
Addrs: []ids.ShortID{alphaValidationRewardKey.Address()},
},
&secp256k1fx.OutputOwners{
Threshold: 1,
Addrs: []ids.ShortID{alphaDelegationRewardKey.Address()},
},
delegationFee,
)
require.NoError(err)

betaValidatorStartTime := time.Now().Add(validatorStartTimeDiff)
betaValidatorEndTime := betaValidatorStartTime.Add(validationPeriod)
tests.Outf("beta node validation period starting at: %v\n", betaValidatorStartTime)

ginkgo.By("adding beta node as a validator")
_, err = pWallet.IssueAddPermissionlessValidatorTx(
&txs.SubnetValidator{Validator: txs.Validator{
NodeID: betaNodeID,
Start: uint64(betaValidatorStartTime.Unix()),
End: uint64(betaValidatorEndTime.Unix()),
Wght: weight,
}},
betaPOP,
pWallet.AVAXAssetID(),
&secp256k1fx.OutputOwners{
Threshold: 1,
Addrs: []ids.ShortID{betaValidationRewardKey.Address()},
},
&secp256k1fx.OutputOwners{
Threshold: 1,
Addrs: []ids.ShortID{betaDelegationRewardKey.Address()},
},
delegationFee,
)
require.NoError(err)

gammaDelegatorStartTime := time.Now().Add(validatorStartTimeDiff)
tests.Outf("gamma delegation period starting at: %v\n", gammaDelegatorStartTime)

ginkgo.By("adding gamma as delegator to the alpha node")
_, err = pWallet.IssueAddPermissionlessDelegatorTx(
&txs.SubnetValidator{Validator: txs.Validator{
NodeID: alphaNodeID,
Start: uint64(gammaDelegatorStartTime.Unix()),
End: uint64(gammaDelegatorStartTime.Add(delegationPeriod).Unix()),
Wght: weight,
}},
pWallet.AVAXAssetID(),
&secp256k1fx.OutputOwners{
Threshold: 1,
Addrs: []ids.ShortID{gammaDelegationRewardKey.Address()},
},
)
require.NoError(err)

deltaDelegatorStartTime := time.Now().Add(validatorStartTimeDiff)
tests.Outf("delta delegation period starting at: %v\n", deltaDelegatorStartTime)

ginkgo.By("adding delta as delegator to the beta node")
_, err = pWallet.IssueAddPermissionlessDelegatorTx(
&txs.SubnetValidator{Validator: txs.Validator{
NodeID: betaNodeID,
Start: uint64(deltaDelegatorStartTime.Unix()),
End: uint64(deltaDelegatorStartTime.Add(delegationPeriod).Unix()),
Wght: weight,
}},
pWallet.AVAXAssetID(),
&secp256k1fx.OutputOwners{
Threshold: 1,
Addrs: []ids.ShortID{deltaDelegationRewardKey.Address()},
},
)
require.NoError(err)

ginkgo.By("stopping beta node to prevent it and its delegator from receiving a validation reward")
require.NoError(betaNode.Stop())

ginkgo.By("waiting until all validation periods are over")
// The beta validator was the last added and so has the latest end time. The
// delegation periods are shorter than the validation periods.
time.Sleep(time.Until(betaValidatorEndTime))

pvmClient := platformvm.NewClient(alphaNode.GetProcessContext().URI)

ginkgo.By("waiting until the alpha and beta nodes are no longer validators")
e2e.Eventually(func() bool {
validators, err := pvmClient.GetCurrentValidators(context.Background(), ids.Empty, nil)
require.NoError(err)
for _, validator := range validators {
if validator.NodeID == alphaNodeID || validator.NodeID == betaNodeID {
return false
}
}
return true
}, e2e.DefaultTimeout, e2e.DefaultPollingInterval, "nodes failed to stop validating before timeout ")

ginkgo.By("retrieving reward configuration for the network")
// TODO(marun) Enable GetConfig to return *node.Config
// directly. Currently, due to a circular dependency issue, a
// map-based equivalent is used for which manual unmarshaling
// is required.
adminClient := admin.NewClient(e2e.Env.GetRandomNodeURI())
rawNodeConfigMap, err := adminClient.GetConfig(context.Background())
require.NoError(err)
nodeConfigMap, ok := rawNodeConfigMap.(map[string]interface{})
require.True(ok)
stakingConfigMap, ok := nodeConfigMap["stakingConfig"].(map[string]interface{})
require.True(ok)
rawRewardConfig := stakingConfigMap["rewardConfig"]
rewardConfig := reward.Config{}
require.NoError(mapstructure.Decode(rawRewardConfig, &rewardConfig))

ginkgo.By("retrieving reward address balances")
rewardBalances := make(map[ids.ShortID]uint64, len(rewardKeys))
for _, rewardKey := range rewardKeys {
keychain := secp256k1fx.NewKeychain(rewardKey)
baseWallet := e2e.Env.NewWallet(keychain)
pWallet := baseWallet.P()
balances, err := pWallet.Builder().GetBalance()
require.NoError(err)
rewardBalances[rewardKey.Address()] = balances[pWallet.AVAXAssetID()]
}
require.Len(rewardBalances, len(rewardKeys))

ginkgo.By("determining expected validation and delegation rewards")
currentSupply, err := pvmClient.GetCurrentSupply(context.Background(), constants.PrimaryNetworkID)
require.NoError(err)
calculator := reward.NewCalculator(rewardConfig)
expectedValidationReward := calculator.Calculate(validationPeriod, weight, currentSupply)
expectedDelegationReward := calculator.Calculate(delegationPeriod, weight, currentSupply)
expectedDelegationFee := uint64(math.Round(float64(expectedDelegationReward) * delegationPercent))

ginkgo.By("checking expected rewards against actual rewards")
expectedRewardBalances := map[ids.ShortID]uint64{
alphaValidationRewardKey.Address(): expectedValidationReward,
alphaDelegationRewardKey.Address(): expectedDelegationFee,
betaValidationRewardKey.Address(): 0, // Validator didn't meet uptime requirement
betaDelegationRewardKey.Address(): 0, // Validator didn't meet uptime requirement
gammaDelegationRewardKey.Address(): expectedDelegationReward - expectedDelegationFee,
deltaDelegationRewardKey.Address(): 0, // Validator didn't meet uptime requirement
}
for address := range expectedRewardBalances {
require.Equal(expectedRewardBalances[address], rewardBalances[address])
}
})
})
3 changes: 3 additions & 0 deletions tests/fixture/testnet/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ import (
const (
DefaultNodeCount = 5
DefaultFundedKeyCount = 50

// A short min stake duration enables testing of staking logic.
DefaultMinStakeDuration = time.Second
)

var (
Expand Down
1 change: 1 addition & 0 deletions tests/fixture/testnet/local/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ func LocalFlags() testnet.FlagsMap {
config.IndexEnabledKey: true,
config.LogDisplayLevelKey: "INFO",
config.LogLevelKey: "DEBUG",
config.MinStakeDurationKey: testnet.DefaultMinStakeDuration.String(),
}
}

Expand Down
10 changes: 3 additions & 7 deletions wallet/chain/p/wallet.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
package p

import (
"errors"
"fmt"
"time"

"github.com/ava-labs/avalanchego/ids"
Expand All @@ -17,11 +17,7 @@ import (
"github.com/ava-labs/avalanchego/wallet/subnet/primary/common"
)

var (
errNotCommitted = errors.New("not committed")

_ Wallet = (*wallet)(nil)
)
var _ Wallet = (*wallet)(nil)

type Wallet interface {
Context
Expand Down Expand Up @@ -503,7 +499,7 @@ func (w *wallet) IssueTx(
}

if txStatus.Status != status.Committed {
return errNotCommitted
return fmt.Errorf("not committed: %s", txStatus.Reason)
}
return nil
}

0 comments on commit 7e81149

Please sign in to comment.