From f47a41988a0d7414484b9f37358fc5618c99a3f3 Mon Sep 17 00:00:00 2001 From: huongnguyenduc Date: Tue, 10 Dec 2024 18:36:59 +0700 Subject: [PATCH 01/15] [WIP] Setup integration tests --- cmd/u2u/launcher/app.go | 11 ++ cmd/u2u/launcher/launcher.go | 6 +- cmd/u2u/launcher/run_test.go | 2 + cmd/u2u/launcher/usage.go | 2 +- cmd/u2u/main.go | 2 +- debug/flags.go | 20 ++ tests/account.go | 21 +++ tests/contracts/.gitignore | 1 + tests/contracts/README.md | 15 ++ tests/contracts/counter/counter.go | 215 +++++++++++++++++++++ tests/contracts/counter/counter.sol | 12 ++ tests/contracts/counter/gen.go | 4 + tests/integration_test_net.go | 279 ++++++++++++++++++++++++++++ tests/integration_test_net_test.go | 115 ++++++++++++ 14 files changed, 702 insertions(+), 3 deletions(-) create mode 100644 cmd/u2u/launcher/app.go create mode 100644 tests/account.go create mode 100644 tests/contracts/.gitignore create mode 100644 tests/contracts/README.md create mode 100644 tests/contracts/counter/counter.go create mode 100644 tests/contracts/counter/counter.sol create mode 100644 tests/contracts/counter/gen.go create mode 100644 tests/integration_test_net.go create mode 100644 tests/integration_test_net_test.go diff --git a/cmd/u2u/launcher/app.go b/cmd/u2u/launcher/app.go new file mode 100644 index 00000000..941f1d2b --- /dev/null +++ b/cmd/u2u/launcher/app.go @@ -0,0 +1,11 @@ +package launcher + +import ( + "os" +) + +func Run() error { + initApp() + initAppHelp() + return app.Run(os.Args) +} diff --git a/cmd/u2u/launcher/launcher.go b/cmd/u2u/launcher/launcher.go index 92aa5321..2ee18704 100644 --- a/cmd/u2u/launcher/launcher.go +++ b/cmd/u2u/launcher/launcher.go @@ -177,7 +177,7 @@ func initFlags() { } // init the CLI app. -func init() { +func initApp() { discfilter.Enable() overrideFlags() overrideParams() @@ -186,6 +186,8 @@ func init() { // App. + app = cli.NewApp() + app.Name = "u2u" app.Action = heliosMain app.Version = params.VersionWithCommit(gitCommit, gitDate) app.HideVersion = true // we have a command to print the version @@ -245,6 +247,8 @@ func init() { } func Launch(args []string) error { + initApp() + initAppHelp() return app.Run(args) } diff --git a/cmd/u2u/launcher/run_test.go b/cmd/u2u/launcher/run_test.go index dd496fae..68ac0a84 100644 --- a/cmd/u2u/launcher/run_test.go +++ b/cmd/u2u/launcher/run_test.go @@ -42,6 +42,8 @@ func (tt *testcli) readConfig() { func init() { // Run the app if we've been exec'd as "u2u-test" in exec(). reexec.Register("u2u-test", func() { + initApp() + initAppHelp() if err := app.Run(os.Args); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) diff --git a/cmd/u2u/launcher/usage.go b/cmd/u2u/launcher/usage.go index 7447fb79..90e81242 100644 --- a/cmd/u2u/launcher/usage.go +++ b/cmd/u2u/launcher/usage.go @@ -90,7 +90,7 @@ func calcAppHelpFlagGroups() []flags.FlagGroup { } } -func init() { +func initAppHelp() { // Override the default app help template cli.AppHelpTemplate = flags.AppHelpTemplate diff --git a/cmd/u2u/main.go b/cmd/u2u/main.go index e33e0d58..fee3a0db 100644 --- a/cmd/u2u/main.go +++ b/cmd/u2u/main.go @@ -8,7 +8,7 @@ import ( ) func main() { - if err := launcher.Launch(os.Args); err != nil { + if err := launcher.Run(); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } diff --git a/debug/flags.go b/debug/flags.go index 6888fefc..0fa93d02 100644 --- a/debug/flags.go +++ b/debug/flags.go @@ -23,6 +23,7 @@ import ( _ "net/http/pprof" "os" "runtime" + "sync" "github.com/mattn/go-colorable" "github.com/mattn/go-isatty" @@ -117,6 +118,25 @@ func init() { // Setup initializes profiling and logging based on the CLI flags. // It should be called as early as possible in the program. func Setup(ctx *cli.Context) error { + setupMutex.Lock() + defer setupMutex.Unlock() + if setupDone { + return nil + } + err := setup(ctx) + if err != nil { + return err + } + setupDone = true + return nil +} + +var ( + setupMutex sync.Mutex + setupDone = false +) + +func setup(ctx *cli.Context) error { var ostream log.Handler output := io.Writer(os.Stderr) if ctx.GlobalBool(logjsonFlag.Name) { diff --git a/tests/account.go b/tests/account.go new file mode 100644 index 00000000..5bf6e095 --- /dev/null +++ b/tests/account.go @@ -0,0 +1,21 @@ +package tests + +import ( + "crypto/ecdsa" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" +) + +type Account struct { + PrivateKey *ecdsa.PrivateKey +} + +func NewAccount() *Account { + key, _ := crypto.GenerateKey() + return &Account{ + PrivateKey: key, + } +} +func (a *Account) Address() common.Address { + return crypto.PubkeyToAddress(a.PrivateKey.PublicKey) +} diff --git a/tests/contracts/.gitignore b/tests/contracts/.gitignore new file mode 100644 index 00000000..c795b054 --- /dev/null +++ b/tests/contracts/.gitignore @@ -0,0 +1 @@ +build \ No newline at end of file diff --git a/tests/contracts/README.md b/tests/contracts/README.md new file mode 100644 index 00000000..e04230f6 --- /dev/null +++ b/tests/contracts/README.md @@ -0,0 +1,15 @@ +# Test Contracts +This directory contains a set of smart contracts used by integration tests. +Contracts are grouped in applications, which may each consist of multiple +contracts. Each application is retained in its own directory. +Within each application directory, the following files should be present: + - *.sol ... the Solidity code for the application + - gen.go ... a go file with a generator rule producing Go bindings for the app + - *.go ... the generated Go binding files (checked in in the code repo) +For an example application, see the `counter` directory. +## Code Generation Tools +For compiling Solidity code and generating the Go bindings the following tools +need to be installed on your system: +- the `solc` compiler; on Ubuntu you an install it using `sudo snap install solc --edge` +- the `abigen` tool; this can be installed using `go install github.com/ethereum/go-ethereum/cmd/abigen@latest` +Background information on those tools can be found [here](https://goethereumbook.org/en/smart-contract-compile/). \ No newline at end of file diff --git a/tests/contracts/counter/counter.go b/tests/contracts/counter/counter.go new file mode 100644 index 00000000..d076e667 --- /dev/null +++ b/tests/contracts/counter/counter.go @@ -0,0 +1,215 @@ +// Code generated - DO NOT EDIT. +// This file is a generated binding and any manual changes will be lost. +package counter +import ( + "errors" + "math/big" + "strings" + ethereum "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/event" +) +// Reference imports to suppress errors if they are not otherwise used. +var ( + _ = errors.New + _ = big.NewInt + _ = strings.NewReader + _ = ethereum.NotFound + _ = bind.Bind + _ = common.Big1 + _ = types.BloomLookup + _ = event.NewSubscription + _ = abi.ConvertType +) +// CounterMetaData contains all meta data concerning the Counter contract. +var CounterMetaData = &bind.MetaData{ + ABI: "[{\"inputs\":[],\"name\":\"getCount\",\"outputs\":[{\"internalType\":\"int256\",\"name\":\"\",\"type\":\"int256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"incrementCounter\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"}]", + Bin: "0x60806040525f80553480156011575f80fd5b5061017d8061001f5f395ff3fe608060405234801561000f575f80fd5b5060043610610034575f3560e01c80635b34b96614610038578063a87d942c14610042575b5f80fd5b610040610060565b005b61004a61007a565b604051610057919061009a565b60405180910390f35b60015f8082825461007191906100e0565b92505081905550565b5f8054905090565b5f819050919050565b61009481610082565b82525050565b5f6020820190506100ad5f83018461008b565b92915050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b5f6100ea82610082565b91506100f583610082565b92508282019050828112155f8312168382125f84121516171561011b5761011a6100b3565b5b9291505056fea26469706673582212202cde1411c0cd5ca1ae5cc4da29a28cd3323857e0c1c4f24109bc9c832c9f376364736f6c637828302e382e32352d646576656c6f702e323032342e322e32342b636f6d6d69742e64626137353465630059", +} +// CounterABI is the input ABI used to generate the binding from. +// Deprecated: Use CounterMetaData.ABI instead. +var CounterABI = CounterMetaData.ABI +// CounterBin is the compiled bytecode used for deploying new contracts. +// Deprecated: Use CounterMetaData.Bin instead. +var CounterBin = CounterMetaData.Bin +// DeployCounter deploys a new Ethereum contract, binding an instance of Counter to it. +func DeployCounter(auth *bind.TransactOpts, backend bind.ContractBackend) (common.Address, *types.Transaction, *Counter, error) { + parsed, err := CounterMetaData.GetAbi() + if err != nil { + return common.Address{}, nil, nil, err + } + if parsed == nil { + return common.Address{}, nil, nil, errors.New("GetABI returned nil") + } + address, tx, contract, err := bind.DeployContract(auth, *parsed, common.FromHex(CounterBin), backend) + if err != nil { + return common.Address{}, nil, nil, err + } + return address, tx, &Counter{CounterCaller: CounterCaller{contract: contract}, CounterTransactor: CounterTransactor{contract: contract}, CounterFilterer: CounterFilterer{contract: contract}}, nil +} +// Counter is an auto generated Go binding around an Ethereum contract. +type Counter struct { + CounterCaller // Read-only binding to the contract + CounterTransactor // Write-only binding to the contract + CounterFilterer // Log filterer for contract events +} +// CounterCaller is an auto generated read-only Go binding around an Ethereum contract. +type CounterCaller struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} +// CounterTransactor is an auto generated write-only Go binding around an Ethereum contract. +type CounterTransactor struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} +// CounterFilterer is an auto generated log filtering Go binding around an Ethereum contract events. +type CounterFilterer struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} +// CounterSession is an auto generated Go binding around an Ethereum contract, +// with pre-set call and transact options. +type CounterSession struct { + Contract *Counter // Generic contract binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} +// CounterCallerSession is an auto generated read-only Go binding around an Ethereum contract, +// with pre-set call options. +type CounterCallerSession struct { + Contract *CounterCaller // Generic contract caller binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session +} +// CounterTransactorSession is an auto generated write-only Go binding around an Ethereum contract, +// with pre-set transact options. +type CounterTransactorSession struct { + Contract *CounterTransactor // Generic contract transactor binding to set the session for + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} +// CounterRaw is an auto generated low-level Go binding around an Ethereum contract. +type CounterRaw struct { + Contract *Counter // Generic contract binding to access the raw methods on +} +// CounterCallerRaw is an auto generated low-level read-only Go binding around an Ethereum contract. +type CounterCallerRaw struct { + Contract *CounterCaller // Generic read-only contract binding to access the raw methods on +} +// CounterTransactorRaw is an auto generated low-level write-only Go binding around an Ethereum contract. +type CounterTransactorRaw struct { + Contract *CounterTransactor // Generic write-only contract binding to access the raw methods on +} +// NewCounter creates a new instance of Counter, bound to a specific deployed contract. +func NewCounter(address common.Address, backend bind.ContractBackend) (*Counter, error) { + contract, err := bindCounter(address, backend, backend, backend) + if err != nil { + return nil, err + } + return &Counter{CounterCaller: CounterCaller{contract: contract}, CounterTransactor: CounterTransactor{contract: contract}, CounterFilterer: CounterFilterer{contract: contract}}, nil +} +// NewCounterCaller creates a new read-only instance of Counter, bound to a specific deployed contract. +func NewCounterCaller(address common.Address, caller bind.ContractCaller) (*CounterCaller, error) { + contract, err := bindCounter(address, caller, nil, nil) + if err != nil { + return nil, err + } + return &CounterCaller{contract: contract}, nil +} +// NewCounterTransactor creates a new write-only instance of Counter, bound to a specific deployed contract. +func NewCounterTransactor(address common.Address, transactor bind.ContractTransactor) (*CounterTransactor, error) { + contract, err := bindCounter(address, nil, transactor, nil) + if err != nil { + return nil, err + } + return &CounterTransactor{contract: contract}, nil +} +// NewCounterFilterer creates a new log filterer instance of Counter, bound to a specific deployed contract. +func NewCounterFilterer(address common.Address, filterer bind.ContractFilterer) (*CounterFilterer, error) { + contract, err := bindCounter(address, nil, nil, filterer) + if err != nil { + return nil, err + } + return &CounterFilterer{contract: contract}, nil +} +// bindCounter binds a generic wrapper to an already deployed contract. +func bindCounter(address common.Address, caller bind.ContractCaller, transactor bind.ContractTransactor, filterer bind.ContractFilterer) (*bind.BoundContract, error) { + parsed, err := CounterMetaData.GetAbi() + if err != nil { + return nil, err + } + return bind.NewBoundContract(address, *parsed, caller, transactor, filterer), nil +} +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_Counter *CounterRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _Counter.Contract.CounterCaller.contract.Call(opts, result, method, params...) +} +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_Counter *CounterRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _Counter.Contract.CounterTransactor.contract.Transfer(opts) +} +// Transact invokes the (paid) contract method with params as input values. +func (_Counter *CounterRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _Counter.Contract.CounterTransactor.contract.Transact(opts, method, params...) +} +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_Counter *CounterCallerRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _Counter.Contract.contract.Call(opts, result, method, params...) +} +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_Counter *CounterTransactorRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _Counter.Contract.contract.Transfer(opts) +} +// Transact invokes the (paid) contract method with params as input values. +func (_Counter *CounterTransactorRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _Counter.Contract.contract.Transact(opts, method, params...) +} +// GetCount is a free data retrieval call binding the contract method 0xa87d942c. +// +// Solidity: function getCount() view returns(int256) +func (_Counter *CounterCaller) GetCount(opts *bind.CallOpts) (*big.Int, error) { + var out []interface{} + err := _Counter.contract.Call(opts, &out, "getCount") + if err != nil { + return *new(*big.Int), err + } + out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + return out0, err +} +// GetCount is a free data retrieval call binding the contract method 0xa87d942c. +// +// Solidity: function getCount() view returns(int256) +func (_Counter *CounterSession) GetCount() (*big.Int, error) { + return _Counter.Contract.GetCount(&_Counter.CallOpts) +} +// GetCount is a free data retrieval call binding the contract method 0xa87d942c. +// +// Solidity: function getCount() view returns(int256) +func (_Counter *CounterCallerSession) GetCount() (*big.Int, error) { + return _Counter.Contract.GetCount(&_Counter.CallOpts) +} +// IncrementCounter is a paid mutator transaction binding the contract method 0x5b34b966. +// +// Solidity: function incrementCounter() returns() +func (_Counter *CounterTransactor) IncrementCounter(opts *bind.TransactOpts) (*types.Transaction, error) { + return _Counter.contract.Transact(opts, "incrementCounter") +} +// IncrementCounter is a paid mutator transaction binding the contract method 0x5b34b966. +// +// Solidity: function incrementCounter() returns() +func (_Counter *CounterSession) IncrementCounter() (*types.Transaction, error) { + return _Counter.Contract.IncrementCounter(&_Counter.TransactOpts) +} +// IncrementCounter is a paid mutator transaction binding the contract method 0x5b34b966. +// +// Solidity: function incrementCounter() returns() +func (_Counter *CounterTransactorSession) IncrementCounter() (*types.Transaction, error) { + return _Counter.Contract.IncrementCounter(&_Counter.TransactOpts) +} \ No newline at end of file diff --git a/tests/contracts/counter/counter.sol b/tests/contracts/counter/counter.sol new file mode 100644 index 00000000..5e3747a4 --- /dev/null +++ b/tests/contracts/counter/counter.sol @@ -0,0 +1,12 @@ + +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; +contract Counter { + int private count = 0; + function incrementCounter() public { + count += 1; + } + function getCount() public view returns (int) { + return count; + } +} \ No newline at end of file diff --git a/tests/contracts/counter/gen.go b/tests/contracts/counter/gen.go new file mode 100644 index 00000000..2b0ebf1c --- /dev/null +++ b/tests/contracts/counter/gen.go @@ -0,0 +1,4 @@ +package counter + +//go:generate solc --bin counter.sol --abi counter.sol -o build --overwrite +//go:generate abigen --bin=build/Counter.bin --abi=build/Counter.abi --pkg=counter --out=counter.go diff --git a/tests/integration_test_net.go b/tests/integration_test_net.go new file mode 100644 index 00000000..8bea7988 --- /dev/null +++ b/tests/integration_test_net.go @@ -0,0 +1,279 @@ +package tests + +import ( + "context" + "errors" + "fmt" + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/ethclient" + u2u "github.com/unicornultrafoundation/go-u2u/cmd/u2u/launcher" + "github.com/unicornultrafoundation/go-u2u/evmcore" + "math/big" + "os" + "syscall" + "time" +) + +// IntegrationTestNet is a in-process test network for integration tests. When +// started, it runs a full U2U node maintaining a chain within the process +// containing this object. The network can be used to run transactions on and +// to perform queries against. +// +// The main purpose of this network is to facilitate end-to-end debugging of +// client code in the controlled scope of individual unit tests. When running +// tests against an integration test network instance, break-points can be set +// in the client code, thereby facilitating debugging. +// +// A typical use case would look as follows: +// +// func TestMyClientCode(t *testing.T) { +// net, err := StartIntegrationTestNet(t.TempDir()) +// if err != nil { +// t.Fatalf("Failed to start the fake network: %v", err) +// } +// defer net.Stop() +// +// } +// +// Additionally, by providing support for scripting test traffic on a network, +// integration test networks can also be used for automated integration and +// regression tests for client code. +type IntegrationTestNet struct { + done <-chan struct{} + validator Account +} + +// StartIntegrationTestNet starts a single-node test network for integration tests. +// The node serving the network is started in the same process as the caller. This +// is intended to facilitate debugging of client code in the context of a running +// node. +func StartIntegrationTestNet(directory string) (*IntegrationTestNet, error) { + done := make(chan struct{}) + go func() { + defer close(done) + originalArgs := os.Args + defer func() { os.Args = originalArgs }() + // start the fakenet u2u node + // equivalent to running `u2u ...` but in this local process + os.Args = []string{ + "u2u", + "--datadir", directory, + "--fakenet", "1/1", + "--http", "--http.addr", "0.0.0.0", "--http.port", "18545", + "--http.api", "admin,eth,web3,net,txpool,ftm,trace,debug", + "--ws", "--ws.addr", "0.0.0.0", "--ws.port", "18546", "--ws.api", "admin,eth,ftm", + "--datadir.minfreedisk", "0", + "--nat", "none", + "--nodiscover", + } + err := u2u.Run() + if err != nil { + panic(fmt.Sprint("Failed to start the fake network:", err)) + } + }() + result := &IntegrationTestNet{ + done: done, + validator: Account{evmcore.FakeKey(1)}, + } + // connect to blockchain network + client, err := result.GetClient() + if err != nil { + return nil, fmt.Errorf("failed to connect to the Ethereum client: %w", err) + } + defer client.Close() + const timeout = 300 * time.Second + start := time.Now() + // wait for the node to be ready to serve requests + const maxDelay = 100 * time.Millisecond + delay := time.Millisecond + for time.Since(start) < timeout { + _, err := client.ChainID(context.Background()) + if err != nil { + time.Sleep(delay) + delay = 2 * delay + if delay > maxDelay { + delay = maxDelay + } + continue + } + return result, nil + } + return nil, fmt.Errorf("failed to successfully start up a test network within %d", timeout) +} + +// Stop shuts the underlying network down. +func (n *IntegrationTestNet) Stop() { + syscall.Kill(syscall.Getpid(), syscall.SIGINT) + <-n.done + n.done = nil +} + +// EndowAccount sends a requested amount of tokens to the given account. This is +// mainly intended to provide funds to accounts for testing purposes. +func (n *IntegrationTestNet) EndowAccount( + address common.Address, + value int64, +) error { + client, err := n.GetClient() + if err != nil { + return fmt.Errorf("failed to connect to the network: %w", err) + } + defer client.Close() + chainId, err := client.ChainID(context.Background()) + if err != nil { + return fmt.Errorf("failed to get chain ID: %w", err) + } + // The requested funds are moved from the validator account to the target account. + nonce, err := client.NonceAt(context.Background(), n.validator.Address(), nil) + if err != nil { + return fmt.Errorf("failed to get nonce: %w", err) + } + price, err := client.SuggestGasPrice(context.Background()) + if err != nil { + return fmt.Errorf("failed to get gas price: %w", err) + } + transaction, err := types.SignTx(types.NewTx(&types.AccessListTx{ + ChainID: chainId, + Gas: 21000, + GasPrice: price, + To: &address, + Value: big.NewInt(value), + Nonce: nonce, + }), types.NewLondonSigner(chainId), n.validator.PrivateKey) + if err != nil { + return fmt.Errorf("failed to sign transaction: %w", err) + } + _, err = n.Run(transaction) + return err +} + +// Run sends the given transaction to the network and waits for it to be processed. +// The resulting receipt is returned. This function times out after 10 seconds. +func (n *IntegrationTestNet) Run(tx *types.Transaction) (*types.Receipt, error) { + client, err := n.GetClient() + if err != nil { + return nil, fmt.Errorf("failed to connect to the Ethereum client: %w", err) + } + defer client.Close() + err = client.SendTransaction(context.Background(), tx) + if err != nil { + return nil, fmt.Errorf("failed to send transaction: %w", err) + } + return n.GetReceipt(tx.Hash()) +} + +// GetReceipt waits for the receipt of the given transaction hash to be available. +// The function times out after 10 seconds. +func (n *IntegrationTestNet) GetReceipt(txHash common.Hash) (*types.Receipt, error) { + client, err := n.GetClient() + if err != nil { + return nil, fmt.Errorf("failed to connect to the Ethereum client: %w", err) + } + defer client.Close() + // Wait for the response with some exponential backoff. + const maxDelay = 100 * time.Millisecond + now := time.Now() + delay := time.Millisecond + for time.Since(now) < 100*time.Second { + receipt, err := client.TransactionReceipt(context.Background(), txHash) + if errors.Is(err, ethereum.NotFound) { + time.Sleep(delay) + delay = 2 * delay + if delay > maxDelay { + delay = maxDelay + } + continue + } + if err != nil { + return nil, fmt.Errorf("failed to get transaction receipt: %w", err) + } + return receipt, nil + } + return nil, fmt.Errorf("failed to get transaction receipt: timeout") +} + +// Apply sends a transaction to the network using the network's validator account +// and waits for the transaction to be processed. The resulting receipt is returned. +func (n *IntegrationTestNet) Apply( + issue func(*bind.TransactOpts) (*types.Transaction, error), +) (*types.Receipt, error) { + txOpts, err := n.GetTransactOptions(&n.validator) + if err != nil { + return nil, fmt.Errorf("failed to get transaction options: %w", err) + } + transaction, err := issue(txOpts) + if err != nil { + return nil, fmt.Errorf("failed to create transaction: %w", err) + } + return n.GetReceipt(transaction.Hash()) +} + +// GetTransactOptions provides transaction options to be used to send a transaction +// with the given account. The options include the chain ID, a suggested gas price, +// the next free nonce of the given account, and a hard-coded gas limit of 1e6. +// The main purpose of this function is to provide a convenient way to collect all +// the necessary information required to create a transaction in one place. +func (n *IntegrationTestNet) GetTransactOptions(account *Account) (*bind.TransactOpts, error) { + client, err := n.GetClient() + if err != nil { + return nil, fmt.Errorf("failed to connect to the Ethereum client: %w", err) + } + defer client.Close() + ctxt := context.Background() + chainId, err := client.ChainID(ctxt) + if err != nil { + return nil, fmt.Errorf("failed to get chain ID: %w", err) + } + gasPrice, err := client.SuggestGasPrice(ctxt) + if err != nil { + return nil, fmt.Errorf("failed to get gas price suggestion: %w", err) + } + nonce, err := client.NonceAt(ctxt, account.Address(), nil) + if err != nil { + return nil, fmt.Errorf("failed to get nonce: %w", err) + } + txOpts, err := bind.NewKeyedTransactorWithChainID(account.PrivateKey, chainId) + if err != nil { + return nil, fmt.Errorf("failed to create transaction options: %w", err) + } + txOpts.GasPrice = new(big.Int).Mul(gasPrice, big.NewInt(2)) + txOpts.Nonce = big.NewInt(int64(nonce)) + txOpts.GasLimit = 1e6 + return txOpts, nil +} + +// GetClient provides raw access to a fresh connection to the network. +// The resulting client must be closed after use. +func (n *IntegrationTestNet) GetClient() (*ethclient.Client, error) { + return ethclient.Dial("http://localhost:18545") +} + +// DeployContract is a utility function handling the deployment of a contract on the network. +// The contract is deployed with by the network's validator account. The function returns the +// deployed contract instance and the transaction receipt. +func DeployContract[T any](n *IntegrationTestNet, deploy contractDeployer[T]) (*T, *types.Receipt, error) { + client, err := n.GetClient() + if err != nil { + return nil, nil, fmt.Errorf("failed to connect to the Ethereum client: %w", err) + } + defer client.Close() + transactOptions, err := n.GetTransactOptions(&n.validator) + if err != nil { + return nil, nil, fmt.Errorf("failed to get transaction options: %w", err) + } + _, transaction, contract, err := deploy(transactOptions, client) + if err != nil { + return nil, nil, fmt.Errorf("failed to deploy contract: %w", err) + } + receipt, err := n.GetReceipt(transaction.Hash()) + if err != nil { + return nil, nil, fmt.Errorf("failed to get receipt: %w", err) + } + return contract, receipt, nil +} + +// contractDeployer is the type of the deployment functions generated by abigen. +type contractDeployer[T any] func(*bind.TransactOpts, bind.ContractBackend) (common.Address, *types.Transaction, *T, error) diff --git a/tests/integration_test_net_test.go b/tests/integration_test_net_test.go new file mode 100644 index 00000000..d4a442e6 --- /dev/null +++ b/tests/integration_test_net_test.go @@ -0,0 +1,115 @@ +package tests + +import ( + "context" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/unicornultrafoundation/go-u2u/tests/contracts/counter" + "math/big" + "testing" +) + +func TestIntegrationTestNet_CanStartAndStopIntegrationTestNet(t *testing.T) { + dataDir := t.TempDir() + net, err := StartIntegrationTestNet(dataDir) + if err != nil { + t.Fatalf("Failed to start the fake network: %v", err) + } + net.Stop() +} +func TestIntegrationTestNet_CanStartMultipleConsecutiveInstances(t *testing.T) { + for i := 0; i < 2; i++ { + dataDir := t.TempDir() + net, err := StartIntegrationTestNet(dataDir) + if err != nil { + t.Fatalf("Failed to start the fake network: %v", err) + } + net.Stop() + } +} +func TestIntegrationTestNet_CanFetchInformationFromTheNetwork(t *testing.T) { + dataDir := t.TempDir() + net, err := StartIntegrationTestNet(dataDir) + if err != nil { + t.Fatalf("Failed to start the fake network: %v", err) + } + defer net.Stop() + client, err := net.GetClient() + if err != nil { + t.Fatalf("Failed to connect to the integration test network: %v", err) + } + defer client.Close() + block, err := client.BlockNumber(context.Background()) + if err != nil { + t.Fatalf("Failed to get block number: %v", err) + } + if block == 0 || block > 1000 { + t.Errorf("Unexpected block number: %v", block) + } +} +func TestIntegrationTestNet_CanEndowAccountsWithTokens(t *testing.T) { + dataDir := t.TempDir() + net, err := StartIntegrationTestNet(dataDir) + if err != nil { + t.Fatalf("Failed to start the fake network: %v", err) + } + defer net.Stop() + client, err := net.GetClient() + if err != nil { + t.Fatalf("Failed to connect to the integration test network: %v", err) + } + address := common.Address{0x01} + balance, err := client.BalanceAt(context.Background(), address, nil) + if err != nil { + t.Fatalf("Failed to get balance for account: %v", err) + } + for i := 0; i < 10; i++ { + increment := int64(1000) + if err := net.EndowAccount(address, increment); err != nil { + t.Fatalf("Failed to endow account 1: %v", err) + } + want := balance.Add(balance, big.NewInt(int64(increment))) + balance, err = client.BalanceAt(context.Background(), address, nil) + if err != nil { + t.Fatalf("Failed to get balance for account: %v", err) + } + if want, got := want, balance; want.Cmp(got) != 0 { + t.Fatalf("Unexpected balance for account, got %v, wanted %v", got, want) + } + balance = want + } +} +func TestIntegrationTestNet_CanDeployContracts(t *testing.T) { + dataDir := t.TempDir() + net, err := StartIntegrationTestNet(dataDir) + if err != nil { + t.Fatalf("Failed to start the fake network: %v", err) + } + defer net.Stop() + _, receipt, err := DeployContract(net, counter.DeployCounter) + if err != nil { + t.Fatalf("Failed to deploy contract: %v", err) + } + if receipt.Status != types.ReceiptStatusSuccessful { + t.Errorf("Contract deployment failed: %v", receipt) + } +} +func TestIntegrationTestNet_CanInteractWithContract(t *testing.T) { + dataDir := t.TempDir() + net, err := StartIntegrationTestNet(dataDir) + if err != nil { + t.Fatalf("Failed to start the fake network: %v", err) + } + defer net.Stop() + contract, _, err := DeployContract(net, counter.DeployCounter) + if err != nil { + t.Fatalf("Failed to deploy contract: %v", err) + } + receipt, err := net.Apply(contract.IncrementCounter) + if err != nil { + t.Fatalf("Failed to send transaction: %v", err) + } + if receipt.Status != types.ReceiptStatusSuccessful { + t.Errorf("Contract deployment failed: %v", receipt) + } +} From 0c4e56d0bc7159b94140f2103bccc7c208072660 Mon Sep 17 00:00:00 2001 From: huongnguyenduc Date: Wed, 11 Dec 2024 17:20:40 +0700 Subject: [PATCH 02/15] Use mux to prevent server from registering multiple routes in integration tests --- monitoring/prometheus/handler.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/monitoring/prometheus/handler.go b/monitoring/prometheus/handler.go index ee7fbc9e..122d993f 100644 --- a/monitoring/prometheus/handler.go +++ b/monitoring/prometheus/handler.go @@ -22,7 +22,9 @@ func PrometheusListener(endpoint string, reg metrics.Registry) { logger.Info("metrics server starts", "endpoint", endpoint) defer logger.Info("metrics server is stopped") - http.HandleFunc( + // http.HandleFunc(promhttp.Handler().ServeHTTP) + mux := http.NewServeMux() + mux.HandleFunc( "/metrics", promhttp.Handler().ServeHTTP) err := http.ListenAndServe(endpoint, nil) if err != nil { From 193b2afb10cf5e1971c8d8f38f360329d5c9399b Mon Sep 17 00:00:00 2001 From: huongnguyenduc Date: Wed, 11 Dec 2024 17:22:24 +0700 Subject: [PATCH 03/15] Setup available random ports for testing --- tests/integration_test_net.go | 78 +++++++++++++++++++++++++++++------ 1 file changed, 66 insertions(+), 12 deletions(-) diff --git a/tests/integration_test_net.go b/tests/integration_test_net.go index 8bea7988..eda24716 100644 --- a/tests/integration_test_net.go +++ b/tests/integration_test_net.go @@ -4,6 +4,14 @@ import ( "context" "errors" "fmt" + "math" + "math/big" + "math/rand/v2" + "net" + "os" + "syscall" + "time" + "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" @@ -11,10 +19,6 @@ import ( "github.com/ethereum/go-ethereum/ethclient" u2u "github.com/unicornultrafoundation/go-u2u/cmd/u2u/launcher" "github.com/unicornultrafoundation/go-u2u/evmcore" - "math/big" - "os" - "syscall" - "time" ) // IntegrationTestNet is a in-process test network for integration tests. When @@ -42,8 +46,31 @@ import ( // integration test networks can also be used for automated integration and // regression tests for client code. type IntegrationTestNet struct { - done <-chan struct{} - validator Account + done <-chan struct{} + validator Account + httpClientPort int +} + +func isPortFree(host string, port int) bool { + address := fmt.Sprintf("%s:%d", host, port) + listener, err := net.Listen("tcp", address) + if err != nil { + return false + } + listener.Close() + return true +} + +func getFreePort() (int, error) { + var port int + retries := 10 + for i := 0; i < retries; i++ { + port = 1023 + (rand.Int()%math.MaxUint16 - 1023) + if isPortFree("127.0.0.1", port) { + return port, nil + } + } + return 0, fmt.Errorf("failed to find a free port after %d retries (last %d)", retries, port) } // StartIntegrationTestNet starts a single-node test network for integration tests. @@ -51,6 +78,20 @@ type IntegrationTestNet struct { // is intended to facilitate debugging of client code in the context of a running // node. func StartIntegrationTestNet(directory string) (*IntegrationTestNet, error) { + // find free ports for the http-client, ws-client, and network interfaces + var err error + httpClientPort, err := getFreePort() + if err != nil { + return nil, err + } + wsPort, err := getFreePort() + if err != nil { + return nil, err + } + netPort, err := getFreePort() + if err != nil { + return nil, err + } done := make(chan struct{}) go func() { defer close(done) @@ -60,12 +101,24 @@ func StartIntegrationTestNet(directory string) (*IntegrationTestNet, error) { // equivalent to running `u2u ...` but in this local process os.Args = []string{ "u2u", + + // data storage options "--datadir", directory, + "--datadir.minfreedisk", "0", + + // fake network options "--fakenet", "1/1", - "--http", "--http.addr", "0.0.0.0", "--http.port", "18545", + + // http-client option + "--http", "--http.addr", "0.0.0.0", "--http.port", fmt.Sprint(httpClientPort), "--http.api", "admin,eth,web3,net,txpool,ftm,trace,debug", - "--ws", "--ws.addr", "0.0.0.0", "--ws.port", "18546", "--ws.api", "admin,eth,ftm", - "--datadir.minfreedisk", "0", + + // websocket-client options + "--ws", "--ws.addr", "0.0.0.0", "--ws.port", fmt.Sprint(wsPort), + "--ws.api", "admin,eth,ftm", + + // net options + "--port", fmt.Sprint(netPort), "--nat", "none", "--nodiscover", } @@ -75,8 +128,9 @@ func StartIntegrationTestNet(directory string) (*IntegrationTestNet, error) { } }() result := &IntegrationTestNet{ - done: done, - validator: Account{evmcore.FakeKey(1)}, + done: done, + validator: Account{evmcore.FakeKey(1)}, + httpClientPort: httpClientPort, } // connect to blockchain network client, err := result.GetClient() @@ -248,7 +302,7 @@ func (n *IntegrationTestNet) GetTransactOptions(account *Account) (*bind.Transac // GetClient provides raw access to a fresh connection to the network. // The resulting client must be closed after use. func (n *IntegrationTestNet) GetClient() (*ethclient.Client, error) { - return ethclient.Dial("http://localhost:18545") + return ethclient.Dial(fmt.Sprintf("http://localhost:%d", n.httpClientPort)) } // DeployContract is a utility function handling the deployment of a contract on the network. From 64fece102e7f629973892e2ba36113a3dac73565 Mon Sep 17 00:00:00 2001 From: trinhdn97 Date: Thu, 12 Dec 2024 15:32:54 +0700 Subject: [PATCH 04/15] Use go-u2u codebase as dependencies instead of geth --- tests/account.go | 5 ++-- tests/contracts/counter/counter.go | 15 ++++++------ tests/integration_test_net.go | 13 +++++----- tests/integration_test_net_test.go | 38 +++++++++++++++++++++++------- 4 files changed, 47 insertions(+), 24 deletions(-) diff --git a/tests/account.go b/tests/account.go index 5bf6e095..0c373153 100644 --- a/tests/account.go +++ b/tests/account.go @@ -2,8 +2,9 @@ package tests import ( "crypto/ecdsa" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/crypto" + + "github.com/unicornultrafoundation/go-u2u/common" + "github.com/unicornultrafoundation/go-u2u/crypto" ) type Account struct { diff --git a/tests/contracts/counter/counter.go b/tests/contracts/counter/counter.go index d076e667..f322bf6a 100644 --- a/tests/contracts/counter/counter.go +++ b/tests/contracts/counter/counter.go @@ -5,19 +5,20 @@ import ( "errors" "math/big" "strings" - ethereum "github.com/ethereum/go-ethereum" - "github.com/ethereum/go-ethereum/accounts/abi" - "github.com/ethereum/go-ethereum/accounts/abi/bind" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/event" + + go_u2u "github.com/unicornultrafoundation/go-u2u" + "github.com/unicornultrafoundation/go-u2u/accounts/abi" + "github.com/unicornultrafoundation/go-u2u/accounts/abi/bind" + "github.com/unicornultrafoundation/go-u2u/common" + "github.com/unicornultrafoundation/go-u2u/core/types" + "github.com/unicornultrafoundation/go-u2u/event" ) // Reference imports to suppress errors if they are not otherwise used. var ( _ = errors.New _ = big.NewInt _ = strings.NewReader - _ = ethereum.NotFound + _ = go_u2u.NotFound _ = bind.Bind _ = common.Big1 _ = types.BloomLookup diff --git a/tests/integration_test_net.go b/tests/integration_test_net.go index eda24716..9a91b607 100644 --- a/tests/integration_test_net.go +++ b/tests/integration_test_net.go @@ -12,12 +12,12 @@ import ( "syscall" "time" - "github.com/ethereum/go-ethereum" - "github.com/ethereum/go-ethereum/accounts/abi/bind" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/ethclient" + go_u2u "github.com/unicornultrafoundation/go-u2u" + "github.com/unicornultrafoundation/go-u2u/accounts/abi/bind" u2u "github.com/unicornultrafoundation/go-u2u/cmd/u2u/launcher" + "github.com/unicornultrafoundation/go-u2u/common" + "github.com/unicornultrafoundation/go-u2u/core/types" + "github.com/unicornultrafoundation/go-u2u/ethclient" "github.com/unicornultrafoundation/go-u2u/evmcore" ) @@ -121,6 +121,7 @@ func StartIntegrationTestNet(directory string) (*IntegrationTestNet, error) { "--port", fmt.Sprint(netPort), "--nat", "none", "--nodiscover", + "--cache", "8192", } err := u2u.Run() if err != nil { @@ -233,7 +234,7 @@ func (n *IntegrationTestNet) GetReceipt(txHash common.Hash) (*types.Receipt, err delay := time.Millisecond for time.Since(now) < 100*time.Second { receipt, err := client.TransactionReceipt(context.Background(), txHash) - if errors.Is(err, ethereum.NotFound) { + if errors.Is(err, go_u2u.NotFound) { time.Sleep(delay) delay = 2 * delay if delay > maxDelay { diff --git a/tests/integration_test_net_test.go b/tests/integration_test_net_test.go index d4a442e6..9522c2e7 100644 --- a/tests/integration_test_net_test.go +++ b/tests/integration_test_net_test.go @@ -2,15 +2,20 @@ package tests import ( "context" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core/types" - "github.com/unicornultrafoundation/go-u2u/tests/contracts/counter" + "golang.org/x/net/nettest" "math/big" "testing" + + "github.com/unicornultrafoundation/go-u2u/common" + "github.com/unicornultrafoundation/go-u2u/core/types" + "github.com/unicornultrafoundation/go-u2u/tests/contracts/counter" ) func TestIntegrationTestNet_CanStartAndStopIntegrationTestNet(t *testing.T) { - dataDir := t.TempDir() + dataDir, err := nettest.LocalPath() + if err != nil { + t.Fatalf(err.Error()) + } net, err := StartIntegrationTestNet(dataDir) if err != nil { t.Fatalf("Failed to start the fake network: %v", err) @@ -19,7 +24,10 @@ func TestIntegrationTestNet_CanStartAndStopIntegrationTestNet(t *testing.T) { } func TestIntegrationTestNet_CanStartMultipleConsecutiveInstances(t *testing.T) { for i := 0; i < 2; i++ { - dataDir := t.TempDir() + dataDir, err := nettest.LocalPath() + if err != nil { + t.Fatalf(err.Error()) + } net, err := StartIntegrationTestNet(dataDir) if err != nil { t.Fatalf("Failed to start the fake network: %v", err) @@ -28,7 +36,10 @@ func TestIntegrationTestNet_CanStartMultipleConsecutiveInstances(t *testing.T) { } } func TestIntegrationTestNet_CanFetchInformationFromTheNetwork(t *testing.T) { - dataDir := t.TempDir() + dataDir, err := nettest.LocalPath() + if err != nil { + t.Fatalf(err.Error()) + } net, err := StartIntegrationTestNet(dataDir) if err != nil { t.Fatalf("Failed to start the fake network: %v", err) @@ -48,7 +59,10 @@ func TestIntegrationTestNet_CanFetchInformationFromTheNetwork(t *testing.T) { } } func TestIntegrationTestNet_CanEndowAccountsWithTokens(t *testing.T) { - dataDir := t.TempDir() + dataDir, err := nettest.LocalPath() + if err != nil { + t.Fatalf(err.Error()) + } net, err := StartIntegrationTestNet(dataDir) if err != nil { t.Fatalf("Failed to start the fake network: %v", err) @@ -80,7 +94,10 @@ func TestIntegrationTestNet_CanEndowAccountsWithTokens(t *testing.T) { } } func TestIntegrationTestNet_CanDeployContracts(t *testing.T) { - dataDir := t.TempDir() + dataDir, err := nettest.LocalPath() + if err != nil { + t.Fatalf(err.Error()) + } net, err := StartIntegrationTestNet(dataDir) if err != nil { t.Fatalf("Failed to start the fake network: %v", err) @@ -95,7 +112,10 @@ func TestIntegrationTestNet_CanDeployContracts(t *testing.T) { } } func TestIntegrationTestNet_CanInteractWithContract(t *testing.T) { - dataDir := t.TempDir() + dataDir, err := nettest.LocalPath() + if err != nil { + t.Fatalf(err.Error()) + } net, err := StartIntegrationTestNet(dataDir) if err != nil { t.Fatalf("Failed to start the fake network: %v", err) From 4fee5de5ac485d1a8a341ee8e40ac057e26cea6b Mon Sep 17 00:00:00 2001 From: trinhdn97 Date: Thu, 12 Dec 2024 15:55:48 +0700 Subject: [PATCH 05/15] Fix CanDeployContract unit test --- tests/contracts/counter/counter.go | 2 +- tests/integration_test_net_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/contracts/counter/counter.go b/tests/contracts/counter/counter.go index f322bf6a..c0aaf3d3 100644 --- a/tests/contracts/counter/counter.go +++ b/tests/contracts/counter/counter.go @@ -28,7 +28,7 @@ var ( // CounterMetaData contains all meta data concerning the Counter contract. var CounterMetaData = &bind.MetaData{ ABI: "[{\"inputs\":[],\"name\":\"getCount\",\"outputs\":[{\"internalType\":\"int256\",\"name\":\"\",\"type\":\"int256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"incrementCounter\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"}]", - Bin: "0x60806040525f80553480156011575f80fd5b5061017d8061001f5f395ff3fe608060405234801561000f575f80fd5b5060043610610034575f3560e01c80635b34b96614610038578063a87d942c14610042575b5f80fd5b610040610060565b005b61004a61007a565b604051610057919061009a565b60405180910390f35b60015f8082825461007191906100e0565b92505081905550565b5f8054905090565b5f819050919050565b61009481610082565b82525050565b5f6020820190506100ad5f83018461008b565b92915050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b5f6100ea82610082565b91506100f583610082565b92508282019050828112155f8312168382125f84121516171561011b5761011a6100b3565b5b9291505056fea26469706673582212202cde1411c0cd5ca1ae5cc4da29a28cd3323857e0c1c4f24109bc9c832c9f376364736f6c637828302e382e32352d646576656c6f702e323032342e322e32342b636f6d6d69742e64626137353465630059", + Bin: "0x608060405260008055348015601357600080fd5b50610164806100236000396000f3fe608060405234801561001057600080fd5b50600436106100365760003560e01c80635b34b9661461003b578063a87d942c14610045575b600080fd5b610043610063565b005b61004d61007e565b60405161005a91906100a0565b60405180910390f35b600160008082825461007591906100ea565b92505081905550565b60008054905090565b6000819050919050565b61009a81610087565b82525050565b60006020820190506100b56000830184610091565b92915050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b60006100f582610087565b915061010083610087565b925082820190508281121560008312168382126000841215161715610128576101276100bb565b5b9291505056fea26469706673582212208ba527a5db44e78fbc882dce86d304c671bdd296aec02f478cdd6b024073291964736f6c634300081a0033", } // CounterABI is the input ABI used to generate the binding from. // Deprecated: Use CounterMetaData.ABI instead. diff --git a/tests/integration_test_net_test.go b/tests/integration_test_net_test.go index 9522c2e7..07e3fa23 100644 --- a/tests/integration_test_net_test.go +++ b/tests/integration_test_net_test.go @@ -108,7 +108,7 @@ func TestIntegrationTestNet_CanDeployContracts(t *testing.T) { t.Fatalf("Failed to deploy contract: %v", err) } if receipt.Status != types.ReceiptStatusSuccessful { - t.Errorf("Contract deployment failed: %v", receipt) + t.Errorf("Contract deployment failed: %+v", receipt) } } func TestIntegrationTestNet_CanInteractWithContract(t *testing.T) { From 378caab8bc8428fdc325dfb2f4212097189efd57 Mon Sep 17 00:00:00 2001 From: trinhdn97 Date: Tue, 10 Dec 2024 16:00:02 +0700 Subject: [PATCH 06/15] Return a close-enough amount of gas instead of the exact number --- ethapi/api.go | 127 ++++++++++++++++++++++--------------- ethapi/transaction_args.go | 2 +- 2 files changed, 78 insertions(+), 51 deletions(-) diff --git a/ethapi/api.go b/ethapi/api.go index 7f686331..f4eee836 100644 --- a/ethapi/api.go +++ b/ethapi/api.go @@ -1034,11 +1034,17 @@ func DoCall(ctx context.Context, b Backend, args TransactionArgs, blockNrOrHash if state == nil || err != nil { return nil, err } + + return doCall(ctx, b, args, state, header, overrides, timeout, globalGasCap) +} + +func doCall(ctx context.Context, b Backend, args TransactionArgs, state *state.StateDB, header *evmcore.EvmHeader, + overrides *StateOverride, timeout time.Duration, globalGasCap uint64) (*evmcore.ExecutionResult, error) { if err := overrides.Apply(state); err != nil { return nil, err } // Setup context so it may be cancelled the call has completed - // or, in case of unmetered gas, setup a context with a timeout. + // or, in case of unmetered gas, set up a context with a timeout. var cancel context.CancelFunc if timeout > 0 { ctx, cancel = context.WithTimeout(ctx, timeout) @@ -1132,13 +1138,30 @@ func (s *PublicBlockChainAPI) Call(ctx context.Context, args TransactionArgs, bl return result.Return(), result.Err } -// DoEstimateGas - binary search the gas requirement, as it may be higher than the amount used -func DoEstimateGas(ctx context.Context, b Backend, args TransactionArgs, blockNrOrHash rpc.BlockNumberOrHash, gasCap uint64) (hexutil.Uint64, error) { - // Binary search the gas requirement, as it may be higher than the amount used +// executeEstimate is a helper that executes the transaction under a given gas limit and returns +// true if the transaction fails for a reason that might be related to not enough gas. A non-nil +// error means execution failed due to reasons unrelated to the gas limit. +func executeEstimate(ctx context.Context, b Backend, args TransactionArgs, state *state.StateDB, header *evmcore.EvmHeader, gasCap uint64, gasLimit uint64) (bool, *evmcore.ExecutionResult, error) { + args.Gas = (*hexutil.Uint64)(&gasLimit) + result, err := doCall(ctx, b, args, state, header, nil, 0, gasCap) + if err != nil { + if errors.Is(err, core.ErrIntrinsicGas) { + return true, nil, nil // Special case, raise gas limit + } + return true, nil, err // Bail out + } + return result.Failed(), result, nil +} + +// DoEstimateGas returns the lowest possible gas limit that allows the transaction to run +// successfully at block `blockNrOrHash`. It returns error if the transaction would revert, or if +// there are unexpected failures. The gas limit is capped by both `args.Gas` (if non-nil & +// non-zero) and `gasCap` (if non-zero). +func DoEstimateGas(ctx context.Context, b Backend, args TransactionArgs, blockNrOrHash rpc.BlockNumberOrHash, overrides *StateOverride, gasCap uint64) (hexutil.Uint64, error) { + // Binary search the gas limit, as it may need to be higher than the amount used var ( - lo uint64 = params.TxGas - 1 - hi uint64 - cap uint64 + lo uint64 // lowest-known gas limit where tx execution fails + hi uint64 // lowest-known gas limit where tx execution succeeds ) // Use zero address if sender unspecified. if args.From == nil { @@ -1161,17 +1184,22 @@ func DoEstimateGas(ctx context.Context, b Backend, args TransactionArgs, blockNr } else { feeCap = common.Big0 } + + state, header, err := b.StateAndHeaderByNumberOrHash(ctx, blockNrOrHash) + if state == nil || err != nil { + return 0, err + } + if err := overrides.Apply(state); err != nil { + return 0, err + } + // Recap the highest gas limit with account's available balance. if feeCap.BitLen() != 0 { - state, _, err := b.StateAndHeaderByNumberOrHash(ctx, blockNrOrHash) - if state == nil || err != nil { - return 0, err - } balance := state.GetBalance(*args.From) // from can't be nil available := new(big.Int).Set(balance) if args.Value != nil { if args.Value.ToInt().Cmp(available) >= 0 { - return 0, errors.New("insufficient funds for transfer") + return 0, core.ErrInsufficientFundsForTransfer } available.Sub(available, args.Value.ToInt()) } @@ -1193,30 +1221,42 @@ func DoEstimateGas(ctx context.Context, b Backend, args TransactionArgs, blockNr log.Warn("Caller gas above allowance, capping", "requested", hi, "cap", gasCap) hi = gasCap } - cap = hi - - // Create a helper to check if a gas allowance results in an executable transaction - executable := func(gas uint64) (bool, *evmcore.ExecutionResult, error) { - args.Gas = (*hexutil.Uint64)(&gas) - result, err := DoCall(ctx, b, args, blockNrOrHash, nil, 0, gasCap) - if err != nil { - if errors.Is(err, evmcore.ErrIntrinsicGas) { - return true, nil, nil // Special case, raise gas limit + // We first execute the transaction at the highest allowable gas limit, since if this fails we + // can return error immediately. + failed, result, err := executeEstimate(ctx, b, args, state.Copy(), header, gasCap, hi) + if err != nil { + return 0, err + } + if failed { + if result != nil && result.Err != vm.ErrOutOfGas { + if len(result.Revert()) > 0 { + return 0, newRevertError(result) } - return true, nil, err // Bail out + return 0, result.Err } - return result.Failed(), result, nil + return 0, fmt.Errorf("gas required exceeds allowance (%d)", hi) } - // Execute the binary search and hone in on an executable gas limit + // For almost any transaction, the gas consumed by the unconstrained execution above + // lower-bounds the gas limit required for it to succeed. One exception is those txs that + // explicitly check gas remaining in order to successfully execute within a given limit, but we + // probably don't want to return a lowest possible gas limit for these cases anyway. + lo = result.UsedGas - 1 + + // Binary search for the smallest gas limit that allows the tx to execute successfully. for lo+1 < hi { mid := (hi + lo) / 2 - failed, _, err := executable(mid) - - // If the error is not nil(consensus error), it means the provided message - // call or transaction will never be accepted no matter how much gas it is - // assigned. Return the error directly, don't struggle any more. + if mid > lo*2 { + // Most txs don't need much higher gas limit than their gas used, and most txs don't + // require near the full block limit of gas, so the selection of where to bisect the + // range here is skewed to favor the low side. + mid = lo * 2 + } + failed, _, err = executeEstimate(ctx, b, args, state.Copy(), header, gasCap, mid) if err != nil { + // This should not happen under normal conditions since if we make it this far the + // transaction had run without error at least once before. + log.Error("execution error in estimate gas", "err", err) return 0, err } if failed { @@ -1225,34 +1265,21 @@ func DoEstimateGas(ctx context.Context, b Backend, args TransactionArgs, blockNr hi = mid } } - // Reject the transaction as invalid if it still fails at the highest allowance - if hi == cap { - failed, result, err := executable(hi) - if err != nil { - return 0, err - } - if failed { - if result != nil && result.Err != vm.ErrOutOfGas { - if len(result.Revert()) > 0 { - return 0, newRevertError(result) - } - return 0, result.Err - } - // Otherwise, the specified gas cap is too low - return 0, fmt.Errorf("gas required exceeds allowance (%d)", cap) - } - } return hexutil.Uint64(hi), nil } -// EstimateGas returns an estimate of the amount of gas needed to execute the -// given transaction against the current pending block. -func (s *PublicBlockChainAPI) EstimateGas(ctx context.Context, args TransactionArgs, blockNrOrHash *rpc.BlockNumberOrHash) (hexutil.Uint64, error) { +// EstimateGas returns the lowest possible gas limit that allows the transaction to run +// successfully at block `blockNrOrHash`, or the latest block if `blockNrOrHash` is unspecified. It +// returns error if the transaction would revert or if there are unexpected failures. The returned +// value is capped by both `args.Gas` (if non-nil & non-zero) and the backend's RPCGasCap +// configuration (if non-zero). +func (s *PublicBlockChainAPI) EstimateGas(ctx context.Context, args TransactionArgs, blockNrOrHash *rpc.BlockNumberOrHash, + overrides *StateOverride) (hexutil.Uint64, error) { bNrOrHash := rpc.BlockNumberOrHashWithNumber(rpc.LatestBlockNumber) if blockNrOrHash != nil { bNrOrHash = *blockNrOrHash } - return DoEstimateGas(ctx, s.b, args, bNrOrHash, s.b.RPCGasCap()) + return DoEstimateGas(ctx, s.b, args, bNrOrHash, overrides, s.b.RPCGasCap()) } // ExecutionResult groups all structured logs emitted by the EVM diff --git a/ethapi/transaction_args.go b/ethapi/transaction_args.go index 5275afe2..eea043cb 100644 --- a/ethapi/transaction_args.go +++ b/ethapi/transaction_args.go @@ -148,7 +148,7 @@ func (args *TransactionArgs) setDefaults(ctx context.Context, b Backend) error { AccessList: args.AccessList, } pendingBlockNr := rpc.BlockNumberOrHashWithNumber(rpc.PendingBlockNumber) - estimated, err := DoEstimateGas(ctx, b, callArgs, pendingBlockNr, b.RPCGasCap()) + estimated, err := DoEstimateGas(ctx, b, callArgs, pendingBlockNr, nil, b.RPCGasCap()) if err != nil { return err } From 4d009684d651e7238f8850a33d229b060757c74f Mon Sep 17 00:00:00 2001 From: trinhdn97 Date: Tue, 10 Dec 2024 16:04:47 +0700 Subject: [PATCH 07/15] Use go-u2u/evmcore --- ethapi/api.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/ethapi/api.go b/ethapi/api.go index f4eee836..a2dc8d9a 100644 --- a/ethapi/api.go +++ b/ethapi/api.go @@ -30,7 +30,6 @@ import ( "github.com/davecgh/go-spew/spew" "github.com/syndtr/goleveldb/leveldb/opt" - "github.com/unicornultrafoundation/go-helios/hash" "github.com/unicornultrafoundation/go-helios/native/idx" @@ -1145,7 +1144,7 @@ func executeEstimate(ctx context.Context, b Backend, args TransactionArgs, state args.Gas = (*hexutil.Uint64)(&gasLimit) result, err := doCall(ctx, b, args, state, header, nil, 0, gasCap) if err != nil { - if errors.Is(err, core.ErrIntrinsicGas) { + if errors.Is(err, evmcore.ErrIntrinsicGas) { return true, nil, nil // Special case, raise gas limit } return true, nil, err // Bail out @@ -1199,7 +1198,7 @@ func DoEstimateGas(ctx context.Context, b Backend, args TransactionArgs, blockNr available := new(big.Int).Set(balance) if args.Value != nil { if args.Value.ToInt().Cmp(available) >= 0 { - return 0, core.ErrInsufficientFundsForTransfer + return 0, evmcore.ErrInsufficientFundsForTransfer } available.Sub(available, args.Value.ToInt()) } From 86b1799b50db50e848ed6e490789e4dd75a579f6 Mon Sep 17 00:00:00 2001 From: trinhdn97 Date: Wed, 11 Dec 2024 18:25:33 +0700 Subject: [PATCH 08/15] Less iterations of eth_estimateGas --- core/types/transaction.go | 23 +-- eth/gasestimator/gasestimator.go | 240 +++++++++++++++++++++++++++++++ ethapi/api.go | 142 +++++------------- ethapi/transaction_args.go | 58 +++++++- evmcore/state_transition.go | 25 ++-- 5 files changed, 358 insertions(+), 130 deletions(-) create mode 100644 eth/gasestimator/gasestimator.go diff --git a/core/types/transaction.go b/core/types/transaction.go index f58baa92..f32e76d8 100644 --- a/core/types/transaction.go +++ b/core/types/transaction.go @@ -638,14 +638,15 @@ func (tx *Transaction) AsMessage(s Signer, baseFee *big.Int) (Message, error) { return msg, err } -func (m Message) From() common.Address { return m.from } -func (m Message) To() *common.Address { return m.to } -func (m Message) GasPrice() *big.Int { return m.gasPrice } -func (m Message) GasFeeCap() *big.Int { return m.gasFeeCap } -func (m Message) GasTipCap() *big.Int { return m.gasTipCap } -func (m Message) Value() *big.Int { return m.amount } -func (m Message) Gas() uint64 { return m.gasLimit } -func (m Message) Nonce() uint64 { return m.nonce } -func (m Message) Data() []byte { return m.data } -func (m Message) AccessList() AccessList { return m.accessList } -func (m Message) IsFake() bool { return m.isFake } +func (m Message) From() common.Address { return m.from } +func (m Message) To() *common.Address { return m.to } +func (m Message) GasPrice() *big.Int { return m.gasPrice } +func (m Message) GasFeeCap() *big.Int { return m.gasFeeCap } +func (m Message) GasTipCap() *big.Int { return m.gasTipCap } +func (m Message) Value() *big.Int { return m.amount } +func (m Message) Gas() uint64 { return m.gasLimit } +func (m Message) Nonce() uint64 { return m.nonce } +func (m Message) Data() []byte { return m.data } +func (m Message) AccessList() AccessList { return m.accessList } +func (m Message) IsFake() bool { return m.isFake } +func (m Message) SetGasLimit(gasLimit uint64) { m.gasLimit = gasLimit } diff --git a/eth/gasestimator/gasestimator.go b/eth/gasestimator/gasestimator.go new file mode 100644 index 00000000..5d2aae49 --- /dev/null +++ b/eth/gasestimator/gasestimator.go @@ -0,0 +1,240 @@ +// Copyright 2023 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package gasestimator + +import ( + "context" + "errors" + "fmt" + "math" + "math/big" + + "github.com/unicornultrafoundation/go-u2u/common" + "github.com/unicornultrafoundation/go-u2u/core/state" + "github.com/unicornultrafoundation/go-u2u/core/types" + "github.com/unicornultrafoundation/go-u2u/core/vm" + "github.com/unicornultrafoundation/go-u2u/evmcore" + "github.com/unicornultrafoundation/go-u2u/log" + "github.com/unicornultrafoundation/go-u2u/params" +) + +// Options are the contextual parameters to execute the requested call. +// +// Whilst it would be possible to pass a blockchain object that aggregates all +// these together, it would be excessively hard to test. Splitting the parts out +// allows testing without needing a proper live chain. +type Options struct { + Config *params.ChainConfig // Chain configuration for hard fork selection + Chain evmcore.DummyChain // Chain context to access past block hashes + Header *evmcore.EvmHeader // Header defining the block context to execute in + State *state.StateDB // Pre-state on top of which to estimate the gas + + ErrorRatio float64 // Allowed an overestimation ratio for faster estimation termination +} + +// Estimate returns the lowest possible gas limit that allows the transaction to +// run successfully with the provided context options. It returns an error if the +// transaction always reverts, or if there are unexpected failures. +func Estimate(ctx context.Context, call *types.Message, opts *Options, gasCap uint64) (uint64, []byte, error) { + // Binary search the gas limit, as it may need to be higher than the amount used + var ( + lo uint64 // lowest-known gas limit where tx execution fails + hi uint64 // lowest-known gas limit where tx execution succeeds + ) + // Determine the highest gas limit can be used during the estimation. + hi = opts.Header.GasLimit + if call.Gas() >= params.TxGas { + hi = call.Gas() + } + // Normalize the max fee per gas the call is willing to spend. + var feeCap *big.Int + if call.GasFeeCap() != nil { + feeCap = call.GasFeeCap() + } else if call.GasPrice() != nil { + feeCap = call.GasPrice() + } else { + feeCap = common.Big0 + } + // Recap the highest gas limit with account's available balance. + if feeCap.BitLen() != 0 { + balance := opts.State.GetBalance(call.From()) + + available := balance + if call.Value() != nil { + if call.Value().Cmp(available) >= 0 { + return 0, nil, evmcore.ErrInsufficientFundsForTransfer + } + available.Sub(available, call.Value()) + } + allowance := new(big.Int).Div(available, feeCap) + + // If the allowance is larger than maximum uint64, skip checking + if allowance.IsUint64() && hi > allowance.Uint64() { + transfer := call.Value() + if transfer == nil { + transfer = new(big.Int) + } + log.Debug("Gas estimation capped by limited funds", "original", hi, "balance", balance, + "sent", transfer, "maxFeePerGas", feeCap, "fundable", allowance) + hi = allowance.Uint64() + } + } + // Recap the highest gas allowance with specified gas cap. + if gasCap != 0 && hi > gasCap { + log.Debug("Caller gas above allowance, capping", "requested", hi, "cap", gasCap) + hi = gasCap + } + // If the transaction is a plain value transfer, short circuit estimation and + // directly try 21000. Returning 21000 without any execution is dangerous as + // some tx field combos might bump the price up even for plain transfers (e.g. + // unused access list items). Ever so slightly wasteful, but safer overall. + if len(call.Data()) == 0 { + if call.To() != nil && opts.State.GetCodeSize(*call.To()) == 0 { + failed, _, err := execute(ctx, call, opts, params.TxGas) + if !failed && err == nil { + return params.TxGas, nil, nil + } + } + } + // We first execute the transaction at the highest allowable gas limit, since if this fails we + // can return error immediately. + failed, result, err := execute(ctx, call, opts, hi) + if err != nil { + return 0, nil, err + } + if failed { + if result != nil && !errors.Is(result.Err, vm.ErrOutOfGas) { + return 0, result.Revert(), result.Err + } + return 0, nil, fmt.Errorf("gas required exceeds allowance (%d)", hi) + } + // For almost any transaction, the gas consumed by the unconstrained execution + // above lower-bounds the gas limit required for it to succeed. One exception + // is those that explicitly check gas remaining in order to execute within a + // given limit, but we probably don't want to return the lowest possible gas + // limit for these cases anyway. + lo = result.UsedGas - 1 + + // There's a fairly high chance for the transaction to execute successfully + // with gasLimit set to the first execution's usedGas + gasRefund. Explicitly + // check that gas amount and use as a limit for the binary search. + optimisticGasLimit := (result.UsedGas + result.RefundedGas + params.CallStipend) * 64 / 63 + if optimisticGasLimit < hi { + failed, _, err = execute(ctx, call, opts, optimisticGasLimit) + if err != nil { + // This should not happen under normal conditions since if we make it this far the + // transaction had run without an error at least once before. + log.Error("Execution error in estimate gas", "err", err) + return 0, nil, err + } + if failed { + lo = optimisticGasLimit + } else { + hi = optimisticGasLimit + } + } + // Binary search for the smallest gas limit that allows the tx to execute successfully. + for lo+1 < hi { + if opts.ErrorRatio > 0 { + // It is a bit pointless to return a perfect estimation, as changing + // network conditions require the caller to bump it up anyway. Since + // wallets tend to use 20-25% bump, allowing a small approximation + // error is fine (as long as it's upwards). + if float64(hi-lo)/float64(hi) < opts.ErrorRatio { + break + } + } + mid := (hi + lo) / 2 + if mid > lo*2 { + // Most txs don't need that much higher gas limit than their gas used. Most txs don't + // require near the full block limit of gas, so the selection of where to bisect the + // range here is skewed to favor the low side. + mid = lo * 2 + } + failed, _, err = execute(ctx, call, opts, mid) + if err != nil { + // This should not happen under normal conditions since if we make it this far the + // transaction had run without an error at least once before. + log.Error("Execution error in estimate gas", "err", err) + return 0, nil, err + } + if failed { + lo = mid + } else { + hi = mid + } + } + return hi, nil, nil +} + +// execute is a helper that executes the transaction under a given gas limit and +// returns true if the transaction fails for a reason that might be related to +// not enough gas. A non-nil error means execution failed due to reasons unrelated +// to the gas limit. +func execute(ctx context.Context, call *types.Message, opts *Options, gasLimit uint64) (bool, *evmcore.ExecutionResult, error) { + // Configure the call for this specific execution (and revert the change after) + defer func(gas uint64) { call.SetGasLimit(gas) }(call.Gas()) + call.SetGasLimit(gasLimit) + + // Execute the call and separate execution faults caused by a lack of gas or + // other non-fixable conditions + result, err := run(ctx, call, opts) + if err != nil { + if errors.Is(err, evmcore.ErrIntrinsicGas) { + return true, nil, nil // Special case, raise gas limit + } + return true, nil, err // Bail out + } + return result.Failed(), result, nil +} + +// run assembles the EVM as defined by the consensus rules and runs the requested +// call invocation. +func run(ctx context.Context, call *types.Message, opts *Options) (*evmcore.ExecutionResult, error) { + // Assemble the call and the call context + var ( + msgContext = evmcore.NewEVMTxContext(call) + evmContext = evmcore.NewEVMBlockContext(opts.Header, opts.Chain, nil) + + dirtyState = opts.State.Copy() + ) + // Lower the base fee to 0 to avoid breaking EVM + // invariants (basefee < feecap). + if msgContext.GasPrice.Sign() == 0 { + evmContext.BaseFee = new(big.Int) + } + evm := vm.NewEVM(evmContext, msgContext, dirtyState, opts.Config, vm.Config{NoBaseFee: true}) + // Monitor the outer context and interrupt the EVM upon cancellation. To avoid + // a dangling goroutine until the outer estimation finishes, create an internal + // context for the lifetime of this method call. + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + go func() { + <-ctx.Done() + evm.Cancel() + }() + // Execute the call, returning a wrapped error or the result + result, err := evmcore.ApplyMessage(evm, call, new(evmcore.GasPool).AddGas(math.MaxUint64)) + if vmerr := dirtyState.Error(); vmerr != nil { + return nil, vmerr + } + if err != nil { + return result, fmt.Errorf("failed with %d gas: %w", call.Gas(), err) + } + return result, nil +} diff --git a/ethapi/api.go b/ethapi/api.go index a2dc8d9a..31482472 100644 --- a/ethapi/api.go +++ b/ethapi/api.go @@ -43,6 +43,7 @@ import ( "github.com/unicornultrafoundation/go-u2u/core/types" "github.com/unicornultrafoundation/go-u2u/core/vm" "github.com/unicornultrafoundation/go-u2u/crypto" + "github.com/unicornultrafoundation/go-u2u/eth/gasestimator" "github.com/unicornultrafoundation/go-u2u/eth/tracers" "github.com/unicornultrafoundation/go-u2u/evmcore" "github.com/unicornultrafoundation/go-u2u/gossip/gasprice" @@ -68,6 +69,10 @@ const ( // and reexecute to produce missing historical state necessary to run a specific // trace. defaultTraceReexec = uint64(128) + + // estimateGasErrorRatio is the amount of overestimation eth_estimateGas is + // allowed to produce in order to speed up calculations. + estimateGasErrorRatio = 0.015 ) var ( @@ -1089,15 +1094,17 @@ func doCall(ctx context.Context, b Backend, args TransactionArgs, state *state.S return result, nil } -func newRevertError(result *evmcore.ExecutionResult) *revertError { - reason, errUnpack := abi.UnpackRevert(result.Revert()) - err := errors.New("execution reverted") +// newRevertError creates a revertError instance with the provided revert data. +func newRevertError(revert []byte) *revertError { + err := vm.ErrExecutionReverted + + reason, errUnpack := abi.UnpackRevert(revert) if errUnpack == nil { - err = fmt.Errorf("execution reverted: %v", reason) + err = fmt.Errorf("%w: %v", vm.ErrExecutionReverted, reason) } return &revertError{ error: err, - reason: hexutil.Encode(result.Revert()), + reason: hexutil.Encode(revert), } } @@ -1132,7 +1139,7 @@ func (s *PublicBlockChainAPI) Call(ctx context.Context, args TransactionArgs, bl } // If the result contains a revert reason, try to unpack and return it. if len(result.Revert()) > 0 { - return nil, newRevertError(result) + return nil, newRevertError(result.Revert()) } return result.Return(), result.Err } @@ -1153,37 +1160,13 @@ func executeEstimate(ctx context.Context, b Backend, args TransactionArgs, state } // DoEstimateGas returns the lowest possible gas limit that allows the transaction to run -// successfully at block `blockNrOrHash`. It returns error if the transaction would revert, or if -// there are unexpected failures. The gas limit is capped by both `args.Gas` (if non-nil & +// successfully at block `blockNrOrHash`. +// It returns error if the transaction reverts, or if +// there are unexpected failures. +// The gas limit is capped by both `args.Gas` (if non-nil & // non-zero) and `gasCap` (if non-zero). func DoEstimateGas(ctx context.Context, b Backend, args TransactionArgs, blockNrOrHash rpc.BlockNumberOrHash, overrides *StateOverride, gasCap uint64) (hexutil.Uint64, error) { - // Binary search the gas limit, as it may need to be higher than the amount used - var ( - lo uint64 // lowest-known gas limit where tx execution fails - hi uint64 // lowest-known gas limit where tx execution succeeds - ) - // Use zero address if sender unspecified. - if args.From == nil { - args.From = new(common.Address) - } - // Determine the highest gas limit can be used during the estimation. - if args.Gas != nil && uint64(*args.Gas) >= params.TxGas { - hi = uint64(*args.Gas) - } else { - hi = b.MaxGasLimit() - } - // Normalize the max fee per gas the call is willing to spend. - var feeCap *big.Int - if args.GasPrice != nil && (args.MaxFeePerGas != nil || args.MaxPriorityFeePerGas != nil) { - return 0, errors.New("both gasPrice and (maxFeePerGas or maxPriorityFeePerGas) specified") - } else if args.GasPrice != nil { - feeCap = args.GasPrice.ToInt() - } else if args.MaxFeePerGas != nil { - feeCap = args.MaxFeePerGas.ToInt() - } else { - feeCap = common.Big0 - } - + // Retrieve the base state and mutate it with any overrides state, header, err := b.StateAndHeaderByNumberOrHash(ctx, blockNrOrHash) if state == nil || err != nil { return 0, err @@ -1191,80 +1174,33 @@ func DoEstimateGas(ctx context.Context, b Backend, args TransactionArgs, blockNr if err := overrides.Apply(state); err != nil { return 0, err } - - // Recap the highest gas limit with account's available balance. - if feeCap.BitLen() != 0 { - balance := state.GetBalance(*args.From) // from can't be nil - available := new(big.Int).Set(balance) - if args.Value != nil { - if args.Value.ToInt().Cmp(available) >= 0 { - return 0, evmcore.ErrInsufficientFundsForTransfer - } - available.Sub(available, args.Value.ToInt()) - } - allowance := new(big.Int).Div(available, feeCap) - - // If the allowance is larger than maximum uint64, skip checking - if allowance.IsUint64() && hi > allowance.Uint64() { - transfer := args.Value - if transfer == nil { - transfer = new(hexutil.Big) - } - log.Warn("Gas estimation capped by limited funds", "original", hi, "balance", balance, - "sent", transfer.ToInt(), "maxFeePerGas", feeCap, "fundable", allowance) - hi = allowance.Uint64() - } + // Construct the gas estimator option from the user input + opts := &gasestimator.Options{ + Config: b.ChainConfig(), + Chain: NewChainContext(ctx, b), + Header: header, + State: state, + ErrorRatio: estimateGasErrorRatio, } - // Recap the highest gas allowance with specified gascap. - if gasCap != 0 && hi > gasCap { - log.Warn("Caller gas above allowance, capping", "requested", hi, "cap", gasCap) - hi = gasCap + // Set any required transaction default, but make sure the gas cap itself is not messed with + // if it was not specified in the original argument list. + if args.Gas == nil { + args.Gas = new(hexutil.Uint64) } - - // We first execute the transaction at the highest allowable gas limit, since if this fails we - // can return error immediately. - failed, result, err := executeEstimate(ctx, b, args, state.Copy(), header, gasCap, hi) - if err != nil { + if err := args.CallDefaults(gasCap, header.BaseFee, b.ChainConfig().ChainID); err != nil { return 0, err } - if failed { - if result != nil && result.Err != vm.ErrOutOfGas { - if len(result.Revert()) > 0 { - return 0, newRevertError(result) - } - return 0, result.Err - } - return 0, fmt.Errorf("gas required exceeds allowance (%d)", hi) - } - // For almost any transaction, the gas consumed by the unconstrained execution above - // lower-bounds the gas limit required for it to succeed. One exception is those txs that - // explicitly check gas remaining in order to successfully execute within a given limit, but we - // probably don't want to return a lowest possible gas limit for these cases anyway. - lo = result.UsedGas - 1 - - // Binary search for the smallest gas limit that allows the tx to execute successfully. - for lo+1 < hi { - mid := (hi + lo) / 2 - if mid > lo*2 { - // Most txs don't need much higher gas limit than their gas used, and most txs don't - // require near the full block limit of gas, so the selection of where to bisect the - // range here is skewed to favor the low side. - mid = lo * 2 - } - failed, _, err = executeEstimate(ctx, b, args, state.Copy(), header, gasCap, mid) - if err != nil { - // This should not happen under normal conditions since if we make it this far the - // transaction had run without error at least once before. - log.Error("execution error in estimate gas", "err", err) - return 0, err - } - if failed { - lo = mid - } else { - hi = mid + call, _ := args.ToMessage(gasCap, header.BaseFee) + + // Run the gas estimation and wrap any revert reason into a custom return + estimate, revert, err := gasestimator.Estimate(ctx, &call, opts, gasCap) + if err != nil { + if len(revert) > 0 { + return 0, newRevertError(revert) } + return 0, err } - return hexutil.Uint64(hi), nil + return hexutil.Uint64(estimate), nil } // EstimateGas returns the lowest possible gas limit that allows the transaction to run diff --git a/ethapi/transaction_args.go b/ethapi/transaction_args.go index eea043cb..bd2e6d91 100644 --- a/ethapi/transaction_args.go +++ b/ethapi/transaction_args.go @@ -82,7 +82,7 @@ func (args *TransactionArgs) setDefaults(ctx context.Context, b Backend) error { } // After london, default to 1559 unless gasPrice is set head := b.CurrentBlock().Header() - // If user specifies both maxPriorityfee and maxFee, then we do not + // If user specifies both maxPriorityFee and maxFee, then we do not // need to consult the chain for defaults. It's definitely a London tx. if args.MaxPriorityFeePerGas == nil || args.MaxFeePerGas == nil { // In this clause, user left some fields unspecified. @@ -162,14 +162,60 @@ func (args *TransactionArgs) setDefaults(ctx context.Context, b Backend) error { return nil } +// CallDefaults sanitizes the transaction arguments, often filling in zero values, +// for the purpose of eth_call class of RPC methods. +func (args *TransactionArgs) CallDefaults(globalGasCap uint64, baseFee *big.Int, chainID *big.Int) error { + // Reject invalid combinations of pre- and post-1559 fee styles + if args.GasPrice != nil && (args.MaxFeePerGas != nil || args.MaxPriorityFeePerGas != nil) { + return errors.New("both gasPrice and (maxFeePerGas or maxPriorityFeePerGas) specified") + } + if args.ChainID == nil { + args.ChainID = (*hexutil.Big)(chainID) + } else { + if have := (*big.Int)(args.ChainID); have.Cmp(chainID) != 0 { + return fmt.Errorf("chainId does not match node's (have=%v, want=%v)", have, chainID) + } + } + if args.Gas == nil { + gas := globalGasCap + if gas == 0 { + gas = uint64(math.MaxUint64 / 2) + } + args.Gas = (*hexutil.Uint64)(&gas) + } else { + if globalGasCap > 0 && globalGasCap < uint64(*args.Gas) { + log.Warn("Caller gas above allowance, capping", "requested", args.Gas, "cap", globalGasCap) + args.Gas = (*hexutil.Uint64)(&globalGasCap) + } + } + if args.Nonce == nil { + args.Nonce = new(hexutil.Uint64) + } + if args.Value == nil { + args.Value = new(hexutil.Big) + } + if baseFee == nil { + // If there's no basefee, then it must be a non-1559 execution + if args.GasPrice == nil { + args.GasPrice = new(hexutil.Big) + } + } else { + // A basefee is provided, requiring 1559-type execution + if args.MaxFeePerGas == nil { + args.MaxFeePerGas = new(hexutil.Big) + } + if args.MaxPriorityFeePerGas == nil { + args.MaxPriorityFeePerGas = new(hexutil.Big) + } + } + + return nil +} + // ToMessage converts the transaction arguments to the Message type used by the // core evm. This method is used in calls and traces that do not require a real // live transaction. func (args *TransactionArgs) ToMessage(globalGasCap uint64, baseFee *big.Int) (types.Message, error) { - // Reject invalid combinations of pre- and post-1559 fee styles - if args.GasPrice != nil && (args.MaxFeePerGas != nil || args.MaxPriorityFeePerGas != nil) { - return types.Message{}, errors.New("both gasPrice and (maxFeePerGas or maxPriorityFeePerGas) specified") - } // Set sender address or use zero address if none specified. addr := args.from() @@ -198,7 +244,7 @@ func (args *TransactionArgs) ToMessage(globalGasCap uint64, baseFee *big.Int) (t } gasFeeCap, gasTipCap = gasPrice, gasPrice } else { - // A basefee is provided, necessitating 1559-type execution + // A basefee is provided, requiring 1559-type execution if args.GasPrice != nil { // User specified the legacy gas field, convert to 1559 gas typing gasPrice = args.GasPrice.ToInt() diff --git a/evmcore/state_transition.go b/evmcore/state_transition.go index 1e6dabce..2fba64ec 100644 --- a/evmcore/state_transition.go +++ b/evmcore/state_transition.go @@ -79,12 +79,13 @@ type Message interface { AccessList() types.AccessList } -// ExecutionResult includes all output after executing given evm +// ExecutionResult includes all output after executing given an evm // message no matter the execution itself is successful or not. type ExecutionResult struct { - UsedGas uint64 // Total used gas but include the refunded gas - Err error // Any error encountered during the execution(listed in core/vm/errors.go) - ReturnData []byte // Returned data from evm(function result or data supplied with revert opcode) + UsedGas uint64 // Total used gas but include the refunded gas + RefundedGas uint64 // Total gas refunded after execution + Err error // Any error encountered during the execution (listed in core/vm/errors.go) + ReturnData []byte // Returned data from evm(function result or data supplied with revert opcode) } // Unwrap returns the internal evm error which allows us for further @@ -298,22 +299,24 @@ func (st *StateTransition) TransitionDb() (*ExecutionResult, error) { st.gas -= st.gas / 10 } + var gasRefund uint64 if !london { // Before EIP-3529: refunds were capped to gasUsed / 2 - st.refundGas(params.RefundQuotient) + gasRefund = st.refundGas(params.RefundQuotient) } else { // After EIP-3529: refunds are capped to gasUsed / 5 - st.refundGas(params.RefundQuotientEIP3529) + gasRefund = st.refundGas(params.RefundQuotientEIP3529) } return &ExecutionResult{ - UsedGas: st.gasUsed(), - Err: vmerr, - ReturnData: ret, + UsedGas: st.gasUsed(), + RefundedGas: gasRefund, + Err: vmerr, + ReturnData: ret, }, nil } -func (st *StateTransition) refundGas(refundQuotient uint64) { +func (st *StateTransition) refundGas(refundQuotient uint64) uint64 { // Apply refund counter, capped to a refund quotient refund := st.gasUsed() / refundQuotient if refund > st.state.GetRefund() { @@ -328,6 +331,8 @@ func (st *StateTransition) refundGas(refundQuotient uint64) { // Also return remaining gas to the block gas counter so it is // available for the next transaction. st.gp.AddGas(st.gas) + + return refund } // gasUsed returns the amount of gas used up by the state transition. From b73b844570b4c587c41706743c52de30387bd825 Mon Sep 17 00:00:00 2001 From: trinhdn97 Date: Wed, 11 Dec 2024 19:00:47 +0700 Subject: [PATCH 09/15] Fix unit test --- cmd/u2u/launcher/testdata/txtracer_test.js | 2 +- cmd/u2u/launcher/txtracer_test.go | 4 ++-- ethapi/api.go | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cmd/u2u/launcher/testdata/txtracer_test.js b/cmd/u2u/launcher/testdata/txtracer_test.js index ae9b6f41..b53d15ad 100644 --- a/cmd/u2u/launcher/testdata/txtracer_test.js +++ b/cmd/u2u/launcher/testdata/txtracer_test.js @@ -1,7 +1,7 @@ var a1 = personal.newAccount(""); personal.unlockAccount(a1, "", 300); admin.sleep(1); -var tx = u2u.sendTransaction({from:u2u.accounts[0], to:a1, value:"10000000000000000000"}); +var tx = u2u.sendTransaction({from:u2u.accounts[0], to:a1, value:"10000000000000000000", gas:"4700000"}); admin.sleep(1); var abi = [{"inputs":[],"name":"deploy","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"getA","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_a","type":"uint256"}],"name":"setA","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_a","type":"uint256"}],"name":"setInA","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"tst","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"}]; var bytecode = "0x608060405234801561001057600080fd5b50610589806100206000396000f3fe608060405234801561001057600080fd5b50600436106100575760003560e01c8063775c300c1461005c57806391888f2e146100a6578063d46300fd146100f0578063ea87f3731461010e578063ee919d501461013c575b600080fd5b61006461016a565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b6100ae61032a565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b6100f8610350565b6040518082815260200191505060405180910390f35b61013a6004803603602081101561012457600080fd5b8101908080359060200190929190505050610359565b005b6101686004803603602081101561015257600080fd5b81019080803590602001909291905050506103ef565b005b6000807f746573740000000000000000000000000000000000000000000000000000000060001b607b60405161019f906103f9565b808281526020019150508190604051809103906000f59050801580156101c9573d6000803e3d6000fd5b5090508073ffffffffffffffffffffffffffffffffffffffff1663d46300fd6040518163ffffffff1660e01b815260040160206040518083038186803b15801561021257600080fd5b505afa158015610226573d6000803e3d6000fd5b505050506040513d602081101561023c57600080fd5b81019080805190602001909291905050506000819055508073ffffffffffffffffffffffffffffffffffffffff1663ee919d506101416040518263ffffffff1660e01b815260040180828152602001915050600060405180830381600087803b1580156102a857600080fd5b505af11580156102bc573d6000803e3d6000fd5b5050505080600160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff160217905550600160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1691505090565b600160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b60008054905090565b6000600160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1690508073ffffffffffffffffffffffffffffffffffffffff1663ee919d50836040518263ffffffff1660e01b815260040180828152602001915050600060405180830381600087803b1580156103d357600080fd5b505af11580156103e7573d6000803e3d6000fd5b505050505050565b8060008190555050565b61014d806104078339019056fe608060405234801561001057600080fd5b5060405161014d38038061014d8339818101604052602081101561003357600080fd5b8101908080519060200190929190505050806000819055505060f38061005a6000396000f3fe6080604052348015600f57600080fd5b5060043610603c5760003560e01c80630dbe671f146041578063d46300fd14605d578063ee919d50146079575b600080fd5b604760a4565b6040518082815260200191505060405180910390f35b606360aa565b6040518082815260200191505060405180910390f35b60a260048036036020811015608d57600080fd5b810190808035906020019092919050505060b3565b005b60005481565b60008054905090565b806000819055505056fea264697066735822122084a43fd050d3de9bc0cdc2f86b02db81bc69b5639cedd1a5b72010d2a664879a64736f6c63430006020033a2646970667358221220a00266288222dc5d9803c3840f0e4d5d04630cb6bac841d30c42511d9b58045864736f6c63430006020033"; diff --git a/cmd/u2u/launcher/txtracer_test.go b/cmd/u2u/launcher/txtracer_test.go index 241ff003..0f40984b 100644 --- a/cmd/u2u/launcher/txtracer_test.go +++ b/cmd/u2u/launcher/txtracer_test.go @@ -50,11 +50,11 @@ func TestTxTracing(t *testing.T) { cliConsoleOutput = *cliConsole.GetOutDataTillCursor() // Call simple contract call to check created trace - cliConsole.InputLine("testContract.setA.sendTransaction(24, {from:u2u.accounts[1]})") + cliConsole.InputLine("testContract.setA.sendTransaction(24, {from:u2u.accounts[1],gas:\"3000000\"})") cliConsoleOutput = *cliConsole.GetOutDataTillCursor() txHashCall := cliConsoleOutput[strings.Index(cliConsoleOutput, "0x") : len(cliConsoleOutput)-3] - cliConsole.InputLine("testContract.deploy.sendTransaction({from:u2u.accounts[1]})") + cliConsole.InputLine("testContract.deploy.sendTransaction({from:u2u.accounts[1],gas:\"3000000\"})") cliConsoleOutput = *cliConsole.GetOutDataTillCursor() txHashDeploy := cliConsoleOutput[strings.Index(cliConsoleOutput, "0x") : len(cliConsoleOutput)-3] time.Sleep(5000 * time.Millisecond) diff --git a/ethapi/api.go b/ethapi/api.go index 31482472..48d0a7e4 100644 --- a/ethapi/api.go +++ b/ethapi/api.go @@ -1832,7 +1832,7 @@ func (s *PublicTransactionPoolAPI) SendTransaction(ctx context.Context, args Tra } if args.Nonce == nil { - // Hold the addresse's mutex around signing to prevent concurrent assignment of + // Hold the addresses' mutex around signing to prevent concurrent assignment of // the same nonce to multiple accounts. s.nonceLock.LockAddr(args.from()) defer s.nonceLock.UnlockAddr(args.from()) From 1c0090429778d29cafd19fa79ad0ab6f25bc04d8 Mon Sep 17 00:00:00 2001 From: trinhdn97 Date: Wed, 11 Dec 2024 20:05:28 +0700 Subject: [PATCH 10/15] Fix gasLimit setter of types.Message --- core/types/transaction.go | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/core/types/transaction.go b/core/types/transaction.go index f32e76d8..5c86f987 100644 --- a/core/types/transaction.go +++ b/core/types/transaction.go @@ -638,15 +638,15 @@ func (tx *Transaction) AsMessage(s Signer, baseFee *big.Int) (Message, error) { return msg, err } -func (m Message) From() common.Address { return m.from } -func (m Message) To() *common.Address { return m.to } -func (m Message) GasPrice() *big.Int { return m.gasPrice } -func (m Message) GasFeeCap() *big.Int { return m.gasFeeCap } -func (m Message) GasTipCap() *big.Int { return m.gasTipCap } -func (m Message) Value() *big.Int { return m.amount } -func (m Message) Gas() uint64 { return m.gasLimit } -func (m Message) Nonce() uint64 { return m.nonce } -func (m Message) Data() []byte { return m.data } -func (m Message) AccessList() AccessList { return m.accessList } -func (m Message) IsFake() bool { return m.isFake } -func (m Message) SetGasLimit(gasLimit uint64) { m.gasLimit = gasLimit } +func (m Message) From() common.Address { return m.from } +func (m Message) To() *common.Address { return m.to } +func (m Message) GasPrice() *big.Int { return m.gasPrice } +func (m Message) GasFeeCap() *big.Int { return m.gasFeeCap } +func (m Message) GasTipCap() *big.Int { return m.gasTipCap } +func (m Message) Value() *big.Int { return m.amount } +func (m Message) Gas() uint64 { return m.gasLimit } +func (m Message) Nonce() uint64 { return m.nonce } +func (m Message) Data() []byte { return m.data } +func (m Message) AccessList() AccessList { return m.accessList } +func (m Message) IsFake() bool { return m.isFake } +func (m *Message) SetGasLimit(gasLimit uint64) { m.gasLimit = gasLimit } From 3be1f019d7015afebb089cb29a5ca141a9c25d3d Mon Sep 17 00:00:00 2001 From: trinhdn97 Date: Wed, 11 Dec 2024 20:09:12 +0700 Subject: [PATCH 11/15] Revert tx tracer unit test --- cmd/u2u/launcher/testdata/txtracer_test.js | 2 +- cmd/u2u/launcher/txtracer_test.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cmd/u2u/launcher/testdata/txtracer_test.js b/cmd/u2u/launcher/testdata/txtracer_test.js index b53d15ad..ae9b6f41 100644 --- a/cmd/u2u/launcher/testdata/txtracer_test.js +++ b/cmd/u2u/launcher/testdata/txtracer_test.js @@ -1,7 +1,7 @@ var a1 = personal.newAccount(""); personal.unlockAccount(a1, "", 300); admin.sleep(1); -var tx = u2u.sendTransaction({from:u2u.accounts[0], to:a1, value:"10000000000000000000", gas:"4700000"}); +var tx = u2u.sendTransaction({from:u2u.accounts[0], to:a1, value:"10000000000000000000"}); admin.sleep(1); var abi = [{"inputs":[],"name":"deploy","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"getA","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_a","type":"uint256"}],"name":"setA","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_a","type":"uint256"}],"name":"setInA","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"tst","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"}]; var bytecode = "0x608060405234801561001057600080fd5b50610589806100206000396000f3fe608060405234801561001057600080fd5b50600436106100575760003560e01c8063775c300c1461005c57806391888f2e146100a6578063d46300fd146100f0578063ea87f3731461010e578063ee919d501461013c575b600080fd5b61006461016a565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b6100ae61032a565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b6100f8610350565b6040518082815260200191505060405180910390f35b61013a6004803603602081101561012457600080fd5b8101908080359060200190929190505050610359565b005b6101686004803603602081101561015257600080fd5b81019080803590602001909291905050506103ef565b005b6000807f746573740000000000000000000000000000000000000000000000000000000060001b607b60405161019f906103f9565b808281526020019150508190604051809103906000f59050801580156101c9573d6000803e3d6000fd5b5090508073ffffffffffffffffffffffffffffffffffffffff1663d46300fd6040518163ffffffff1660e01b815260040160206040518083038186803b15801561021257600080fd5b505afa158015610226573d6000803e3d6000fd5b505050506040513d602081101561023c57600080fd5b81019080805190602001909291905050506000819055508073ffffffffffffffffffffffffffffffffffffffff1663ee919d506101416040518263ffffffff1660e01b815260040180828152602001915050600060405180830381600087803b1580156102a857600080fd5b505af11580156102bc573d6000803e3d6000fd5b5050505080600160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff160217905550600160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1691505090565b600160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b60008054905090565b6000600160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1690508073ffffffffffffffffffffffffffffffffffffffff1663ee919d50836040518263ffffffff1660e01b815260040180828152602001915050600060405180830381600087803b1580156103d357600080fd5b505af11580156103e7573d6000803e3d6000fd5b505050505050565b8060008190555050565b61014d806104078339019056fe608060405234801561001057600080fd5b5060405161014d38038061014d8339818101604052602081101561003357600080fd5b8101908080519060200190929190505050806000819055505060f38061005a6000396000f3fe6080604052348015600f57600080fd5b5060043610603c5760003560e01c80630dbe671f146041578063d46300fd14605d578063ee919d50146079575b600080fd5b604760a4565b6040518082815260200191505060405180910390f35b606360aa565b6040518082815260200191505060405180910390f35b60a260048036036020811015608d57600080fd5b810190808035906020019092919050505060b3565b005b60005481565b60008054905090565b806000819055505056fea264697066735822122084a43fd050d3de9bc0cdc2f86b02db81bc69b5639cedd1a5b72010d2a664879a64736f6c63430006020033a2646970667358221220a00266288222dc5d9803c3840f0e4d5d04630cb6bac841d30c42511d9b58045864736f6c63430006020033"; diff --git a/cmd/u2u/launcher/txtracer_test.go b/cmd/u2u/launcher/txtracer_test.go index 0f40984b..241ff003 100644 --- a/cmd/u2u/launcher/txtracer_test.go +++ b/cmd/u2u/launcher/txtracer_test.go @@ -50,11 +50,11 @@ func TestTxTracing(t *testing.T) { cliConsoleOutput = *cliConsole.GetOutDataTillCursor() // Call simple contract call to check created trace - cliConsole.InputLine("testContract.setA.sendTransaction(24, {from:u2u.accounts[1],gas:\"3000000\"})") + cliConsole.InputLine("testContract.setA.sendTransaction(24, {from:u2u.accounts[1]})") cliConsoleOutput = *cliConsole.GetOutDataTillCursor() txHashCall := cliConsoleOutput[strings.Index(cliConsoleOutput, "0x") : len(cliConsoleOutput)-3] - cliConsole.InputLine("testContract.deploy.sendTransaction({from:u2u.accounts[1],gas:\"3000000\"})") + cliConsole.InputLine("testContract.deploy.sendTransaction({from:u2u.accounts[1]})") cliConsoleOutput = *cliConsole.GetOutDataTillCursor() txHashDeploy := cliConsoleOutput[strings.Index(cliConsoleOutput, "0x") : len(cliConsoleOutput)-3] time.Sleep(5000 * time.Millisecond) From ee277722912c3c0c15b835066b5b3b6a5a1cec9b Mon Sep 17 00:00:00 2001 From: trinhdn97 Date: Thu, 12 Dec 2024 12:19:16 +0700 Subject: [PATCH 12/15] Bump client version to v1.1.1 --- params/version.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/params/version.go b/params/version.go index 6dfd9f66..75a55ef8 100644 --- a/params/version.go +++ b/params/version.go @@ -23,7 +23,7 @@ import ( var ( VersionMajor = 1 // Major version component of the current release VersionMinor = 1 // Minor version component of the current release - VersionPatch = 0 // Patch version component of the current release + VersionPatch = 1 // Patch version component of the current release VersionMeta = "stable" // Version metadata to append to the version string ) From 4b634acdc678548639d420391516744ebc27a9b0 Mon Sep 17 00:00:00 2001 From: huongnguyenduc Date: Fri, 13 Dec 2024 11:23:57 +0700 Subject: [PATCH 13/15] Wording to U2U --- tests/integration_test_net.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/integration_test_net.go b/tests/integration_test_net.go index 9a91b607..0f28e1e8 100644 --- a/tests/integration_test_net.go +++ b/tests/integration_test_net.go @@ -136,7 +136,7 @@ func StartIntegrationTestNet(directory string) (*IntegrationTestNet, error) { // connect to blockchain network client, err := result.GetClient() if err != nil { - return nil, fmt.Errorf("failed to connect to the Ethereum client: %w", err) + return nil, fmt.Errorf("failed to connect to the U2U client: %w", err) } defer client.Close() const timeout = 300 * time.Second @@ -210,7 +210,7 @@ func (n *IntegrationTestNet) EndowAccount( func (n *IntegrationTestNet) Run(tx *types.Transaction) (*types.Receipt, error) { client, err := n.GetClient() if err != nil { - return nil, fmt.Errorf("failed to connect to the Ethereum client: %w", err) + return nil, fmt.Errorf("failed to connect to the U2U client: %w", err) } defer client.Close() err = client.SendTransaction(context.Background(), tx) @@ -225,7 +225,7 @@ func (n *IntegrationTestNet) Run(tx *types.Transaction) (*types.Receipt, error) func (n *IntegrationTestNet) GetReceipt(txHash common.Hash) (*types.Receipt, error) { client, err := n.GetClient() if err != nil { - return nil, fmt.Errorf("failed to connect to the Ethereum client: %w", err) + return nil, fmt.Errorf("failed to connect to the U2U client: %w", err) } defer client.Close() // Wait for the response with some exponential backoff. @@ -274,7 +274,7 @@ func (n *IntegrationTestNet) Apply( func (n *IntegrationTestNet) GetTransactOptions(account *Account) (*bind.TransactOpts, error) { client, err := n.GetClient() if err != nil { - return nil, fmt.Errorf("failed to connect to the Ethereum client: %w", err) + return nil, fmt.Errorf("failed to connect to the U2U client: %w", err) } defer client.Close() ctxt := context.Background() @@ -312,7 +312,7 @@ func (n *IntegrationTestNet) GetClient() (*ethclient.Client, error) { func DeployContract[T any](n *IntegrationTestNet, deploy contractDeployer[T]) (*T, *types.Receipt, error) { client, err := n.GetClient() if err != nil { - return nil, nil, fmt.Errorf("failed to connect to the Ethereum client: %w", err) + return nil, nil, fmt.Errorf("failed to connect to the U2U client: %w", err) } defer client.Close() transactOptions, err := n.GetTransactOptions(&n.validator) From 9111e759b9153cc966e3de57126287d8f8ef9024 Mon Sep 17 00:00:00 2001 From: huongnguyenduc Date: Fri, 13 Dec 2024 11:25:53 +0700 Subject: [PATCH 14/15] Use U2U abigen tool --- tests/contracts/README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/contracts/README.md b/tests/contracts/README.md index e04230f6..dc26e895 100644 --- a/tests/contracts/README.md +++ b/tests/contracts/README.md @@ -11,5 +11,4 @@ For an example application, see the `counter` directory. For compiling Solidity code and generating the Go bindings the following tools need to be installed on your system: - the `solc` compiler; on Ubuntu you an install it using `sudo snap install solc --edge` -- the `abigen` tool; this can be installed using `go install github.com/ethereum/go-ethereum/cmd/abigen@latest` -Background information on those tools can be found [here](https://goethereumbook.org/en/smart-contract-compile/). \ No newline at end of file +- the `abigen` tool; this can be installed using `go install github.com/unicornultrafoundation/go-u2u/cmd/abigen@latest` \ No newline at end of file From b261e11e3cc4a493e4e6e5459ece8404d1c9f092 Mon Sep 17 00:00:00 2001 From: trinhdn97 Date: Tue, 17 Dec 2024 16:02:04 +0700 Subject: [PATCH 15/15] Move new integration tests to resolve import cycle --- {tests => integrationtests}/account.go | 2 +- {tests => integrationtests}/contracts/.gitignore | 0 {tests => integrationtests}/contracts/README.md | 0 {tests => integrationtests}/contracts/counter/counter.go | 0 {tests => integrationtests}/contracts/counter/counter.sol | 0 {tests => integrationtests}/contracts/counter/gen.go | 0 {tests => integrationtests}/integration_test_net.go | 2 +- {tests => integrationtests}/integration_test_net_test.go | 7 ++++--- 8 files changed, 6 insertions(+), 5 deletions(-) rename {tests => integrationtests}/account.go (94%) rename {tests => integrationtests}/contracts/.gitignore (100%) rename {tests => integrationtests}/contracts/README.md (100%) rename {tests => integrationtests}/contracts/counter/counter.go (100%) rename {tests => integrationtests}/contracts/counter/counter.sol (100%) rename {tests => integrationtests}/contracts/counter/gen.go (100%) rename {tests => integrationtests}/integration_test_net.go (99%) rename {tests => integrationtests}/integration_test_net_test.go (97%) diff --git a/tests/account.go b/integrationtests/account.go similarity index 94% rename from tests/account.go rename to integrationtests/account.go index 0c373153..b8583d17 100644 --- a/tests/account.go +++ b/integrationtests/account.go @@ -1,4 +1,4 @@ -package tests +package integrationtests import ( "crypto/ecdsa" diff --git a/tests/contracts/.gitignore b/integrationtests/contracts/.gitignore similarity index 100% rename from tests/contracts/.gitignore rename to integrationtests/contracts/.gitignore diff --git a/tests/contracts/README.md b/integrationtests/contracts/README.md similarity index 100% rename from tests/contracts/README.md rename to integrationtests/contracts/README.md diff --git a/tests/contracts/counter/counter.go b/integrationtests/contracts/counter/counter.go similarity index 100% rename from tests/contracts/counter/counter.go rename to integrationtests/contracts/counter/counter.go diff --git a/tests/contracts/counter/counter.sol b/integrationtests/contracts/counter/counter.sol similarity index 100% rename from tests/contracts/counter/counter.sol rename to integrationtests/contracts/counter/counter.sol diff --git a/tests/contracts/counter/gen.go b/integrationtests/contracts/counter/gen.go similarity index 100% rename from tests/contracts/counter/gen.go rename to integrationtests/contracts/counter/gen.go diff --git a/tests/integration_test_net.go b/integrationtests/integration_test_net.go similarity index 99% rename from tests/integration_test_net.go rename to integrationtests/integration_test_net.go index 0f28e1e8..0edb0894 100644 --- a/tests/integration_test_net.go +++ b/integrationtests/integration_test_net.go @@ -1,4 +1,4 @@ -package tests +package integrationtests import ( "context" diff --git a/tests/integration_test_net_test.go b/integrationtests/integration_test_net_test.go similarity index 97% rename from tests/integration_test_net_test.go rename to integrationtests/integration_test_net_test.go index 07e3fa23..67b5fcad 100644 --- a/tests/integration_test_net_test.go +++ b/integrationtests/integration_test_net_test.go @@ -1,14 +1,15 @@ -package tests +package integrationtests import ( "context" - "golang.org/x/net/nettest" "math/big" "testing" + "golang.org/x/net/nettest" + "github.com/unicornultrafoundation/go-u2u/common" "github.com/unicornultrafoundation/go-u2u/core/types" - "github.com/unicornultrafoundation/go-u2u/tests/contracts/counter" + "github.com/unicornultrafoundation/go-u2u/integrationtests/contracts/counter" ) func TestIntegrationTestNet_CanStartAndStopIntegrationTestNet(t *testing.T) {