support initialization of evm state in e2e tests (#1524)

* check receipt status for failed txs from evm

* make EvmSigner's Auth public

* setup evm state initialization for e2e

* add a dummy Greeter contract, deployed on start
* move WaitForEvmTxReceipt to from account to util
* add tests for interacting with the contract
* add ContractAddrs map to Chain
This commit is contained in:
Robert Pirtle 2023-04-03 09:58:45 -07:00 committed by GitHub
parent 6a1438fbe9
commit 735d44ba32
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 411 additions and 28 deletions

View File

@ -0,0 +1 @@
[{"inputs":[{"internalType":"string","name":"_greeting","type":"string"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"greet","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"string","name":"_greeting","type":"string"}],"name":"setGreeting","outputs":[],"stateMutability":"nonpayable","type":"function"}]

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,18 @@
//SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.3;
contract Greeter {
string greeting;
constructor(string memory _greeting) {
greeting = _greeting;
}
function greet() public view returns (string memory) {
return greeting;
}
function setGreeting(string memory _greeting) public {
greeting = _greeting;
}
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,24 @@
This directory contains contract interfaces used by the e2e test suite.
# Prereq
* Abigen: https://geth.ethereum.org/docs/tools/abigen
* Solidity: https://docs.soliditylang.org/en/latest/installing-solidity.html
# Create Contract Interfaces for Go
If you have the compiled ABI, you can skip directly to step 4.
To create new go interfaces to contracts:
1. add the solidity file: `<filename>.sol`
2. decide on a package name. this will be the name of the package you'll import into go (`<pkg-name>`)
3. compile the abi & bin for the contract: `solc -o <pkg-name> --abi --bin <filename>.sol`
* run from this directory
* note that `-o` is the output directory. this will generate `<pkg-name>/<filename>.abi`
4. generate the golang interface:
`abigen --abi=<pkg-name>/<filename>.abi --bin=<pkg-name>/<filename>.bin --pkg=<pkg-name> --out=<pkg-name>/main.go`
5. import and use the contract in Go.
By including the bin, the generated interface will have a `Deploy*` method. If you only need to interact with an existing contract, you can exclude the `--bin` and only an interaction method interface will be generated.
# Resources
* https://geth.ethereum.org/docs/developers/dapp-developer/native-bindings

View File

@ -0,0 +1,37 @@
package e2e_test
import (
"time"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/kava-labs/kava/tests/e2e/contracts/greeter"
"github.com/kava-labs/kava/tests/util"
)
func (suite *IntegrationTestSuite) TestEthCallToGreeterContract() {
// this test manipulates state of the Greeter contract which means other tests shouldn't use it.
// setup funded account to interact with contract
user := suite.Kava.NewFundedAccount("greeter-contract-user", sdk.NewCoins(ukava(10e6)))
greeterAddr := suite.Kava.ContractAddrs["greeter"]
contract, err := greeter.NewGreeter(greeterAddr, suite.Kava.EvmClient)
suite.NoError(err)
beforeGreeting, err := contract.Greet(nil)
suite.NoError(err)
updatedGreeting := "look at me, using the evm"
tx, err := contract.SetGreeting(user.EvmAuth, updatedGreeting)
suite.NoError(err)
_, err = util.WaitForEvmTxReceipt(suite.Kava.EvmClient, tx.Hash(), 10*time.Second)
suite.NoError(err)
afterGreeting, err := contract.Greet(nil)
suite.NoError(err)
suite.Equal("what's up!", beforeGreeting)
suite.Equal(updatedGreeting, afterGreeting)
}

@ -1 +1 @@
Subproject commit 8153036b95a930c7830c6969d1653165afa65f2c Subproject commit af9629a2b97475d4d324e4578b58676efa7f07ed

View File

@ -1,9 +1,7 @@
package testutil package testutil
import ( import (
"context"
"encoding/hex" "encoding/hex"
"errors"
"fmt" "fmt"
"log" "log"
"os" "os"
@ -13,7 +11,7 @@ import (
sdk "github.com/cosmos/cosmos-sdk/types" sdk "github.com/cosmos/cosmos-sdk/types"
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
"github.com/cosmos/go-bip39" "github.com/cosmos/go-bip39"
"github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
ethtypes "github.com/ethereum/go-ethereum/core/types" ethtypes "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/crypto"
@ -25,8 +23,6 @@ import (
"github.com/kava-labs/kava/tests/util" "github.com/kava-labs/kava/tests/util"
) )
var ErrBroadcastTimeout = errors.New("timed out waiting for tx to be committed to block")
type SigningAccount struct { type SigningAccount struct {
name string name string
mnemonic string mnemonic string
@ -39,6 +35,8 @@ type SigningAccount struct {
sdkReqChan chan<- util.KavaMsgRequest sdkReqChan chan<- util.KavaMsgRequest
sdkResChan <-chan util.KavaMsgResponse sdkResChan <-chan util.KavaMsgResponse
EvmAuth *bind.TransactOpts
EvmAddress common.Address EvmAddress common.Address
SdkAddress sdk.AccAddress SdkAddress sdk.AccAddress
@ -109,6 +107,8 @@ func (chain *Chain) AddNewSigningAccount(name string, hdPath *hd.BIP44Params, ch
sdkReqChan: sdkReqChan, sdkReqChan: sdkReqChan,
sdkResChan: sdkResChan, sdkResChan: sdkResChan,
EvmAuth: evmSigner.Auth,
EvmAddress: evmSigner.Address(), EvmAddress: evmSigner.Address(),
SdkAddress: kavaSigner.Address(), SdkAddress: kavaSigner.Address(),
} }
@ -162,21 +162,7 @@ func (a *SigningAccount) SignAndBroadcastEvmTx(req util.EvmTxRequest) EvmTxRespo
} }
// if we don't have a tx receipt within a given timeout, fail the request // if we don't have a tx receipt within a given timeout, fail the request
timeout := time.After(10 * time.Second) response.Receipt, response.Err = util.WaitForEvmTxReceipt(a.evmSigner.EvmClient, res.TxHash, 10*time.Second)
for {
select {
case <-timeout:
response.Err = ErrBroadcastTimeout
default:
response.Receipt, response.Err = a.evmSigner.EvmClient.TransactionReceipt(context.Background(), res.TxHash)
if errors.Is(response.Err, ethereum.NotFound) {
// tx still not committed to a block. retry!
time.Sleep(100 * time.Millisecond)
continue
}
}
break
}
return response return response
} }

View File

@ -11,6 +11,7 @@ import (
txtypes "github.com/cosmos/cosmos-sdk/types/tx" txtypes "github.com/cosmos/cosmos-sdk/types/tx"
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/ethclient"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@ -32,6 +33,7 @@ type Chain struct {
ChainId string ChainId string
EvmClient *ethclient.Client EvmClient *ethclient.Client
ContractAddrs map[string]common.Address
Auth authtypes.QueryClient Auth authtypes.QueryClient
Bank banktypes.QueryClient Bank banktypes.QueryClient
@ -49,6 +51,7 @@ func NewChain(t *testing.T, details *runner.ChainDetails, fundedAccountMnemonic
t: t, t: t,
StakingDenom: details.StakingDenom, StakingDenom: details.StakingDenom,
ChainId: details.ChainId, ChainId: details.ChainId,
ContractAddrs: make(map[string]common.Address),
} }
chain.encodingConfig = app.MakeEncodingConfig() chain.encodingConfig = app.MakeEncodingConfig()

View File

@ -0,0 +1,18 @@
package testutil
import "github.com/kava-labs/kava/tests/e2e/contracts/greeter"
// InitKavaEvmData is run after the chain is running, but before the tests are run.
// It is used to initialize some EVM state, such as deploying contracts.
func (suite *E2eTestSuite) InitKavaEvmData() {
whale := suite.Kava.GetAccount(FundedAccountName)
// deploy an example contract
greeterAddr, _, _, err := greeter.DeployGreeter(
whale.evmSigner.Auth,
whale.evmSigner.EvmClient,
"what's up!",
)
suite.NoError(err)
suite.Kava.ContractAddrs["greeter"] = greeterAddr
}

View File

@ -73,6 +73,8 @@ func (suite *E2eTestSuite) SetupSuite() {
suite.T().Fatalf("failed to create ibc chain querier: %s", err) suite.T().Fatalf("failed to create ibc chain querier: %s", err)
} }
} }
suite.InitKavaEvmData()
} }
func (suite *E2eTestSuite) TearDownSuite() { func (suite *E2eTestSuite) TearDownSuite() {

View File

@ -3,9 +3,12 @@ package util
import ( import (
"context" "context"
"crypto/ecdsa" "crypto/ecdsa"
"errors"
"fmt" "fmt"
"math/big" "math/big"
"time"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
ethtypes "github.com/ethereum/go-ethereum/core/types" ethtypes "github.com/ethereum/go-ethereum/core/types"
@ -13,6 +16,14 @@ import (
"github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/ethclient"
) )
var (
ErrEvmBroadcastTimeout = errors.New("timed out waiting for tx to be committed to block")
// ErrEvmTxFailed is returned when a tx is committed to a block, but the receipt status is 0.
// this means the tx failed. we don't have debug_traceTransaction RPC command so the best way
// to determine the problem is to attempt to make the tx manually.
ErrEvmTxFailed = errors.New("transaction was committed but failed. likely an execution revert by contract code")
)
type EvmTxRequest struct { type EvmTxRequest struct {
Tx *ethtypes.Transaction Tx *ethtypes.Transaction
Data interface{} Data interface{}
@ -38,8 +49,8 @@ func (e ErrEvmFailedToBroadcast) Error() string {
// EvmSigner manages signing and broadcasting requests to transfer Erc20 tokens // EvmSigner manages signing and broadcasting requests to transfer Erc20 tokens
// Will work for calling all contracts that have func signature `transfer(address,uint256)` // Will work for calling all contracts that have func signature `transfer(address,uint256)`
type EvmSigner struct { type EvmSigner struct {
auth *bind.TransactOpts
signerAddress common.Address signerAddress common.Address
Auth *bind.TransactOpts
EvmClient *ethclient.Client EvmClient *ethclient.Client
} }
@ -60,7 +71,7 @@ func NewEvmSigner(
} }
return &EvmSigner{ return &EvmSigner{
auth: auth, Auth: auth,
signerAddress: crypto.PubkeyToAddress(*publicKeyECDSA), signerAddress: crypto.PubkeyToAddress(*publicKeyECDSA),
EvmClient: evmClient, EvmClient: evmClient,
}, nil }, nil
@ -77,7 +88,7 @@ func (s *EvmSigner) Run(requests <-chan EvmTxRequest) <-chan EvmTxResponse {
// wait for incoming request // wait for incoming request
req := <-requests req := <-requests
signedTx, err := s.auth.Signer(s.signerAddress, req.Tx) signedTx, err := s.Auth.Signer(s.signerAddress, req.Tx)
if err != nil { if err != nil {
err = ErrEvmFailedToSign{Err: err} err = ErrEvmFailedToSign{Err: err}
} else { } else {
@ -101,3 +112,30 @@ func (s *EvmSigner) Run(requests <-chan EvmTxRequest) <-chan EvmTxResponse {
func (s *EvmSigner) Address() common.Address { func (s *EvmSigner) Address() common.Address {
return s.signerAddress return s.signerAddress
} }
// WaitForEvmTxReceipt polls for a tx receipt and errors on timeout.
// If the receipt comes back, but with status 0 (failed), an error is returned.
func WaitForEvmTxReceipt(client *ethclient.Client, txHash common.Hash, timeout time.Duration) (*ethtypes.Receipt, error) {
var receipt *ethtypes.Receipt
var err error
outOfTime := time.After(timeout)
for {
select {
case <-outOfTime:
err = ErrEvmBroadcastTimeout
default:
receipt, err = client.TransactionReceipt(context.Background(), txHash)
if errors.Is(err, ethereum.NotFound) {
// tx still not committed to a block. retry!
time.Sleep(100 * time.Millisecond)
continue
}
// a response status of 0 means the tx was successfully committed but failed to execute
if receipt.Status == 0 {
err = ErrEvmTxFailed
}
}
break
}
return receipt, err
}