mirror of
https://github.com/0glabs/0g-chain.git
synced 2024-12-26 00:05:18 +00:00
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:
parent
6a1438fbe9
commit
735d44ba32
1
tests/e2e/contracts/greeter/Greeter.abi
Normal file
1
tests/e2e/contracts/greeter/Greeter.abi
Normal 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"}]
|
1
tests/e2e/contracts/greeter/Greeter.bin
Normal file
1
tests/e2e/contracts/greeter/Greeter.bin
Normal file
File diff suppressed because one or more lines are too long
18
tests/e2e/contracts/greeter/Greeter.sol
Normal file
18
tests/e2e/contracts/greeter/Greeter.sol
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
255
tests/e2e/contracts/greeter/main.go
Normal file
255
tests/e2e/contracts/greeter/main.go
Normal file
File diff suppressed because one or more lines are too long
24
tests/e2e/contracts/readme.md
Normal file
24
tests/e2e/contracts/readme.md
Normal 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
|
37
tests/e2e/e2e_evm_contracts_test.go
Normal file
37
tests/e2e/e2e_evm_contracts_test.go
Normal 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
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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"
|
||||||
|
|
||||||
@ -31,7 +32,8 @@ type Chain struct {
|
|||||||
StakingDenom string
|
StakingDenom string
|
||||||
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
|
||||||
@ -46,9 +48,10 @@ type Chain struct {
|
|||||||
// code as "whale" and it is used to supply funds to all new accounts.
|
// code as "whale" and it is used to supply funds to all new accounts.
|
||||||
func NewChain(t *testing.T, details *runner.ChainDetails, fundedAccountMnemonic string) (*Chain, error) {
|
func NewChain(t *testing.T, details *runner.ChainDetails, fundedAccountMnemonic string) (*Chain, error) {
|
||||||
chain := &Chain{
|
chain := &Chain{
|
||||||
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()
|
||||||
|
|
||||||
|
18
tests/e2e/testutil/init_evm.go
Normal file
18
tests/e2e/testutil/init_evm.go
Normal 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
|
||||||
|
}
|
@ -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() {
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user