Skip to content

Commit

Permalink
e2e: add bidirectional end-to-end tests
Browse files Browse the repository at this point in the history
  • Loading branch information
Leo authored and leoluk committed Nov 29, 2020
1 parent 3027839 commit c31777d
Show file tree
Hide file tree
Showing 12 changed files with 721 additions and 22 deletions.
2 changes: 1 addition & 1 deletion bridge/cmd/guardiand/bridge.go
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ func runBridge(cmd *cobra.Command, args []string) {
*p2pBootstrap = fmt.Sprintf("/dns4/guardian-0.guardian/udp/%d/quic/p2p/%s", *p2pPort, g0key.String())

// Deterministic ganache ETH devnet address.
*ethContract = devnet.BridgeContractAddress.Hex()
*ethContract = devnet.GanacheBridgeContractAddress.Hex()

// Use the hostname as nodeName. For production, we don't want to do this to
// prevent accidentally leaking sensitive hostnames.
Expand Down
110 changes: 110 additions & 0 deletions bridge/e2e/e2e_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package e2e

import (
"context"
"testing"
"time"

"github.com/ethereum/go-ethereum/ethclient"

"github.com/certusone/wormhole/bridge/pkg/devnet"
)

func TestEndToEnd(t *testing.T) {
// List of pods we need in a ready state before we can run tests.
want := []string{
// Our test guardian set.
"guardian-0",
//"guardian-1",
//"guardian-2",
//"guardian-3",
//"guardian-4",
//"guardian-5",

// Connected chains
"solana-devnet-0",

"terra-terrad-0",
"terra-lcd-0",

"eth-devnet-0",
}

c := getk8sClient()

// Wait for all pods to be ready. This blocks until the bridge is ready to receive lockups.
ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
defer cancel()
waitForPods(ctx, c, want)
if ctx.Err() != nil {
t.Fatal(ctx.Err())
}

// Ethereum client.
ec, err := ethclient.Dial(devnet.GanacheRPCURL)
if err != nil {
t.Fatalf("dialing devnet eth rpc failed: %v", err)
}
kt := devnet.GetKeyedTransactor(ctx)

// Generic context for tests.
ctx, cancel = context.WithCancel(context.Background())
defer cancel()

t.Run("[SOL] Native -> [ETH] Wrapped", func(t *testing.T) {
testSolanaLockup(t, ctx, ec, c,
// Source SPL account
devnet.SolanaExampleTokenOwningAccount,
// Source SPL token
devnet.SolanaExampleToken,
// Our wrapped destination token on Ethereum
devnet.GanacheExampleERC20WrappedSOL,
// Amount of SPL token value to transfer.
50*devnet.SolanaDefaultPrecision,
// Same precision - same amount, no precision gained.
0,
)
})

t.Run("[ETH] Wrapped -> [SOL] Native", func(t *testing.T) {
testEthereumLockup(t, ctx, ec, kt, c,
// Source ERC20 token
devnet.GanacheExampleERC20WrappedSOL,
// Destination SPL token account
devnet.SolanaExampleTokenOwningAccount,
// Amount (the reverse of what the previous test did, with the same precision because
// the wrapped ERC20 is set to the original asset's 10**9 precision).
50*devnet.SolanaDefaultPrecision,
// No precision loss
0,
)
})

t.Run("[ETH] Native -> [SOL] Wrapped", func(t *testing.T) {
testEthereumLockup(t, ctx, ec, kt, c,
// Source ERC20 token
devnet.GanacheExampleERC20Token,
// Destination SPL token account
devnet.SolanaExampleWrappedERCTokenOwningAccount,
// Amount
0.000000012*devnet.ERC20DefaultPrecision,
// We lose 9 digits of precision on this path, as the default ERC20 token has 10**18 precision.
9,
)
})

t.Run("[SOL] Wrapped -> [ETH] Native", func(t *testing.T) {
testSolanaLockup(t, ctx, ec, c,
// Source SPL account
devnet.SolanaExampleWrappedERCTokenOwningAccount,
// Source SPL token
devnet.SolanaExampleWrappedERCToken,
// Our wrapped destination token on Ethereum
devnet.GanacheExampleERC20Token,
// Amount of SPL token value to transfer.
0.000000012*devnet.SolanaDefaultPrecision,
// We gain 9 digits of precision on Eth.
9,
)
})
}
107 changes: 107 additions & 0 deletions bridge/e2e/eth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package e2e

import (
"context"
"math"
"math/big"
"testing"
"time"

"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/tendermint/tendermint/libs/rand"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/client-go/kubernetes"

"github.com/certusone/wormhole/bridge/pkg/devnet"
"github.com/certusone/wormhole/bridge/pkg/ethereum/abi"
"github.com/certusone/wormhole/bridge/pkg/ethereum/erc20"
"github.com/certusone/wormhole/bridge/pkg/vaa"
)

// waitEthBalance waits for target account before to increase.
func waitEthBalance(t *testing.T, ctx context.Context, token *erc20.Erc20, before *big.Int, target int64) {
ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()

err := wait.PollUntil(1*time.Second, func() (bool, error) {
after, err := token.BalanceOf(nil, devnet.GanacheClientDefaultAccountAddress)
if err != nil {
t.Log(err)
return false, nil
}

d := new(big.Int).Sub(after, before)
t.Logf("ERC20 balance after: %d -> %d, delta %d", before, after, d)

if after.Cmp(before) != 0 {
if d.Cmp(new(big.Int).SetInt64(target)) != 0 {
t.Errorf("expected ERC20 delta of %v, got: %v", target, d)
}
return true, nil
}
return false, nil
}, ctx.Done())

if err != nil {
t.Error(err)
}
}

func testEthereumLockup(t *testing.T, ctx context.Context, ec *ethclient.Client, kt *bind.TransactOpts,
c *kubernetes.Clientset, tokenAddr common.Address, destination string, amount int64, precisionLoss int) {

// Bridge client
ethBridge, err := abi.NewAbi(devnet.GanacheBridgeContractAddress, ec)
if err != nil {
panic(err)
}

// Source token client
token, err := erc20.NewErc20(tokenAddr, ec)
if err != nil {
panic(err)
}

// Store balance of source ERC20 token
beforeErc20, err := token.BalanceOf(nil, devnet.GanacheClientDefaultAccountAddress)
if err != nil {
t.Log(err) // account may not yet exist, defaults to 0
}
t.Logf("ERC20 balance: %v", beforeErc20)

// Store balance of destination SPL token
beforeSPL, err := getSPLBalance(ctx, c, destination)
if err != nil {
t.Fatal(err)
}
t.Logf("SPL balance: %d", beforeSPL)

// Send lockup
tx, err := ethBridge.LockAssets(kt,
// asset address
tokenAddr,
// token amount
new(big.Int).SetInt64(amount),
// recipient address on target chain
devnet.MustBase58ToEthAddress(destination),
// target chain
vaa.ChainIDSolana,
// random nonce
rand.Uint32(),
// refund dust?
false,
)
if err != nil {
t.Error(err)
}

t.Logf("sent lockup tx: %v", tx.Hash().Hex())

// Destination account increases by full amount.
waitSPLBalance(t, ctx, c, destination, beforeSPL, int64(float64(amount)/math.Pow10(precisionLoss)))

// Source account decreases by the full amount.
waitEthBalance(t, ctx, token, beforeErc20, -int64(amount))
}
149 changes: 149 additions & 0 deletions bridge/e2e/k8s.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
package e2e

import (
"bytes"
"context"
"fmt"
"log"
"strings"

"github.com/golang/glog"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/cache"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/client-go/tools/remotecommand"
)

const (
TiltDefaultNamespace = metav1.NamespaceDefault // hardcoded Tilt assumption
)

func getk8sClient() *kubernetes.Clientset {
config, err := getk8sConfig()

c, err := kubernetes.NewForConfig(config)
if err != nil {
glog.Errorln(err)
}
return c
}

func getk8sConfig() (*rest.Config, error) {
// Load local default kubeconfig.
rules := clientcmd.NewDefaultClientConfigLoadingRules()
rules.DefaultClientConfig = &clientcmd.DefaultClientConfig
return clientcmd.NewNonInteractiveDeferredLoadingClientConfig(
clientcmd.ClientConfigLoader(rules), nil).ClientConfig()
}

func hasPodReadyCondition(conditions []corev1.PodCondition) bool {
for _, condition := range conditions {
if condition.Type == corev1.PodReady && condition.Status == corev1.ConditionTrue {
return true
}
}
return false
}

func waitForPods(ctx context.Context, c *kubernetes.Clientset, want []string) {
found := make(map[string]bool)
ctx, cancel := context.WithCancel(ctx)

watchlist := cache.NewListWatchFromClient(
c.CoreV1().RESTClient(),
"pods",
TiltDefaultNamespace,
fields.Everything(),
)

handle := func(pod *corev1.Pod) {
ready := hasPodReadyCondition(pod.Status.Conditions)
log.Printf("pod added/changed: %s is %s, ready: %v", pod.Name, pod.Status.Phase, ready)

if ready {
found[pod.Name] = true
}

missing := 0
for _, v := range want {
if found[v] == false {
missing += 1
}
}

if missing == 0 {
cancel()
}
}

_, controller := cache.NewInformer(
watchlist,
&corev1.Pod{},
0,
cache.ResourceEventHandlerFuncs{
AddFunc: func(obj interface{}) { handle(obj.(*corev1.Pod)) },
UpdateFunc: func(oldObj, newObj interface{}) { handle(newObj.(*corev1.Pod)) },
},
)

controller.Run(ctx.Done())
}

func executeCommandInPod(ctx context.Context, c *kubernetes.Clientset, podName string, container string, cmd []string) ([]byte, error) {
p, err := c.CoreV1().Pods(TiltDefaultNamespace).Get(ctx, podName, metav1.GetOptions{})
if err != nil {
return nil, fmt.Errorf("failed to get pod %s: %w", p, err)
}

req := c.CoreV1().RESTClient().Post().
Resource("pods").
Name(podName).
Namespace(TiltDefaultNamespace).
SubResource("exec")

req = req.VersionedParams(&corev1.PodExecOptions{
Stdin: false,
Stdout: true,
Stderr: true,
TTY: false,
Container: container,
Command: cmd,
}, scheme.ParameterCodec)

config, err := getk8sConfig()
if err != nil {
return nil, fmt.Errorf("failed to get config: %w", err)
}

exec, err := remotecommand.NewSPDYExecutor(config, "POST", req.URL())
if err != nil {
return nil, fmt.Errorf("failed to init executor: %w", err)
}

var (
execOut bytes.Buffer
execErr bytes.Buffer
)

err = exec.Stream(remotecommand.StreamOptions{
Stdout: &execOut,
Stderr: &execErr,
Tty: false,
})

log.Printf("command: %s", strings.Join(cmd, " "))
if execErr.Len() > 0 {
log.Printf("stderr: %s", execErr.String())
}

if err != nil {
return nil, fmt.Errorf("failed to execute remote command: %w", err)
}

return execOut.Bytes(), nil
}
Loading

0 comments on commit c31777d

Please sign in to comment.