Skip to content

Commit

Permalink
e2e: Ensure interchain workflow coverage for X-Chain and C-Chain (ava…
Browse files Browse the repository at this point in the history
marun authored Aug 25, 2023
1 parent 8d278a1 commit c3e9a69
Showing 7 changed files with 430 additions and 26 deletions.
165 changes: 165 additions & 0 deletions tests/e2e/c/interchain_workflow.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
// Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved.
// See the file LICENSE for licensing terms.

package c

import (
"math/big"

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

"github.com/stretchr/testify/require"

"github.com/ava-labs/coreth/core/types"
"github.com/ava-labs/coreth/plugin/evm"

"github.com/ava-labs/avalanchego/ids"
"github.com/ava-labs/avalanchego/tests/e2e"
"github.com/ava-labs/avalanchego/utils/constants"
"github.com/ava-labs/avalanchego/utils/crypto/secp256k1"
"github.com/ava-labs/avalanchego/utils/set"
"github.com/ava-labs/avalanchego/utils/units"
"github.com/ava-labs/avalanchego/vms/secp256k1fx"
"github.com/ava-labs/avalanchego/wallet/subnet/primary/common"
)

var _ = e2e.DescribeCChain("[Interchain Workflow]", func() {
require := require.New(ginkgo.GinkgoT())

const (
txAmount = 10 * units.Avax // Arbitrary amount to send and transfer
gasLimit = uint64(21000) // Standard gas limit
)

ginkgo.It("should ensure that funds can be transferred from the C-Chain to the X-Chain and the P-Chain", func() {
ginkgo.By("allocating a pre-funded key to send from and a recipient key to deliver to")
senderKey := e2e.Env.AllocateFundedKey()
senderEthAddress := evm.GetEthAddress(senderKey)
factory := secp256k1.Factory{}
recipientKey, err := factory.NewPrivateKey()
require.NoError(err)
recipientEthAddress := evm.GetEthAddress(recipientKey)

// Select a random node URI to use for both the eth client and
// the wallet to avoid having to verify that all nodes are at
// the same height before initializing the wallet.
nodeURI := e2e.Env.GetRandomNodeURI()
ethClient := e2e.Env.NewEthClientForURI(nodeURI)

ginkgo.By("sending funds from one address to another on the C-Chain", func() {
// Create transaction
acceptedNonce, err := ethClient.AcceptedNonceAt(e2e.DefaultContext(), senderEthAddress)
require.NoError(err)
gasPrice, err := ethClient.SuggestGasPrice(e2e.DefaultContext())
require.NoError(err)
tx := types.NewTransaction(
acceptedNonce,
recipientEthAddress,
big.NewInt(int64(txAmount)),
gasLimit,
gasPrice,
nil,
)

// Sign transaction
cChainID, err := ethClient.ChainID(e2e.DefaultContext())
require.NoError(err)
signer := types.NewEIP155Signer(cChainID)
signedTx, err := types.SignTx(tx, signer, senderKey.ToECDSA())
require.NoError(err)

require.NoError(ethClient.SendTransaction(e2e.DefaultContext(), signedTx))

ginkgo.By("waiting for the C-Chain recipient address to have received the sent funds")
e2e.Eventually(func() bool {
balance, err := ethClient.BalanceAt(e2e.DefaultContext(), recipientEthAddress, nil)
require.NoError(err)
return balance.Cmp(big.NewInt(0)) > 0
}, e2e.DefaultTimeout, e2e.DefaultPollingInterval, "failed to see funds delivered before timeout")
})

// Wallet must be initialized after sending funds on the
// C-Chain with the same node URI to ensure wallet state
// matches on-chain state.
ginkgo.By("initializing a keychain and associated wallet")
keychain := secp256k1fx.NewKeychain(senderKey, recipientKey)
baseWallet := e2e.Env.NewWalletForURI(keychain, nodeURI)
xWallet := baseWallet.X()
cWallet := baseWallet.C()
pWallet := baseWallet.P()

ginkgo.By("defining common configuration")
avaxAssetID := xWallet.AVAXAssetID()
// Use the same owner for import funds to X-Chain and P-Chain
recipientOwner := secp256k1fx.OutputOwners{
Threshold: 1,
Addrs: []ids.ShortID{
recipientKey.Address(),
},
}
// Use the same outputs for both X-Chain and P-Chain exports
exportOutputs := []*secp256k1fx.TransferOutput{
{
Amt: txAmount,
OutputOwners: secp256k1fx.OutputOwners{
Threshold: 1,
Addrs: []ids.ShortID{
keychain.Keys[0].Address(),
},
},
},
}

ginkgo.By("exporting AVAX from the C-Chain to the X-Chain", func() {
_, err := cWallet.IssueExportTx(
xWallet.BlockchainID(),
exportOutputs,
e2e.WithDefaultContext(),
)
require.NoError(err)
})

ginkgo.By("importing AVAX from the C-Chain to the X-Chain", func() {
_, err := xWallet.IssueImportTx(
cWallet.BlockchainID(),
&recipientOwner,
e2e.WithDefaultContext(),
)
require.NoError(err)
})

ginkgo.By("checking that the recipient address has received imported funds on the X-Chain", func() {
balances, err := xWallet.Builder().GetFTBalance(common.WithCustomAddresses(set.Of(
recipientKey.Address(),
)))
require.NoError(err)
require.Positive(balances[avaxAssetID])
})

ginkgo.By("exporting AVAX from the C-Chain to the P-Chain", func() {
_, err := cWallet.IssueExportTx(
constants.PlatformChainID,
exportOutputs,
e2e.WithDefaultContext(),
)
require.NoError(err)
})

ginkgo.By("importing AVAX from the C-Chain to the P-Chain", func() {
_, err = pWallet.IssueImportTx(
cWallet.BlockchainID(),
&recipientOwner,
e2e.WithDefaultContext(),
)
require.NoError(err)
})

ginkgo.By("checking that the recipient address has received imported funds on the P-Chain", func() {
balances, err := pWallet.Builder().GetBalance(common.WithCustomAddresses(set.Of(
recipientKey.Address(),
)))
require.NoError(err)
require.Greater(balances[avaxAssetID], uint64(0))
})
})
})
6 changes: 6 additions & 0 deletions tests/e2e/describe.go
Original file line number Diff line number Diff line change
@@ -24,3 +24,9 @@ func DescribeXChainSerial(text string, body func()) bool {
func DescribePChain(text string, body func()) bool {
return ginkgo.Describe("[P-Chain] "+text, body)
}

// DescribeCChain annotates the tests for C-Chain.
// Can run with any type of cluster (e.g., local, fuji, mainnet).
func DescribeCChain(text string, body func()) bool {
return ginkgo.Describe("[C-Chain] "+text, body)
}
67 changes: 62 additions & 5 deletions tests/e2e/e2e.go
Original file line number Diff line number Diff line change
@@ -7,20 +7,25 @@ package e2e
import (
"context"
"encoding/json"
"fmt"
"math/rand"
"strings"
"time"

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

"github.com/stretchr/testify/require"

"github.com/ava-labs/coreth/ethclient"

"github.com/ava-labs/avalanchego/tests"
"github.com/ava-labs/avalanchego/tests/fixture"
"github.com/ava-labs/avalanchego/tests/fixture/testnet"
"github.com/ava-labs/avalanchego/tests/fixture/testnet/local"
"github.com/ava-labs/avalanchego/utils/crypto/secp256k1"
"github.com/ava-labs/avalanchego/vms/secp256k1fx"
"github.com/ava-labs/avalanchego/wallet/subnet/primary"
"github.com/ava-labs/avalanchego/wallet/subnet/primary/common"
)

const (
@@ -98,16 +103,68 @@ func (te *TestEnvironment) NewKeychain(count int) *secp256k1fx.Keychain {
return secp256k1fx.NewKeychain(keys...)
}

// Create a new wallet for the provided keychain.
// Create a new wallet for the provided keychain against a random node URI.
func (te *TestEnvironment) NewWallet(keychain *secp256k1fx.Keychain) primary.Wallet {
return te.NewWalletForURI(keychain, te.GetRandomNodeURI())
}

// Create a new wallet for the provided keychain against the specified node URI.
func (te *TestEnvironment) NewWalletForURI(keychain *secp256k1fx.Keychain, uri string) primary.Wallet {
tests.Outf("{{blue}} initializing a new wallet {{/}}\n")
ctx, cancel := context.WithTimeout(context.Background(), DefaultTimeout)
defer cancel()
wallet, err := primary.MakeWallet(ctx, &primary.WalletConfig{
URI: te.GetRandomNodeURI(),
wallet, err := primary.MakeWallet(DefaultContext(), &primary.WalletConfig{
URI: uri,
AVAXKeychain: keychain,
EthKeychain: keychain,
})
te.require.NoError(err)
return wallet
}

// Create a new eth client targeting a random node.
func (te *TestEnvironment) NewEthClient() ethclient.Client {
return te.NewEthClientForURI(te.GetRandomNodeURI())
}

// Create a new eth client targeting the specified node URI.
func (te *TestEnvironment) NewEthClientForURI(nodeURI string) ethclient.Client {
nodeAddress := strings.Split(nodeURI, "//")[1]
uri := fmt.Sprintf("ws://%s/ext/bc/C/ws", nodeAddress)
client, err := ethclient.Dial(uri)
te.require.NoError(err)
return client
}

// Helper simplifying use of a timed context by canceling the context on ginkgo teardown.
func ContextWithTimeout(duration time.Duration) context.Context {
ctx, cancel := context.WithTimeout(context.Background(), duration)
ginkgo.DeferCleanup(cancel)
return ctx
}

// Helper simplifying use of a timed context configured with the default timeout.
func DefaultContext() context.Context {
return ContextWithTimeout(DefaultTimeout)
}

// Helper simplifying use via an option of a timed context configured with the default timeout.
func WithDefaultContext() common.Option {
return common.WithContext(DefaultContext())
}

// 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:
}
}
}
2 changes: 2 additions & 0 deletions tests/e2e/e2e_test.go
Original file line number Diff line number Diff line change
@@ -25,8 +25,10 @@ import (

// ensure test packages are scanned by ginkgo
_ "github.com/ava-labs/avalanchego/tests/e2e/banff"
_ "github.com/ava-labs/avalanchego/tests/e2e/c"
_ "github.com/ava-labs/avalanchego/tests/e2e/p"
_ "github.com/ava-labs/avalanchego/tests/e2e/static-handlers"
_ "github.com/ava-labs/avalanchego/tests/e2e/x"
_ "github.com/ava-labs/avalanchego/tests/e2e/x/transfer"
)

Loading

0 comments on commit c3e9a69

Please sign in to comment.