forked from wormhole-foundation/wormhole
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
e2e: add bidirectional end-to-end tests
- Loading branch information
Showing
12 changed files
with
721 additions
and
22 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
) | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
Oops, something went wrong.