Skip to content

Commit cb7166d

Browse files
committed
e2e: Ensure interchain workflow coverage for the P-Chain
1 parent 6c54acf commit cb7166d

File tree

3 files changed

+259
-0
lines changed

3 files changed

+259
-0
lines changed

tests/e2e/e2e.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,17 @@ package e2e
77
import (
88
"context"
99
"encoding/json"
10+
"fmt"
1011
"math/rand"
12+
"strings"
1113
"time"
1214

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

1517
"github.com/stretchr/testify/require"
1618

19+
"github.com/ava-labs/coreth/ethclient"
20+
1721
"github.com/ava-labs/avalanchego/config"
1822
"github.com/ava-labs/avalanchego/tests"
1923
"github.com/ava-labs/avalanchego/tests/fixture"
@@ -22,6 +26,7 @@ import (
2226
"github.com/ava-labs/avalanchego/utils/crypto/secp256k1"
2327
"github.com/ava-labs/avalanchego/vms/secp256k1fx"
2428
"github.com/ava-labs/avalanchego/wallet/subnet/primary"
29+
"github.com/ava-labs/avalanchego/wallet/subnet/primary/common"
2530
)
2631

2732
const (
@@ -44,6 +49,10 @@ const (
4449
// Interval appropriate for network operations that should be
4550
// retried periodically but not too often.
4651
DefaultPollingInterval = 500 * time.Millisecond
52+
53+
// Start time must be a minimum of 15s ahead of the current time
54+
// or validator addition will fail.
55+
DefaultValidatorStartTimeDiff = 20 * time.Second
4756
)
4857

4958
// Env is used to access shared test fixture. Intended to be
@@ -117,6 +126,16 @@ func (te *TestEnvironment) NewWallet(keychain *secp256k1fx.Keychain) primary.Wal
117126
return wallet
118127
}
119128

129+
// Create a new eth client targeting a random node.
130+
func (te *TestEnvironment) NewEthClient() ethclient.Client {
131+
nodeURI := te.GetRandomNodeURI()
132+
nodeAddress := strings.Split(nodeURI, "//")[1]
133+
uri := fmt.Sprintf("ws://%s/ext/bc/C/ws", nodeAddress)
134+
client, err := ethclient.Dial(uri)
135+
te.require.NoError(err)
136+
return client
137+
}
138+
120139
// Add a temporary node that is only intended to be used by a single test. Its ID and
121140
// URI are not intended to be returned from the Network instance to minimize
122141
// accessibility from other tests.
@@ -145,3 +164,38 @@ func WaitForHealthy(node testnet.Node) {
145164
defer cancel()
146165
require.NoError(ginkgo.GinkgoT(), node.WaitForHealthy(ctx))
147166
}
167+
168+
// Helper simplifying use of a timed context by canceling the context on ginkgo teardown.
169+
func ContextWithTimeout(duration time.Duration) context.Context {
170+
ctx, cancel := context.WithTimeout(context.Background(), duration)
171+
ginkgo.DeferCleanup(cancel)
172+
return ctx
173+
}
174+
175+
// Helper simplifying use of a timed context configured with the default timeout.
176+
func DefaultContext() context.Context {
177+
return ContextWithTimeout(DefaultTimeout)
178+
}
179+
180+
// Helper simplifying use via an option of a timed context configured with the default timeout.
181+
func WithDefaultContext() common.Option {
182+
return common.WithContext(DefaultContext())
183+
}
184+
185+
// Re-implementation of testify/require.Eventually that is compatible with ginkgo. testify's
186+
// version calls the condition function with a goroutine and ginkgo assertions don't work
187+
// properly in goroutines.
188+
func Eventually(condition func() bool, waitFor time.Duration, tick time.Duration, msg string) {
189+
ticker := time.NewTicker(tick)
190+
defer ticker.Stop()
191+
192+
ctx, cancel := context.WithTimeout(context.Background(), waitFor)
193+
defer cancel()
194+
for !condition() {
195+
select {
196+
case <-ctx.Done():
197+
require.Fail(ginkgo.GinkgoT(), msg)
198+
case <-ticker.C:
199+
}
200+
}
201+
}

tests/e2e/p/interchain_workflow.go

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
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/big"
8+
"time"
9+
10+
ginkgo "github.com/onsi/ginkgo/v2"
11+
12+
"github.com/stretchr/testify/require"
13+
14+
"github.com/ava-labs/coreth/plugin/evm"
15+
16+
"github.com/ava-labs/avalanchego/api/info"
17+
"github.com/ava-labs/avalanchego/ids"
18+
"github.com/ava-labs/avalanchego/tests/e2e"
19+
"github.com/ava-labs/avalanchego/tests/fixture/testnet"
20+
"github.com/ava-labs/avalanchego/utils/constants"
21+
"github.com/ava-labs/avalanchego/utils/crypto/secp256k1"
22+
"github.com/ava-labs/avalanchego/utils/set"
23+
"github.com/ava-labs/avalanchego/utils/units"
24+
"github.com/ava-labs/avalanchego/vms/components/avax"
25+
"github.com/ava-labs/avalanchego/vms/platformvm/reward"
26+
"github.com/ava-labs/avalanchego/vms/platformvm/txs"
27+
"github.com/ava-labs/avalanchego/vms/secp256k1fx"
28+
"github.com/ava-labs/avalanchego/wallet/subnet/primary/common"
29+
)
30+
31+
var _ = e2e.DescribeXChain("[Interchain Workflow]", func() {
32+
require := require.New(ginkgo.GinkgoT())
33+
34+
const (
35+
transferAmount = 10 * units.Avax
36+
weight = 2_000 * units.Avax // Used for both validation and delegation
37+
)
38+
39+
ginkgo.It("should ensure that funds can be transferred from the P-Chain to the X-Chain and the C-Chain", func() {
40+
ginkgo.By("creating wallet with a funded key to send from and recipient key to deliver to")
41+
factory := secp256k1.Factory{}
42+
recipientKey, err := factory.NewPrivateKey()
43+
require.NoError(err)
44+
keychain := e2e.Env.NewKeychain(1)
45+
keychain.Add(recipientKey)
46+
baseWallet := e2e.Env.NewWallet(keychain)
47+
xWallet := baseWallet.X()
48+
cWallet := baseWallet.C()
49+
pWallet := baseWallet.P()
50+
51+
ginkgo.By("defining common configuration")
52+
recipientEthAddress := evm.GetEthAddress(recipientKey)
53+
avaxAssetID := xWallet.AVAXAssetID()
54+
// Use the same owner for sending to X-Chain and importing funds to P-Chain
55+
recipientOwner := secp256k1fx.OutputOwners{
56+
Threshold: 1,
57+
Addrs: []ids.ShortID{
58+
recipientKey.Address(),
59+
},
60+
}
61+
// Use the same outputs for both X-Chain and C-Chain exports
62+
exportOutputs := []*avax.TransferableOutput{
63+
{
64+
Asset: avax.Asset{
65+
ID: avaxAssetID,
66+
},
67+
Out: &secp256k1fx.TransferOutput{
68+
Amt: transferAmount,
69+
OutputOwners: secp256k1fx.OutputOwners{
70+
Threshold: 1,
71+
Addrs: []ids.ShortID{
72+
keychain.Keys[0].Address(),
73+
},
74+
},
75+
},
76+
},
77+
}
78+
79+
ginkgo.By("adding new node and waiting for it to report healthy")
80+
network := e2e.Env.GetNetwork()
81+
node := e2e.AddTemporaryNode(network, testnet.FlagsMap{})
82+
e2e.WaitForHealthy(node)
83+
84+
ginkgo.By("retrieving new node's id and pop")
85+
infoClient := info.NewClient(node.GetProcessContext().URI)
86+
nodeID, nodePOP, err := infoClient.GetNodeID(e2e.DefaultContext())
87+
require.NoError(err)
88+
89+
ginkgo.By("adding the new node as a validator", func() {
90+
startTime := time.Now().Add(e2e.DefaultValidatorStartTimeDiff)
91+
// Validation duration doesn't actually matter to this
92+
// test - it is only ensuring that adding a validator
93+
// doesn't break interchain transfer.
94+
endTime := startTime.Add(30 * time.Second)
95+
96+
rewardKey, err := factory.NewPrivateKey()
97+
require.NoError(err)
98+
99+
delegationPercent := 0.10 // 10%
100+
delegationFee := uint32(reward.PercentDenominator * delegationPercent)
101+
102+
_, err = pWallet.IssueAddPermissionlessValidatorTx(
103+
&txs.SubnetValidator{Validator: txs.Validator{
104+
NodeID: nodeID,
105+
Start: uint64(startTime.Unix()),
106+
End: uint64(endTime.Unix()),
107+
Wght: weight,
108+
}},
109+
nodePOP,
110+
pWallet.AVAXAssetID(),
111+
&secp256k1fx.OutputOwners{
112+
Threshold: 1,
113+
Addrs: []ids.ShortID{rewardKey.Address()},
114+
},
115+
&secp256k1fx.OutputOwners{
116+
Threshold: 1,
117+
Addrs: []ids.ShortID{rewardKey.Address()},
118+
},
119+
delegationFee,
120+
)
121+
require.NoError(err)
122+
})
123+
124+
ginkgo.By("adding a delegator to the new node", func() {
125+
startTime := time.Now().Add(e2e.DefaultValidatorStartTimeDiff)
126+
// Delegation duration doesn't actually matter to this
127+
// test - it is only ensuring that adding a delegator
128+
// doesn't break interchain transfer.
129+
endTime := startTime.Add(15 * time.Second)
130+
131+
rewardKey, err := factory.NewPrivateKey()
132+
require.NoError(err)
133+
134+
_, err = pWallet.IssueAddPermissionlessDelegatorTx(
135+
&txs.SubnetValidator{Validator: txs.Validator{
136+
NodeID: nodeID,
137+
Start: uint64(startTime.Unix()),
138+
End: uint64(endTime.Unix()),
139+
Wght: weight,
140+
}},
141+
pWallet.AVAXAssetID(),
142+
&secp256k1fx.OutputOwners{
143+
Threshold: 1,
144+
Addrs: []ids.ShortID{rewardKey.Address()},
145+
},
146+
)
147+
require.NoError(err)
148+
})
149+
150+
ginkgo.By("exporting AVAX from the P-Chain to the X-Chain", func() {
151+
_, err := pWallet.IssueExportTx(
152+
xWallet.BlockchainID(),
153+
exportOutputs,
154+
e2e.WithDefaultContext(),
155+
)
156+
require.NoError(err)
157+
})
158+
159+
ginkgo.By("importing AVAX from the P-Chain to the X-Chain", func() {
160+
_, err := xWallet.IssueImportTx(
161+
constants.PlatformChainID,
162+
&recipientOwner,
163+
e2e.WithDefaultContext(),
164+
)
165+
require.NoError(err)
166+
})
167+
168+
ginkgo.By("checking that the recipient address has received imported funds on the X-Chain", func() {
169+
balances, err := xWallet.Builder().GetFTBalance(common.WithCustomAddresses(set.Of(
170+
recipientKey.Address(),
171+
)))
172+
require.NoError(err)
173+
require.Greater(balances[avaxAssetID], uint64(0))
174+
})
175+
176+
ginkgo.By("exporting AVAX from the P-Chain to the C-Chain", func() {
177+
_, err := pWallet.IssueExportTx(
178+
cWallet.BlockchainID(),
179+
exportOutputs,
180+
e2e.WithDefaultContext(),
181+
)
182+
require.NoError(err)
183+
})
184+
185+
ginkgo.By("importing AVAX from the P-Chain to the C-Chain", func() {
186+
_, err := cWallet.IssueImportTx(
187+
constants.PlatformChainID,
188+
recipientEthAddress,
189+
e2e.WithDefaultContext(),
190+
)
191+
require.NoError(err)
192+
})
193+
194+
ginkgo.By("checking that the recipient address has received imported funds on the C-Chain")
195+
ethClient := e2e.Env.NewEthClient()
196+
e2e.Eventually(func() bool {
197+
balance, err := ethClient.BalanceAt(e2e.DefaultContext(), recipientEthAddress, nil)
198+
require.NoError(err)
199+
return balance.Cmp(big.NewInt(0)) > 0
200+
}, e2e.DefaultTimeout, e2e.DefaultPollingInterval, "failed to see recipient address funded before timeout")
201+
})
202+
})

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

0 commit comments

Comments
 (0)