From 54fcf365580b5462edcf0aa4acd4318fc865edec Mon Sep 17 00:00:00 2001 From: Diego Essaya Date: Thu, 14 Sep 2023 15:42:14 -0300 Subject: [PATCH 1/7] minor log improvements --- components/logger/evm.go | 9 ++++++--- packages/chain/mempool/typed_pool_by_nonce.go | 2 +- packages/evm/evmtest/env.go | 4 +++- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/components/logger/evm.go b/components/logger/evm.go index c37a05d6ae..3ae0b7c3a2 100644 --- a/components/logger/evm.go +++ b/components/logger/evm.go @@ -6,15 +6,18 @@ import ( "github.com/iotaledger/hive.go/logger" ) +var format = log.TerminalFormat(false) + func initGoEthLogger(waspLogger *logger.Logger) { log.Root().SetHandler(log.FuncHandler(func(r *log.Record) error { + s := string(format.Format(r)) switch r.Lvl { case log.LvlCrit, log.LvlError: - waspLogger.Errorf("[%s] %s", r.Lvl.AlignedString(), r.Msg) + waspLogger.Error(s) case log.LvlTrace, log.LvlDebug: - waspLogger.Debugf("[%s] %s", r.Lvl.AlignedString(), r.Msg) + waspLogger.Debug(s) default: - waspLogger.Infof("[%s] %s", r.Lvl.AlignedString(), r.Msg) + waspLogger.Info(s) } return nil })) diff --git a/packages/chain/mempool/typed_pool_by_nonce.go b/packages/chain/mempool/typed_pool_by_nonce.go index a115f9426e..98f755161c 100644 --- a/packages/chain/mempool/typed_pool_by_nonce.go +++ b/packages/chain/mempool/typed_pool_by_nonce.go @@ -67,7 +67,7 @@ func (p *TypedPoolByNonce[V]) Add(request V) { } defer func() { - p.log.Debugf("ADD %v as key=%v, senderAccount: ", request.ID(), ref, account) + p.log.Debugf("ADD %v as key=%v, senderAccount: %s", request.ID(), ref, account) p.sizeMetric(p.refLUT.Size()) p.waitReq.MarkAvailable(request) }() diff --git a/packages/evm/evmtest/env.go b/packages/evm/evmtest/env.go index 80635e9295..6c4fbed9e9 100644 --- a/packages/evm/evmtest/env.go +++ b/packages/evm/evmtest/env.go @@ -9,10 +9,12 @@ import ( "github.com/ethereum/go-ethereum/log" ) +var format = log.TerminalFormat(false) + func InitGoEthLogger(t testing.TB) { log.Root().SetHandler(log.FuncHandler(func(r *log.Record) error { if r.Lvl <= log.LvlWarn { - t.Logf("[%s] %s", r.Lvl.AlignedString(), r.Msg) + t.Log(string(format.Format(r))) } return nil })) From 28f1de4f1332fc00374487cb317500c33f008c66 Mon Sep 17 00:00:00 2001 From: Diego Essaya Date: Wed, 20 Sep 2023 15:30:37 -0300 Subject: [PATCH 2/7] The return of the evmemulator tool --- Makefile | 10 ++- packages/metrics/chain.go | 4 +- packages/metrics/chain_webapi.go | 4 +- packages/parameters/l1parameters.go | 4 +- packages/solo/context.go | 12 +++ packages/solo/evm.go | 15 ++-- packages/solo/solo.go | 2 +- tools/evm/evmemulator/main.go | 135 ++++++++++++++++++++++++++++ 8 files changed, 172 insertions(+), 14 deletions(-) create mode 100644 packages/solo/context.go create mode 100644 tools/evm/evmemulator/main.go diff --git a/Makefile b/Makefile index 2ca5694dcf..8a80f6b911 100644 --- a/Makefile +++ b/Makefile @@ -30,6 +30,10 @@ compile-solidity: build-cli: cd tools/wasp-cli && go mod tidy && go build -ldflags $(BUILD_LD_FLAGS) -o ../../ +# use like: make build-tool TOOL=./tools/dbinspector +build-tool: + $(BUILD_CMD) $(TOOL) + build-full: build-cli $(BUILD_CMD) ./... @@ -53,6 +57,10 @@ test-short: install-cli: cd tools/wasp-cli && go mod tidy && go install -ldflags $(BUILD_LD_FLAGS) +# use like: make install-tool TOOL=./tools/dbinspector +install-tool: + $(INSTALL_CMD) $(TOOL) + install-full: install-cli $(INSTALL_CMD) ./... @@ -103,4 +111,4 @@ deps-versions: awk -F ":" '{ print $$1 }' | \ { read from ; read to; awk -v s="$$from" -v e="$$to" 'NR>1*s&&NR<1*e' packages/testutil/privtangle/privtangle.go; } -.PHONY: all wasm compile-solidity build-cli build-full build build-lint test-full test test-short install-cli install-full install lint gofumpt-list docker-build deps-versions +.PHONY: all wasm compile-solidity build-tool install-tool build-cli build-full build build-lint test-full test test-short install-cli install-full install lint gofumpt-list docker-build deps-versions diff --git a/packages/metrics/chain.go b/packages/metrics/chain.go index 3dc7d74954..dff52e87d1 100644 --- a/packages/metrics/chain.go +++ b/packages/metrics/chain.go @@ -55,7 +55,7 @@ func NewChainMetricsProvider() *ChainMetricsProvider { StateManager: newChainStateManagerMetricsProvider(), Snapshots: newChainSnapshotsMetricsProvider(), NodeConn: newChainNodeConnMetricsProvider(), - WebAPI: newChainWebAPIMetricsProvider(), + WebAPI: NewChainWebAPIMetricsProvider(), State: newChainStateMetricsProvider(), } } @@ -91,7 +91,7 @@ func (m *ChainMetricsProvider) GetChainMetrics(chainID isc.ChainID) *ChainMetric StateManager: m.StateManager.createForChain(chainID), Snapshots: m.Snapshots.createForChain(chainID), NodeConn: m.NodeConn.createForChain(chainID), - WebAPI: m.WebAPI.createForChain(chainID), + WebAPI: m.WebAPI.CreateForChain(chainID), State: m.State.createForChain(chainID), } m.chains[chainID] = cm diff --git a/packages/metrics/chain_webapi.go b/packages/metrics/chain_webapi.go index ffcfe5542b..18b36ee494 100644 --- a/packages/metrics/chain_webapi.go +++ b/packages/metrics/chain_webapi.go @@ -14,7 +14,7 @@ type ChainWebAPIMetricsProvider struct { evmRPCCalls *prometheus.HistogramVec } -func newChainWebAPIMetricsProvider() *ChainWebAPIMetricsProvider { +func NewChainWebAPIMetricsProvider() *ChainWebAPIMetricsProvider { return &ChainWebAPIMetricsProvider{ requests: prometheus.NewHistogramVec(prometheus.HistogramOpts{ Namespace: "iota_wasp", @@ -40,7 +40,7 @@ func (p *ChainWebAPIMetricsProvider) register(reg prometheus.Registerer) { ) } -func (p *ChainWebAPIMetricsProvider) createForChain(chainID isc.ChainID) *ChainWebAPIMetrics { +func (p *ChainWebAPIMetricsProvider) CreateForChain(chainID isc.ChainID) *ChainWebAPIMetrics { return newChainWebAPIMetrics(p, chainID) } diff --git a/packages/parameters/l1parameters.go b/packages/parameters/l1parameters.go index 58ec777fd4..f33d08bf22 100644 --- a/packages/parameters/l1parameters.go +++ b/packages/parameters/l1parameters.go @@ -37,7 +37,7 @@ var ( l1ParamsMutex = &sync.RWMutex{} l1Params *L1Params - l1ForTesting = &L1Params{ + L1ForTesting = &L1Params{ // There are no limits on how big from a size perspective an essence can be, so it is just derived from 32KB - Message fields without payload = max size of the payload MaxPayloadSize: MaxPayloadSize, Protocol: &iotago.ProtocolParameters{ @@ -80,7 +80,7 @@ func L1() *L1Params { func L1NoLock() *L1Params { if l1Params == nil { if isTestContext() { - l1Params = l1ForTesting + l1Params = L1ForTesting } else if l1ParamsLazyInit != nil { l1ParamsLazyInit() } diff --git a/packages/solo/context.go b/packages/solo/context.go new file mode 100644 index 0000000000..c3d9edc827 --- /dev/null +++ b/packages/solo/context.go @@ -0,0 +1,12 @@ +package solo + +import "github.com/stretchr/testify/require" + +type Context interface { + require.TestingT + Name() string + Cleanup(func()) + Helper() + Logf(string, ...any) + Fatalf(string, ...any) +} diff --git a/packages/solo/evm.go b/packages/solo/evm.go index 6f0e506b79..24a261962e 100644 --- a/packages/solo/evm.go +++ b/packages/solo/evm.go @@ -118,14 +118,17 @@ func (ch *Chain) PostEthereumTransaction(tx *types.Transaction) (dict.Dict, erro return ch.RunOffLedgerRequest(req) } -var EthereumAccounts []*ecdsa.PrivateKey +var EthereumAccounts [10]*ecdsa.PrivateKey func init() { - EthereumAccounts = make([]*ecdsa.PrivateKey, 4) - EthereumAccounts[0], _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291") - EthereumAccounts[1], _ = crypto.HexToECDSA("289c2857d4598e37fb9647507e47a309d6133539bf21a8b9cb6df88fd5232032") - EthereumAccounts[2], _ = crypto.HexToECDSA("8a1f9a8f95be41cd7ccb6168179afb4504aefe388d1e14474d32c45c72ce7b7a") - EthereumAccounts[3], _ = crypto.HexToECDSA("49a7b37aa6f6645917e7b807e9d1c00d4fa71f18343b0d4122a4d2df64dd6fee") + for i := 0; i < len(EthereumAccounts); i++ { + seed := crypto.Keccak256([]byte(fmt.Sprintf("seed %d", i))) + key, err := crypto.ToECDSA(seed) + if err != nil { + panic(err) + } + EthereumAccounts[i] = key + } } func (ch *Chain) EthereumAccountByIndexWithL2Funds(i int, baseTokens ...uint64) (*ecdsa.PrivateKey, common.Address) { diff --git a/packages/solo/solo.go b/packages/solo/solo.go index 83d5fc8860..5b79bda452 100644 --- a/packages/solo/solo.go +++ b/packages/solo/solo.go @@ -56,7 +56,7 @@ const ( // Solo is a structure which contains global parameters of the test: one per test instance type Solo struct { // instance of the test - T testing.TB + T Context logger *logger.Logger chainStateDatabaseManager *database.ChainStateDatabaseManager utxoDB *utxodb.UtxoDB diff --git a/tools/evm/evmemulator/main.go b/tools/evm/evmemulator/main.go new file mode 100644 index 0000000000..36d407783b --- /dev/null +++ b/tools/evm/evmemulator/main.go @@ -0,0 +1,135 @@ +// Copyright 2020 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "crypto/ecdsa" + "encoding/hex" + "fmt" + "net/http" + "os" + + "github.com/ethereum/go-ethereum/crypto" + "github.com/spf13/cobra" + + "github.com/iotaledger/wasp/components/app" + "github.com/iotaledger/wasp/packages/evm/jsonrpc" + "github.com/iotaledger/wasp/packages/isc" + "github.com/iotaledger/wasp/packages/kv/codec" + "github.com/iotaledger/wasp/packages/kv/dict" + "github.com/iotaledger/wasp/packages/metrics" + "github.com/iotaledger/wasp/packages/origin" + "github.com/iotaledger/wasp/packages/parameters" + "github.com/iotaledger/wasp/packages/solo" + "github.com/iotaledger/wasp/packages/vm/core/evm/emulator" + "github.com/iotaledger/wasp/tools/wasp-cli/log" +) + +type soloContext struct { + cleanup []func() +} + +func (s *soloContext) Cleanup(f func()) { + s.cleanup = append(s.cleanup, f) +} + +func (*soloContext) Errorf(format string, args ...interface{}) { + log.Printf("error: "+format, args) +} + +func (*soloContext) FailNow() { + os.Exit(1) +} + +func (s *soloContext) Fatalf(format string, args ...any) { + log.Printf("fatal: "+format, args) + s.FailNow() +} + +func (*soloContext) Helper() { +} + +func (*soloContext) Logf(format string, args ...any) { + log.Printf(format, args...) +} + +func (*soloContext) Name() string { + return "evmemulator" +} + +func init() { + parameters.InitL1(parameters.L1ForTesting) +} + +func main() { + cmd := &cobra.Command{ + Args: cobra.NoArgs, + Run: start, + Use: "evmemulator", + Short: "evmemulator runs a JSONRPC server with Solo as backend", + Long: fmt.Sprintf(`evmemulator runs a JSONRPC server with Solo as backend. + +evmemulator does the following: + +- Starts a Solo environment (a framework for running local ISC chains in-memory) +- Deploys an ISC chain +- Initializes 10 ethereum accounts with funds (private keys and addresses printed after init) +- Starts a JSON-RPC server + +You can connect any Ethereum tool (eg Metamask) to this JSON-RPC server and use it for testing Ethereum contracts running on ISCP. +`, + ), + } + + log.Init(cmd) + + err := cmd.Execute() + log.Check(err) +} + +func start(cmd *cobra.Command, args []string) { + ctx := &soloContext{} + defer func() { + for i := len(ctx.cleanup) - 1; i >= 0; i-- { + ctx.cleanup[i]() + } + }() + + env := solo.New(ctx, &solo.InitOptions{Debug: log.DebugFlag, PrintStackTrace: log.DebugFlag}) + + chainOwner, chainOwnerAddr := env.NewKeyPairWithFunds() + chain, _ := env.NewChainExt(chainOwner, 1*isc.Million, "evmemulator", dict.Dict{ + origin.ParamChainOwner: isc.NewAgentID(chainOwnerAddr).Bytes(), + origin.ParamEVMChainID: codec.EncodeUint16(1074), + origin.ParamBlockKeepAmount: codec.EncodeInt32(emulator.BlockKeepAll), + origin.ParamWaspVersion: codec.EncodeString(app.Version), + }) + + var accounts []*ecdsa.PrivateKey + log.Printf("creating accounts with funds...\n") + header := []string{"private key", "address"} + var rows [][]string + for i := 0; i < len(solo.EthereumAccounts); i++ { + pk, addr := chain.EthereumAccountByIndexWithL2Funds(i) + accounts = append(accounts, pk) + rows = append(rows, []string{hex.EncodeToString(crypto.FromECDSA(pk)), addr.String()}) + } + log.PrintTable(header, rows) + + srv, err := jsonrpc.NewServer( + chain.EVM(), + jsonrpc.NewAccountManager(accounts), + metrics.NewChainWebAPIMetricsProvider().CreateForChain(chain.ChainID), + ) + log.Check(err) + + const addr = ":8545" + s := &http.Server{ + Addr: addr, + Handler: srv, + } + log.Printf("starting JSONRPC server on %s...\n", addr) + err = s.ListenAndServe() + log.Check(err) +} From c7176f8f6295c095e3a87ce8b8d9e1676365cc32 Mon Sep 17 00:00:00 2001 From: Diego Essaya Date: Wed, 20 Sep 2023 15:32:16 -0300 Subject: [PATCH 3/7] feat(solo/evmemulator): add evm_snapshot jsonrpc endpoint --- packages/evm/jsonrpc/chainbackend.go | 2 ++ packages/evm/jsonrpc/server.go | 1 + packages/evm/jsonrpc/service.go | 19 ++++++++++++ packages/evm/jsonrpc/waspevmbackend.go | 11 +++++++ packages/solo/evm.go | 16 ++++++++++ packages/solo/snapshot.go | 43 ++++++++++++++++---------- packages/solo/solo.go | 33 +++++++++++--------- packages/solo/solotest/solo_test.go | 4 +-- 8 files changed, 96 insertions(+), 33 deletions(-) diff --git a/packages/evm/jsonrpc/chainbackend.go b/packages/evm/jsonrpc/chainbackend.go index f86625a699..79b1392185 100644 --- a/packages/evm/jsonrpc/chainbackend.go +++ b/packages/evm/jsonrpc/chainbackend.go @@ -30,4 +30,6 @@ type ChainBackend interface { ISCStateByBlockIndex(blockIndex uint32) (state.State, error) ISCStateByTrieRoot(trieRoot trie.Hash) (state.State, error) BaseToken() *parameters.BaseToken + TakeSnapshot() (int, error) + RevertToSnapshot(int) error } diff --git a/packages/evm/jsonrpc/server.go b/packages/evm/jsonrpc/server.go index 5187203352..b62e012624 100644 --- a/packages/evm/jsonrpc/server.go +++ b/packages/evm/jsonrpc/server.go @@ -25,6 +25,7 @@ func NewServer( {"eth", NewEthService(evmChain, accountManager, metrics)}, {"debug", NewDebugService(evmChain, metrics)}, {"txpool", NewTxPoolService()}, + {"evm", NewEVMService(evmChain)}, } { err := rpcsrv.RegisterName(srv.namespace, srv.service) if err != nil { diff --git a/packages/evm/jsonrpc/service.go b/packages/evm/jsonrpc/service.go index cc949daef8..5209dfb0ca 100644 --- a/packages/evm/jsonrpc/service.go +++ b/packages/evm/jsonrpc/service.go @@ -684,3 +684,22 @@ func (d *DebugService) TraceTransaction(txHash common.Hash, config *tracers.Trac }, ) } + +type EVMService struct { + evmChain *EVMChain +} + +func NewEVMService(evmChain *EVMChain) *EVMService { + return &EVMService{ + evmChain: evmChain, + } +} + +func (e *EVMService) Snapshot() (hexutil.Uint, error) { + n, err := e.evmChain.backend.TakeSnapshot() + return hexutil.Uint(n), err +} + +func (e *EVMService) Revert(snapshot hexutil.Uint) error { + return e.evmChain.backend.RevertToSnapshot(int(snapshot)) +} diff --git a/packages/evm/jsonrpc/waspevmbackend.go b/packages/evm/jsonrpc/waspevmbackend.go index a740873248..ef8377a664 100644 --- a/packages/evm/jsonrpc/waspevmbackend.go +++ b/packages/evm/jsonrpc/waspevmbackend.go @@ -4,6 +4,7 @@ package jsonrpc import ( + "errors" "fmt" "time" @@ -141,3 +142,13 @@ func (b *WaspEVMBackend) ISCChainID() *isc.ChainID { chID := b.chain.ID() return &chID } + +var errNotImplemented = errors.New("method not implemented") + +func (*WaspEVMBackend) RevertToSnapshot(int) error { + return errNotImplemented +} + +func (*WaspEVMBackend) TakeSnapshot() (int, error) { + return 0, errNotImplemented +} diff --git a/packages/solo/evm.go b/packages/solo/evm.go index 24a261962e..e8bfa8d4bd 100644 --- a/packages/solo/evm.go +++ b/packages/solo/evm.go @@ -2,6 +2,7 @@ package solo import ( "crypto/ecdsa" + "errors" "fmt" "time" @@ -27,6 +28,7 @@ import ( type jsonRPCSoloBackend struct { Chain *Chain baseToken *parameters.BaseToken + snapshots []*Snapshot } func newJSONRPCSoloBackend(chain *Chain, baseToken *parameters.BaseToken) jsonrpc.ChainBackend { @@ -99,6 +101,20 @@ func (b *jsonRPCSoloBackend) ISCChainID() *isc.ChainID { return &b.Chain.ChainID } +func (b *jsonRPCSoloBackend) RevertToSnapshot(i int) error { + if i < 0 || i >= len(b.snapshots) { + return errors.New("invalid snapshot index") + } + b.Chain.Env.RestoreSnapshot(b.snapshots[i]) + b.snapshots = b.snapshots[:i] + return nil +} + +func (b *jsonRPCSoloBackend) TakeSnapshot() (int, error) { + b.snapshots = append(b.snapshots, b.Chain.Env.TakeSnapshot()) + return len(b.snapshots) - 1, nil +} + func (ch *Chain) EVM() *jsonrpc.EVMChain { return jsonrpc.NewEVMChain( newJSONRPCSoloBackend(ch, parameters.L1().BaseToken), diff --git a/packages/solo/snapshot.go b/packages/solo/snapshot.go index cfb88237d5..087335d4a8 100644 --- a/packages/solo/snapshot.go +++ b/packages/solo/snapshot.go @@ -14,12 +14,12 @@ import ( "github.com/iotaledger/wasp/packages/util/rwutil" ) -type soloSnapshot struct { +type Snapshot struct { UtxoDB *utxodb.UtxoDBState - Chains []soloChainSnapshot + Chains []*ChainSnapshot } -type soloChainSnapshot struct { +type ChainSnapshot struct { Name string StateControllerKeyPair []byte ChainID []byte @@ -29,16 +29,16 @@ type soloChainSnapshot struct { } // SaveSnapshot generates a snapshot of the Solo environment -func (env *Solo) SaveSnapshot(fname string) { +func (env *Solo) TakeSnapshot() *Snapshot { env.chainsMutex.Lock() defer env.chainsMutex.Unlock() - snapshot := soloSnapshot{ + snapshot := &Snapshot{ UtxoDB: env.utxoDB.State(), } for _, ch := range env.chains { - chainSnapshot := soloChainSnapshot{ + chainSnapshot := &ChainSnapshot{ Name: ch.Name, StateControllerKeyPair: rwutil.WriteToBytes(ch.StateControllerKeyPair), ChainID: ch.ChainID.Bytes(), @@ -55,23 +55,14 @@ func (env *Solo) SaveSnapshot(fname string) { snapshot.Chains = append(snapshot.Chains, chainSnapshot) } - b, err := json.Marshal(snapshot) - require.NoError(env.T, err) - err = os.WriteFile(fname, b, 0o600) - require.NoError(env.T, err) + return snapshot } // LoadSnapshot restores the Solo environment from the given snapshot -func (env *Solo) LoadSnapshot(fname string) { +func (env *Solo) RestoreSnapshot(snapshot *Snapshot) { env.chainsMutex.Lock() defer env.chainsMutex.Unlock() - b, err := os.ReadFile(fname) - require.NoError(env.T, err) - var snapshot soloSnapshot - err = json.Unmarshal(b, &snapshot) - require.NoError(env.T, err) - env.utxoDB.SetState(snapshot.UtxoDB) for _, chainSnapshot := range snapshot.Chains { sckp, err := rwutil.ReadFromBytes(chainSnapshot.StateControllerKeyPair, new(cryptolib.KeyPair)) @@ -105,3 +96,21 @@ func (env *Solo) LoadSnapshot(fname string) { env.addChain(chainData) } } + +// SaveSnapshot saves the given snapshot to a file +func (env *Solo) SaveSnapshot(snapshot *Snapshot, fname string) { + b, err := json.Marshal(snapshot) + require.NoError(env.T, err) + err = os.WriteFile(fname, b, 0o600) + require.NoError(env.T, err) +} + +// LoadSnapshot loads a snapshot previously saved with SaveSnapshot +func (env *Solo) LoadSnapshot(fname string) *Snapshot { + b, err := os.ReadFile(fname) + require.NoError(env.T, err) + var snapshot Snapshot + err = json.Unmarshal(b, &snapshot) + require.NoError(env.T, err) + return &snapshot +} diff --git a/packages/solo/solo.go b/packages/solo/solo.go index 5b79bda452..fd25170279 100644 --- a/packages/solo/solo.go +++ b/packages/solo/solo.go @@ -9,9 +9,9 @@ import ( "math/big" "math/rand" "sync" - "testing" "time" + "github.com/samber/lo" "github.com/stretchr/testify/require" "go.uber.org/zap/zapcore" @@ -148,7 +148,7 @@ func DefaultInitOptions() *InitOptions { // New creates an instance of the Solo environment // If solo is used for unit testing, 't' should be the *testing.T instance; // otherwise it can be either nil or an instance created with NewTestContext. -func New(t testing.TB, initOptions ...*InitOptions) *Solo { +func New(t Context, initOptions ...*InitOptions) *Solo { opt := DefaultInitOptions() if len(initOptions) > 0 { opt = initOptions[0] @@ -197,13 +197,27 @@ func New(t testing.TB, initOptions ...*InitOptions) *Solo { ret.logger.Infof("solo publisher: %s %s %v", ev.Kind, ev.ChainID, ev.String()) }) - go func() { - ret.publisher.Run(ctx) - }() + go ret.publisher.Run(ctx) + + go ret.batchLoop() return ret } +func (env *Solo) batchLoop() { + for { + time.Sleep(50 * time.Millisecond) + chains := func() []*Chain { + env.chainsMutex.Lock() + defer env.chainsMutex.Unlock() + return lo.Values(env.chains) + }() + for _, ch := range chains { + ch.collateAndRunBatch() + } + } +} + func (env *Solo) GetDBHash() hashing.HashValue { return env.chainStateDatabaseManager.DBHash() } @@ -371,7 +385,6 @@ func (env *Solo) addChain(chData chainData) *Chain { migrationScheme: allmigrations.DefaultScheme, } env.chains[chData.ChainID] = ch - go ch.batchLoop() return ch } @@ -447,14 +460,6 @@ func (ch *Chain) collateBatch() []isc.Request { return requests[:batchSize] } -// batchLoop mimics behavior Wasp consensus -func (ch *Chain) batchLoop() { - for { - ch.collateAndRunBatch() - time.Sleep(50 * time.Millisecond) - } -} - func (ch *Chain) collateAndRunBatch() { ch.runVMMutex.Lock() defer ch.runVMMutex.Unlock() diff --git a/packages/solo/solotest/solo_test.go b/packages/solo/solotest/solo_test.go index 6f32dca06c..a480129d66 100644 --- a/packages/solo/solotest/solo_test.go +++ b/packages/solo/solotest/solo_test.go @@ -47,7 +47,7 @@ func TestSaveSnapshot(t *testing.T) { require.NotEmpty(t, ch.L2NFTs(ch.OriginatorAgentID)) - ch.Env.SaveSnapshot("snapshot.db") + ch.Env.SaveSnapshot(ch.Env.TakeSnapshot(), "snapshot.db") } // This test is an example of how to restore a Solo snapshot. @@ -57,7 +57,7 @@ func TestLoadSnapshot(t *testing.T) { t.SkipNow() env := solo.New(t, &solo.InitOptions{AutoAdjustStorageDeposit: true, Debug: true, PrintStackTrace: true}) - env.LoadSnapshot("snapshot.db") + env.RestoreSnapshot(env.LoadSnapshot("snapshot.db")) ch := env.GetChainByName("chain1") From 554d921f4abb5801ea2c2fb658d5ac4a6b8501d5 Mon Sep 17 00:00:00 2001 From: Diego Essaya Date: Wed, 20 Sep 2023 15:33:19 -0300 Subject: [PATCH 4/7] fix(evm): return revert reason when estimateGas fails --- packages/chainutil/evmestimategas.go | 54 +++++++++++-------- .../testdbhash/TestStorageContract.hex | 2 +- 2 files changed, 34 insertions(+), 22 deletions(-) diff --git a/packages/chainutil/evmestimategas.go b/packages/chainutil/evmestimategas.go index f348770984..c1a7d17fc4 100644 --- a/packages/chainutil/evmestimategas.go +++ b/packages/chainutil/evmestimategas.go @@ -15,7 +15,7 @@ import ( "github.com/iotaledger/wasp/packages/vm/gas" ) -var evmErrorsRegex = regexp.MustCompile("out of gas|intrinsic gas too low|(execution reverted$)") +var evmErrOutOfGasRegex = regexp.MustCompile("out of gas|intrinsic gas too low") // EVMEstimateGas executes the given request and discards the resulting chain state. It is useful // for estimating gas. @@ -39,29 +39,14 @@ func EVMEstimateGas(ch chain.ChainCore, aliasOutput *isc.AliasOutputWithID, call // Create a helper to check if a gas allowance results in an executable transaction blockTime := time.Now() - executable := func(gas uint64) (failed bool, used uint64, err error) { + executable := func(gas uint64) (failed bool, result *vm.RequestResult, err error) { call.Gas = gas iscReq := isc.NewEVMOffLedgerCallRequest(ch.ID(), call) res, err := runISCRequest(ch, aliasOutput, blockTime, iscReq, true) if err != nil { - return true, 0, err + return true, nil, err } - if res.Receipt.Error != nil { - if res.Receipt.Error.ErrorCode == vm.ErrGasBudgetExceeded.Code() { - // out of gas when charging ISC gas - return true, 0, nil - } - vmerr, resolvingErr := ResolveError(ch, res.Receipt.Error) - if resolvingErr != nil { - panic(fmt.Errorf("error resolving vmerror %w", resolvingErr)) - } - if evmErrorsRegex.Match([]byte(vmerr.Error())) { - // increase gas - return true, 0, nil - } - return true, 0, vmerr - } - return false, res.Receipt.GasBurned, nil + return res.Receipt.Error != nil, res, nil } // Execute the binary search and hone in on an executable gas limit @@ -83,13 +68,15 @@ func EVMEstimateGas(ch chain.ChainCore, aliasOutput *isc.AliasOutputWithID, call var failed bool var err error - failed, lastUsed, err = executable(mid) + failed, res, err := executable(mid) if err != nil { return 0, err } if failed { + lastUsed = 0 lo = mid } else { + lastUsed = res.Receipt.GasBurned hi = mid if lastUsed == mid { // if used gas == gas limit, then use this as the estimation. @@ -103,11 +90,20 @@ func EVMEstimateGas(ch chain.ChainCore, aliasOutput *isc.AliasOutputWithID, call // Reject the transaction as invalid if it still fails at the highest allowance if hi == gasCap { - failed, _, err := executable(hi) + failed, res, err := executable(hi) if err != nil { return 0, err } if failed { + if res.Receipt.Error != nil { + isOutOfGas, resolvedErr, err := resolveError(ch, res.Receipt.Error) + if err != nil { + return 0, err + } + if resolvedErr != nil && !isOutOfGas { + return 0, resolvedErr + } + } if hi == maximumPossibleGas { return 0, fmt.Errorf("request might require more gas than it is allowed by the VM (%d), or will never succeed", gasCap) } @@ -122,3 +118,19 @@ func getMaxCallGasLimit(ch chain.ChainCore) uint64 { info := governance.NewStateAccess(mustLatestState(ch)).ChainInfo(ch.ID()) return gas.EVMCallGasLimit(info.GasLimits, &info.GasFeePolicy.EVMGasRatio) } + +func resolveError(ch chain.ChainCore, receiptError *isc.UnresolvedVMError) (isOutOfGas bool, resolved *isc.VMError, err error) { + if receiptError.ErrorCode == vm.ErrGasBudgetExceeded.Code() { + // out of gas when charging ISC gas + return true, nil, nil + } + vmerr, resolvingErr := ResolveError(ch, receiptError) + if resolvingErr != nil { + return true, nil, fmt.Errorf("error resolving vmerror: %w", resolvingErr) + } + if evmErrOutOfGasRegex.Match([]byte(vmerr.Error())) { + // increase gas + return true, vmerr, nil + } + return false, vmerr, nil +} diff --git a/packages/testutil/testdbhash/TestStorageContract.hex b/packages/testutil/testdbhash/TestStorageContract.hex index 7d0c33a156..eb88e6f852 100644 --- a/packages/testutil/testdbhash/TestStorageContract.hex +++ b/packages/testutil/testdbhash/TestStorageContract.hex @@ -1 +1 @@ -0xa400ec6dfc8ab5d3a36a37f72ffc5536b7c250e4a243bdcd043769fba228d18c +0x4ab1a9595eef744558f1ff4a38adf4c5232b5f14a90a9a1cc9d67e12c889831a From 81f1658008aec00d88242e568d4f7f5a6682117b Mon Sep 17 00:00:00 2001 From: Diego Essaya Date: Wed, 20 Sep 2023 18:50:57 -0300 Subject: [PATCH 5/7] add README for evmemulator --- tools/evm/evmemulator/README.md | 37 +++++++++++++++++++++++++++++++++ tools/evm/evmemulator/main.go | 9 ++++---- 2 files changed, 42 insertions(+), 4 deletions(-) create mode 100644 tools/evm/evmemulator/README.md diff --git a/tools/evm/evmemulator/README.md b/tools/evm/evmemulator/README.md new file mode 100644 index 0000000000..a325ca7f68 --- /dev/null +++ b/tools/evm/evmemulator/README.md @@ -0,0 +1,37 @@ +# evmemulator + +The `evmemulator` tool provides a JSONRPC server with Solo as a backend, allowing +to test Ethereum contracts. + +## Example: Uniswap test suite + +The following commands will clone and run the Uniswap contract tests against ISC's EVM. + +Start the `evmemulator`: + +``` +evmemulator +``` + +In another terminal, clone uniswap: + +``` +git clone https://github.com/Uniswap/uniswap-v3-core.git +yarn install +npx hardhat compile +``` + +Edit `hardhat.config.ts`, section `networks`: + +``` +wasp: { + chainId: 1074, + url: 'http://localhost:8545', +}, +``` + +Run the test suite: + +``` +npx hardhat test --network wasp +``` diff --git a/tools/evm/evmemulator/main.go b/tools/evm/evmemulator/main.go index 36d407783b..d1df4224e3 100644 --- a/tools/evm/evmemulator/main.go +++ b/tools/evm/evmemulator/main.go @@ -72,12 +72,13 @@ func main() { evmemulator does the following: -- Starts a Solo environment (a framework for running local ISC chains in-memory) -- Deploys an ISC chain +- Starts an ISC chain in a Solo environment - Initializes 10 ethereum accounts with funds (private keys and addresses printed after init) -- Starts a JSON-RPC server +- Starts a JSONRPC server -You can connect any Ethereum tool (eg Metamask) to this JSON-RPC server and use it for testing Ethereum contracts running on ISCP. +You can connect any Ethereum tool (eg Metamask) to this JSON-RPC server and use it for testing Ethereum contracts. + +Note: chain data is stored in-memory and will be lost upon termination. `, ), } From 59f12dcc52ef8bdc6e431c61a87db2dcdba668bb Mon Sep 17 00:00:00 2001 From: Diego Essaya Date: Wed, 20 Sep 2023 18:52:34 -0300 Subject: [PATCH 6/7] fix(jsonrpc): preserve order in eth_accounts --- packages/evm/jsonrpc/accounts.go | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/packages/evm/jsonrpc/accounts.go b/packages/evm/jsonrpc/accounts.go index 01ccb6ee05..149d8b6d29 100644 --- a/packages/evm/jsonrpc/accounts.go +++ b/packages/evm/jsonrpc/accounts.go @@ -8,10 +8,12 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" + "golang.org/x/exp/slices" ) type AccountManager struct { accounts map[common.Address]*ecdsa.PrivateKey + addrs []common.Address } func NewAccountManager(accounts []*ecdsa.PrivateKey) *AccountManager { @@ -30,6 +32,7 @@ func (a *AccountManager) Add(keyPair *ecdsa.PrivateKey) { return } a.accounts[addr] = keyPair + a.addrs = append(a.addrs, addr) } func (a *AccountManager) Get(addr common.Address) *ecdsa.PrivateKey { @@ -37,11 +40,5 @@ func (a *AccountManager) Get(addr common.Address) *ecdsa.PrivateKey { } func (a *AccountManager) Addresses() []common.Address { - ret := make([]common.Address, len(a.accounts)) - i := 0 - for addr := range a.accounts { - ret[i] = addr - i++ - } - return ret + return slices.Clone(a.addrs) } From c8bee5c02594d8aa972747924d5a608c38d8496c Mon Sep 17 00:00:00 2001 From: Diego Essaya Date: Wed, 20 Sep 2023 18:53:27 -0300 Subject: [PATCH 7/7] fix(evm): debug logging of jsonrpc --- components/logger/component.go | 4 ++-- .../evm/evmlogger/evmlogger.go | 8 ++++--- packages/evm/evmtest/env.go | 21 ------------------- .../evm/jsonrpc/jsonrpctest/jsonrpc_test.go | 2 -- packages/solo/solo.go | 2 ++ tools/cluster/cluster.go | 2 ++ tools/cluster/tests/evm_jsonrpc_test.go | 3 --- 7 files changed, 11 insertions(+), 31 deletions(-) rename components/logger/evm.go => packages/evm/evmlogger/evmlogger.go (75%) delete mode 100644 packages/evm/evmtest/env.go diff --git a/components/logger/component.go b/components/logger/component.go index 704f31ada7..727878fb25 100644 --- a/components/logger/component.go +++ b/components/logger/component.go @@ -2,6 +2,7 @@ package logger import ( "github.com/iotaledger/hive.go/app" + "github.com/iotaledger/wasp/packages/evm/evmlogger" ) func init() { @@ -14,7 +15,6 @@ func init() { var Component *app.Component func configure() error { - initGoEthLogger(Component.App().NewLogger("go-ethereum")) - + evmlogger.Init(Component.App().NewLogger("go-ethereum")) return nil } diff --git a/components/logger/evm.go b/packages/evm/evmlogger/evmlogger.go similarity index 75% rename from components/logger/evm.go rename to packages/evm/evmlogger/evmlogger.go index 3ae0b7c3a2..aa9bfa146c 100644 --- a/components/logger/evm.go +++ b/packages/evm/evmlogger/evmlogger.go @@ -1,6 +1,8 @@ -package logger +package evmlogger import ( + "strings" + "github.com/ethereum/go-ethereum/log" "github.com/iotaledger/hive.go/logger" @@ -8,9 +10,9 @@ import ( var format = log.TerminalFormat(false) -func initGoEthLogger(waspLogger *logger.Logger) { +func Init(waspLogger *logger.Logger) { log.Root().SetHandler(log.FuncHandler(func(r *log.Record) error { - s := string(format.Format(r)) + s := strings.TrimRight(string(format.Format(r)), "\n") switch r.Lvl { case log.LvlCrit, log.LvlError: waspLogger.Error(s) diff --git a/packages/evm/evmtest/env.go b/packages/evm/evmtest/env.go deleted file mode 100644 index 6c4fbed9e9..0000000000 --- a/packages/evm/evmtest/env.go +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright 2020 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -package evmtest - -import ( - "testing" - - "github.com/ethereum/go-ethereum/log" -) - -var format = log.TerminalFormat(false) - -func InitGoEthLogger(t testing.TB) { - log.Root().SetHandler(log.FuncHandler(func(r *log.Record) error { - if r.Lvl <= log.LvlWarn { - t.Log(string(format.Format(r))) - } - return nil - })) -} diff --git a/packages/evm/jsonrpc/jsonrpctest/jsonrpc_test.go b/packages/evm/jsonrpc/jsonrpctest/jsonrpc_test.go index 4a09d38c92..e1374dd33e 100644 --- a/packages/evm/jsonrpc/jsonrpctest/jsonrpc_test.go +++ b/packages/evm/jsonrpc/jsonrpctest/jsonrpc_test.go @@ -38,8 +38,6 @@ type soloTestEnv struct { } func newSoloTestEnv(t testing.TB) *soloTestEnv { - evmtest.InitGoEthLogger(t) - var log *logger.Logger if _, ok := t.(*testing.B); ok { log = testlogger.NewSilentLogger(t.Name(), true) diff --git a/packages/solo/solo.go b/packages/solo/solo.go index fd25170279..753aff8dc5 100644 --- a/packages/solo/solo.go +++ b/packages/solo/solo.go @@ -22,6 +22,7 @@ import ( "github.com/iotaledger/wasp/packages/chain" "github.com/iotaledger/wasp/packages/cryptolib" "github.com/iotaledger/wasp/packages/database" + "github.com/iotaledger/wasp/packages/evm/evmlogger" "github.com/iotaledger/wasp/packages/hashing" "github.com/iotaledger/wasp/packages/isc" "github.com/iotaledger/wasp/packages/isc/coreutil" @@ -159,6 +160,7 @@ func New(t Context, initOptions ...*InitOptions) *Solo { opt.Log = testlogger.WithLevel(opt.Log, zapcore.InfoLevel, opt.PrintStackTrace) } } + evmlogger.Init(opt.Log) chainRecordRegistryProvider, err := registry.NewChainRecordRegistryImpl("") require.NoError(t, err) diff --git a/tools/cluster/cluster.go b/tools/cluster/cluster.go index 2c159e4207..8fe65fd552 100644 --- a/tools/cluster/cluster.go +++ b/tools/cluster/cluster.go @@ -32,6 +32,7 @@ import ( "github.com/iotaledger/wasp/components/app" "github.com/iotaledger/wasp/packages/apilib" "github.com/iotaledger/wasp/packages/cryptolib" + "github.com/iotaledger/wasp/packages/evm/evmlogger" "github.com/iotaledger/wasp/packages/isc" "github.com/iotaledger/wasp/packages/kv/codec" "github.com/iotaledger/wasp/packages/kv/dict" @@ -69,6 +70,7 @@ func New(name string, config *ClusterConfig, dataPath string, t *testing.T, log } log = testlogger.NewLogger(t) } + evmlogger.Init(log) config.setValidatorAddressIfNotSet() // privtangle prefix diff --git a/tools/cluster/tests/evm_jsonrpc_test.go b/tools/cluster/tests/evm_jsonrpc_test.go index 0d0fbb76f4..068d50b7cd 100644 --- a/tools/cluster/tests/evm_jsonrpc_test.go +++ b/tools/cluster/tests/evm_jsonrpc_test.go @@ -17,7 +17,6 @@ import ( "github.com/stretchr/testify/require" "github.com/iotaledger/wasp/clients/chainclient" - "github.com/iotaledger/wasp/packages/evm/evmtest" "github.com/iotaledger/wasp/packages/evm/jsonrpc/jsonrpctest" "github.com/iotaledger/wasp/packages/isc" "github.com/iotaledger/wasp/packages/kv" @@ -36,8 +35,6 @@ type clusterTestEnv struct { } func newClusterTestEnv(t *testing.T, env *ChainEnv, nodeIndex int) *clusterTestEnv { - evmtest.InitGoEthLogger(t) - evmJSONRPCPath := fmt.Sprintf("/v1/chains/%v/evm", env.Chain.ChainID.String()) jsonRPCEndpoint := env.Clu.Config.APIHost(nodeIndex) + evmJSONRPCPath rawClient, err := rpc.DialHTTP(jsonRPCEndpoint)