From ee89ae600f86b8ab1f5de3633b4322f8973e67eb Mon Sep 17 00:00:00 2001 From: Ardit Marku Date: Mon, 4 Nov 2024 13:28:35 +0200 Subject: [PATCH] WIP: Integrate & incorporate flow-go's onchain package --- api/debug.go | 345 +++++++++++++++-- api/encode_transaction.go | 30 +- bootstrap/bootstrap.go | 64 +++- go.mod | 4 + go.sum | 21 ++ services/evm/executor.go | 172 +++++++++ services/ingestion/engine.go | 73 +++- services/ingestion/engine_test.go | 89 ++++- services/replayer/blocks_provider.go | 3 +- services/replayer/call_tracer_collector.go | 290 +++++++++++++++ services/replayer/config.go | 12 + services/requester/requester.go | 408 ++++++--------------- storage/pebble/register.go | 172 +++++++++ tests/e2e_web3js_test.go | 1 - tests/go.mod | 4 + tests/go.sum | 28 +- tests/web3js/eth_rate_limit_test.js | 16 +- 17 files changed, 1332 insertions(+), 400 deletions(-) create mode 100644 services/evm/executor.go create mode 100644 services/replayer/call_tracer_collector.go create mode 100644 services/replayer/config.go create mode 100644 storage/pebble/register.go diff --git a/api/debug.go b/api/debug.go index bf3a0f89..8ef28f33 100644 --- a/api/debug.go +++ b/api/debug.go @@ -2,16 +2,31 @@ package api import ( "context" + "fmt" + "math/big" "github.com/goccy/go-json" + "github.com/onflow/flow-go/fvm/evm/offchain/query" gethCommon "github.com/onflow/go-ethereum/common" "github.com/onflow/go-ethereum/eth/tracers" + "github.com/onflow/go-ethereum/eth/tracers/logger" "github.com/onflow/go-ethereum/rpc" "github.com/rs/zerolog" + "github.com/onflow/flow-evm-gateway/config" "github.com/onflow/flow-evm-gateway/metrics" "github.com/onflow/flow-evm-gateway/models" + errs "github.com/onflow/flow-evm-gateway/models/errors" + "github.com/onflow/flow-evm-gateway/services/evm" + "github.com/onflow/flow-evm-gateway/services/replayer" "github.com/onflow/flow-evm-gateway/storage" + "github.com/onflow/flow-evm-gateway/storage/pebble" + flowEVM "github.com/onflow/flow-go/fvm/evm" + + // this import is needed for side-effects, because the + // tracers.DefaultDirectory is relying on the init function + _ "github.com/onflow/go-ethereum/eth/tracers/js" + _ "github.com/onflow/go-ethereum/eth/tracers/native" ) // txTraceResult is the result of a single transaction trace. @@ -22,77 +37,345 @@ type txTraceResult struct { } type DebugAPI struct { - logger zerolog.Logger - tracer storage.TraceIndexer - blocks storage.BlockIndexer - collector metrics.Collector + store *pebble.Storage + logger zerolog.Logger + tracer storage.TraceIndexer + blocks storage.BlockIndexer + transactions storage.TransactionIndexer + receipts storage.ReceiptIndexer + config *config.Config + collector metrics.Collector } -func NewDebugAPI(tracer storage.TraceIndexer, blocks storage.BlockIndexer, logger zerolog.Logger, collector metrics.Collector) *DebugAPI { +func NewDebugAPI( + store *pebble.Storage, + tracer storage.TraceIndexer, + blocks storage.BlockIndexer, + transactions storage.TransactionIndexer, + receipts storage.ReceiptIndexer, + config *config.Config, + logger zerolog.Logger, + collector metrics.Collector, +) *DebugAPI { return &DebugAPI{ - logger: logger, - tracer: tracer, - blocks: blocks, - collector: collector, + store: store, + logger: logger, + tracer: tracer, + blocks: blocks, + transactions: transactions, + receipts: receipts, + config: config, + collector: collector, } } // TraceTransaction will return a debug execution trace of a transaction if it exists, // currently we only support CALL traces, so the config is ignored. func (d *DebugAPI) TraceTransaction( - _ context.Context, + ctx context.Context, hash gethCommon.Hash, - _ *tracers.TraceConfig, + config *tracers.TraceConfig, ) (json.RawMessage, error) { - res, err := d.tracer.GetTransaction(hash) + receipt, err := d.receipts.GetByTransactionID(hash) + if err != nil { + return nil, err + } + + tracer, err := tracerForReceipt(config, receipt) + if err != nil { + return nil, err + } + + block, err := d.blocks.GetByHeight(receipt.BlockNumber.Uint64()) + if err != nil { + return nil, err + } + // We need to re-execute the given transaction and all the + // transactions that precede it in the same block, based on + // the previous block state, to generate the correct trace. + previousBlock, err := d.blocks.GetByHeight(block.Height - 1) if err != nil { - return handleError[json.RawMessage](err, d.logger, d.collector) + return nil, err + } + + blockExecutor, err := d.executorAtBlock(previousBlock) + if err != nil { + return nil, err + } + + // Re-execute the transactions in the order they appear, for the block + // that contains the given transaction. We set the tracer only for + // the given transaction, as we don't need it for the preceding + // transactions. Once we re-execute the desired transaction, we ignore + // the rest of the transactions in the block, and simply return the trace + // result. + txExecuted := false + var txTracer *tracers.Tracer + for _, h := range block.TransactionHashes { + if txExecuted { + break + } + + tx, err := d.transactions.Get(h) + if err != nil { + return nil, err + } + + if h == hash { + txTracer = tracer + txExecuted = true + } + + _, err = blockExecutor.Run(tx, txTracer) + if err != nil { + return nil, err + } } - return res, nil + + return txTracer.GetResult() } func (d *DebugAPI) TraceBlockByNumber( ctx context.Context, number rpc.BlockNumber, - cfg *tracers.TraceConfig, + config *tracers.TraceConfig, ) ([]*txTraceResult, error) { block, err := d.blocks.GetByHeight(uint64(number.Int64())) if err != nil { - return handleError[[]*txTraceResult](err, d.logger, d.collector) + return nil, err + } + + // We need to re-execute all the transactions from the given block, + // on top of the previous block state, to generate the correct traces. + previousBlock, err := d.blocks.GetByHeight(block.Height - 1) + if err != nil { + return nil, err } - return d.traceBlock(ctx, block, cfg) + blockExecutor, err := d.executorAtBlock(previousBlock) + if err != nil { + return nil, err + } + + results := make([]*txTraceResult, len(block.TransactionHashes)) + for i, h := range block.TransactionHashes { + tx, err := d.transactions.Get(h) + if err != nil { + return nil, err + } + + receipt, err := d.receipts.GetByTransactionID(tx.Hash()) + if err != nil { + return nil, err + } + + tracer, err := tracerForReceipt(config, receipt) + if err != nil { + return nil, err + } + + _, err = blockExecutor.Run(tx, tracer) + if err != nil { + results[i] = &txTraceResult{TxHash: h, Error: err.Error()} + continue + } + + txTrace, err := tracer.GetResult() + if err != nil { + results[i] = &txTraceResult{TxHash: h, Error: err.Error()} + } else { + results[i] = &txTraceResult{TxHash: h, Result: txTrace} + } + } + + return results, nil } func (d *DebugAPI) TraceBlockByHash( ctx context.Context, hash gethCommon.Hash, - cfg *tracers.TraceConfig, + config *tracers.TraceConfig, ) ([]*txTraceResult, error) { block, err := d.blocks.GetByID(hash) if err != nil { - return handleError[[]*txTraceResult](err, d.logger, d.collector) + return nil, err } - return d.traceBlock(ctx, block, cfg) + return d.TraceBlockByNumber(ctx, rpc.BlockNumber(block.Height), config) } -func (d *DebugAPI) traceBlock( +func (d *DebugAPI) TraceCall( ctx context.Context, - block *models.Block, - _ *tracers.TraceConfig, -) ([]*txTraceResult, error) { - results := make([]*txTraceResult, len(block.TransactionHashes)) - for i, h := range block.TransactionHashes { + args TransactionArgs, + blockNrOrHash rpc.BlockNumberOrHash, + config *tracers.TraceCallConfig, +) (interface{}, error) { + tx, err := encodeTxFromArgs(args) + if err != nil { + return nil, err + } + + // Default address in case user does not provide one + from := d.config.Coinbase + if args.From != nil { + from = *args.From + } + + var traceConfig *tracers.TraceConfig + if config != nil { + traceConfig = &config.TraceConfig + } + + tracer, err := tracerForReceipt(traceConfig, nil) + if err != nil { + return nil, err + } + + height, err := d.resolveBlockNumberOrHash(&blockNrOrHash) + if err != nil { + return nil, err + } - txTrace, err := d.TraceTransaction(ctx, h, nil) + block, err := d.blocks.GetByHeight(height) + if err != nil { + return nil, err + } + + ledger := pebble.NewRegister(d.store, block.Height, nil) + blocksProvider := replayer.NewBlocksProvider( + d.blocks, + d.config.FlowNetworkID, + tracer, + ) + viewProvider := query.NewViewProvider( + d.config.FlowNetworkID, + flowEVM.StorageAccountAddress(d.config.FlowNetworkID), + ledger, + blocksProvider, + 120_000_000, + ) + + view, err := viewProvider.GetBlockView(block.Height) + if err != nil { + return nil, err + } + + to := gethCommon.Address{} + if tx.To != nil { + to = *tx.To + } + opts := []query.DryCallOption{} + opts = append(opts, query.WithTracer(tracer)) + if config.StateOverrides != nil { + for addr, overrideAccount := range *config.StateOverrides { + if overrideAccount.Nonce != nil { + opts = append(opts, query.WithStateOverrideNonce(addr, uint64(*overrideAccount.Nonce))) + } + if overrideAccount.Code != nil { + opts = append(opts, query.WithStateOverrideCode(addr, *overrideAccount.Code)) + } + if overrideAccount.Balance != nil { + opts = append(opts, query.WithStateOverrideBalance(addr, (*big.Int)(*overrideAccount.Balance))) + } + if overrideAccount.State != nil { + opts = append(opts, query.WithStateOverrideState(addr, *overrideAccount.State)) + } + if overrideAccount.StateDiff != nil { + opts = append(opts, query.WithStateOverrideStateDiff(addr, *overrideAccount.StateDiff)) + } + } + } + _, err = view.DryCall( + from, + to, + tx.Data, + tx.Value, + tx.Gas, + opts..., + ) + if err != nil { + return nil, err + } + + return tracer.GetResult() +} + +func (d *DebugAPI) executorAtBlock(block *models.Block) (*evm.BlockExecutor, error) { + ledger := pebble.NewRegister(d.store, block.Height, d.store.NewBatch()) + + return evm.NewBlockExecutor( + block, + ledger, + d.config.FlowNetworkID, + d.blocks, + d.receipts, + d.logger, + ) +} + +func (d *DebugAPI) resolveBlockNumberOrHash(block *rpc.BlockNumberOrHash) (uint64, error) { + err := fmt.Errorf("%w: neither block number nor hash specified", errs.ErrInvalid) + if block == nil { + return 0, err + } + if number, ok := block.Number(); ok { + return d.resolveBlockNumber(number) + } + + if hash, ok := block.Hash(); ok { + evmHeight, err := d.blocks.GetHeightByID(hash) if err != nil { - results[i] = &txTraceResult{TxHash: h, Error: err.Error()} - } else { - results[i] = &txTraceResult{TxHash: h, Result: txTrace} + return 0, err } + return evmHeight, nil } - return results, nil + return 0, err +} + +func (d *DebugAPI) resolveBlockNumber(number rpc.BlockNumber) (uint64, error) { + height := number.Int64() + + // if special values (latest) we return latest executed height + if height < 0 { + executed, err := d.blocks.LatestEVMHeight() + if err != nil { + return 0, err + } + height = int64(executed) + } + + return uint64(height), nil +} + +func tracerForReceipt( + config *tracers.TraceConfig, + receipt *models.Receipt, +) (*tracers.Tracer, error) { + tracerCtx := &tracers.Context{} + if receipt != nil { + tracerCtx = &tracers.Context{ + BlockHash: receipt.BlockHash, + BlockNumber: receipt.BlockNumber, + TxIndex: int(receipt.TransactionIndex), + TxHash: receipt.TxHash, + } + } + + if config == nil { + config = &tracers.TraceConfig{} + } + + // Default tracer is the struct logger + if config.Tracer == nil { + logger := logger.NewStructLogger(config.Config) + return &tracers.Tracer{ + Hooks: logger.Hooks(), + GetResult: logger.GetResult, + Stop: logger.Stop, + }, nil + } + + return tracers.DefaultDirectory.New(*config.Tracer, tracerCtx, config.TracerConfig) } diff --git a/api/encode_transaction.go b/api/encode_transaction.go index 44dacec8..b6e4d7a1 100644 --- a/api/encode_transaction.go +++ b/api/encode_transaction.go @@ -1,12 +1,9 @@ package api import ( - "fmt" "math/big" "github.com/onflow/go-ethereum/core/types" - - errs "github.com/onflow/flow-evm-gateway/models/errors" ) const blockGasLimit uint64 = 120_000_000 @@ -16,7 +13,7 @@ const blockGasLimit uint64 = 120_000_000 // `EVM.dryRun` inside Cadence scripts, meaning that no state change // will occur. // This is only useful for `eth_estimateGas` and `eth_call` endpoints. -func encodeTxFromArgs(args TransactionArgs) ([]byte, error) { +func encodeTxFromArgs(args TransactionArgs) (*types.LegacyTx, error) { var data []byte if args.Data != nil { data = *args.Data @@ -36,21 +33,12 @@ func encodeTxFromArgs(args TransactionArgs) ([]byte, error) { value = args.Value.ToInt() } - tx := types.NewTx( - &types.LegacyTx{ - Nonce: 0, - To: args.To, - Value: value, - Gas: gasLimit, - GasPrice: big.NewInt(0), - Data: data, - }, - ) - - enc, err := tx.MarshalBinary() - if err != nil { - return nil, fmt.Errorf("%w: %w", errs.ErrInvalid, err) - } - - return enc, nil + return &types.LegacyTx{ + Nonce: 0, + To: args.To, + Value: value, + Gas: gasLimit, + GasPrice: big.NewInt(0), + Data: data, + }, nil } diff --git a/bootstrap/bootstrap.go b/bootstrap/bootstrap.go index 9398019d..e0f26ec7 100644 --- a/bootstrap/bootstrap.go +++ b/bootstrap/bootstrap.go @@ -11,6 +11,9 @@ import ( "github.com/onflow/flow-go-sdk/access" "github.com/onflow/flow-go-sdk/access/grpc" "github.com/onflow/flow-go-sdk/crypto" + "github.com/onflow/flow-go/fvm/environment" + "github.com/onflow/flow-go/fvm/evm" + flowGo "github.com/onflow/flow-go/model/flow" gethTypes "github.com/onflow/go-ethereum/core/types" "github.com/onflow/go-ethereum/eth/tracers" "github.com/rs/zerolog" @@ -126,27 +129,31 @@ func (b *Bootstrap) StartEventIngestion(ctx context.Context) error { Uint64("missed-heights", latestCadenceBlock.Height-latestCadenceHeight). Msg("indexing cadence height information") + chainID := b.config.FlowNetworkID + // create event subscriber subscriber := ingestion.NewRPCEventSubscriber( b.logger, b.client, - b.config.FlowNetworkID, + chainID, latestCadenceHeight, ) - tracer, err := tracers.DefaultDirectory.New( - callTracerName, - &tracers.Context{}, - json.RawMessage(callTracerConfig), - ) + callTracerCollector, err := replayer.NewCallTracerCollector(b.logger) if err != nil { return err } blocksProvider := replayer.NewBlocksProvider( b.storages.Blocks, - b.config.FlowNetworkID, - tracer, + chainID, + callTracerCollector.TxTracer(), ) + replayerConfig := replayer.Config{ + ChainID: chainID, + RootAddr: evm.StorageAccountAddress(chainID), + CallTracerCollector: callTracerCollector, + ValidateResults: true, + } // initialize event ingestion engine b.events = ingestion.NewEventIngestionEngine( @@ -157,10 +164,12 @@ func (b *Bootstrap) StartEventIngestion(ctx context.Context) error { b.storages.Receipts, b.storages.Transactions, b.storages.Accounts, + b.storages.Traces, b.publishers.Block, b.publishers.Logs, b.logger, b.collector, + replayerConfig, ) StartEngine(ctx, b.events, l) @@ -209,7 +218,23 @@ func (b *Bootstrap) StartAPIServer(ctx context.Context) error { b.logger, ) + tracer, err := tracers.DefaultDirectory.New( + callTracerName, + &tracers.Context{}, + json.RawMessage(callTracerConfig), + ) + if err != nil { + return err + } + blocksProvider := replayer.NewBlocksProvider( + b.storages.Blocks, + b.config.FlowNetworkID, + tracer, + ) + evm, err := requester.NewEVM( + b.storages.Storage, + blocksProvider, b.client, b.config, signer, @@ -269,7 +294,16 @@ func (b *Bootstrap) StartAPIServer(ctx context.Context) error { ratelimiter, ) - var debugAPI = api.NewDebugAPI(b.storages.Traces, b.storages.Blocks, b.logger, b.collector) + debugAPI := api.NewDebugAPI( + b.storages.Storage, + b.storages.Traces, + b.storages.Blocks, + b.storages.Transactions, + b.storages.Receipts, + b.config, + b.logger, + b.collector, + ) var walletAPI *api.WalletAPI if b.config.WalletEnabled { @@ -462,6 +496,18 @@ func setupStorage( return nil, fmt.Errorf("could not fetch provided cadence height, make sure it's correct: %w", err) } + storageProvider := pebble.NewRegister(store, 0, nil) + storageAddress := evm.StorageAccountAddress(config.FlowNetworkID) + accountStatus := environment.NewAccountStatus() + err = storageProvider.SetValue( + storageAddress[:], + []byte(flowGo.AccountStatusKey), + accountStatus.ToBytes(), + ) + if err != nil { + return nil, fmt.Errorf("could not initialize state index: %w", err) + } + if err := blocks.InitHeights(cadenceHeight, cadenceBlock.ID); err != nil { return nil, fmt.Errorf( "failed to init the database for block height: %d and ID: %s, with : %w", diff --git a/go.mod b/go.mod index 8358b3b3..09c4cc00 100644 --- a/go.mod +++ b/go.mod @@ -59,6 +59,8 @@ require ( github.com/dgraph-io/badger/v2 v2.2007.4 // indirect github.com/dgraph-io/ristretto v0.1.0 // indirect github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 // indirect + github.com/dlclark/regexp2 v1.7.0 // indirect + github.com/dop251/goja v0.0.0-20230806174421-c933cf95e127 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/ef-ds/deque v1.0.4 // indirect github.com/ethereum/c-kzg-4844 v1.0.0 // indirect @@ -74,12 +76,14 @@ require ( github.com/go-logr/logr v1.4.1 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-ole/go-ole v1.3.0 // indirect + github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect github.com/gofrs/flock v0.8.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/glog v1.2.0 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb // indirect + github.com/google/pprof v0.0.0-20231229205709-960ae82b1e42 // indirect github.com/google/s2a-go v0.1.7 // indirect github.com/google/uuid v1.6.0 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect diff --git a/go.sum b/go.sum index 2160cd17..1ea7c5d4 100644 --- a/go.sum +++ b/go.sum @@ -91,8 +91,11 @@ github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/logex v1.2.0/go.mod h1:9+9sk7u7pGNWYMkh0hdiL++6OeibzJccyQU4p4MedaY= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/readline v1.5.0/go.mod h1:x22KAscuvRqlLoK9CsoYsmxoXZMMFVyOl86cAH8qUic= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/chzyer/test v0.0.0-20210722231415-061457976a23/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= @@ -153,8 +156,16 @@ github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZm github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= +github.com/dlclark/regexp2 v1.7.0 h1:7lJfhqlPssTb1WQx4yvTHN0uElPEv52sbaECrAQxjAo= +github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/dop251/goja v0.0.0-20211022113120-dc8c55024d06/go.mod h1:R9ET47fwRVRPZnOGvHxxhuZcbrMCuiqOz3Rlrh4KSnk= +github.com/dop251/goja v0.0.0-20230806174421-c933cf95e127 h1:qwcF+vdFrvPSEUDSX5RVoRccG8a5DhOdWdQ4zN62zzo= +github.com/dop251/goja v0.0.0-20230806174421-c933cf95e127/go.mod h1:QMWlm50DNe14hD7t24KEqZuUdC9sOTy8W6XbCU1mlw4= +github.com/dop251/goja_nodejs v0.0.0-20210225215109-d91c329300e7/go.mod h1:hn7BA7c8pLvoGndExHudxTDKZ84Pyvv+90pbBjbTz0Y= +github.com/dop251/goja_nodejs v0.0.0-20211022123610-8dd9abb0616d/go.mod h1:DngW8aVqWbuLRMHItjPUyqdj+HWPvnQe8V8y1nDpIbM= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= @@ -219,6 +230,8 @@ github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM= github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY= +github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU= +github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= @@ -308,6 +321,9 @@ github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20230207041349-798e818bf904/go.mod h1:uglQLonpP8qtYCYyzA+8c/9qtqgA3qsXGYqCPKARAFg= +github.com/google/pprof v0.0.0-20231229205709-960ae82b1e42 h1:dHLYa5D8/Ta0aLR2XcPsrkpAgGeFs6thhMcQK0oQ0n8= +github.com/google/pprof v0.0.0-20231229205709-960ae82b1e42/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= @@ -363,6 +379,7 @@ github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc= github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20220319035150-800ac71e25c2/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= @@ -417,6 +434,8 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxv github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= @@ -591,6 +610,7 @@ github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= @@ -924,6 +944,7 @@ golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/services/evm/executor.go b/services/evm/executor.go new file mode 100644 index 00000000..ff6900b2 --- /dev/null +++ b/services/evm/executor.go @@ -0,0 +1,172 @@ +package evm + +import ( + "fmt" + + "github.com/onflow/atree" + "github.com/onflow/flow-go/fvm/evm" + "github.com/onflow/flow-go/fvm/evm/emulator" + "github.com/onflow/flow-go/fvm/evm/emulator/state" + "github.com/onflow/flow-go/fvm/evm/precompiles" + "github.com/onflow/flow-go/fvm/evm/types" + flowGo "github.com/onflow/flow-go/model/flow" + "github.com/onflow/go-ethereum/common" + gethTypes "github.com/onflow/go-ethereum/core/types" + "github.com/onflow/go-ethereum/eth/tracers" + "github.com/rs/zerolog" + + "github.com/onflow/flow-evm-gateway/models" + "github.com/onflow/flow-evm-gateway/storage" +) + +type BlockExecutor struct { + types.StateDB // todo change to types.ReadOnlyView + emulator types.Emulator + chainID flowGo.ChainID + block *models.Block + blocks storage.BlockIndexer + logger zerolog.Logger + receipts storage.ReceiptIndexer + + // block dynamic data + txIndex uint + gasUsed uint64 +} + +func NewBlockExecutor( + block *models.Block, + ledger atree.Ledger, + chainID flowGo.ChainID, + blocks storage.BlockIndexer, + receipts storage.ReceiptIndexer, + logger zerolog.Logger, +) (*BlockExecutor, error) { + logger = logger.With().Str("component", "state-execution").Logger() + storageAddress := evm.StorageAccountAddress(chainID) + + stateDB, err := state.NewStateDB(ledger, storageAddress) + if err != nil { + return nil, err + } + + return &BlockExecutor{ + emulator: emulator.NewEmulator(ledger, storageAddress), + StateDB: stateDB, + chainID: chainID, + block: block, + blocks: blocks, + receipts: receipts, + logger: logger, + }, nil +} + +func (s *BlockExecutor) Run( + tx models.Transaction, + tracer *tracers.Tracer, +) (*gethTypes.Receipt, error) { + l := s.logger.With().Str("tx-hash", tx.Hash().String()).Logger() + l.Info().Msg("executing new transaction") + + receipt, err := s.receipts.GetByTransactionID(tx.Hash()) + if err != nil { + return nil, err + } + + ctx, err := s.blockContext(receipt) + ctx.Tracer = tracer + if err != nil { + return nil, err + } + + bv, err := s.emulator.NewBlockView(ctx) + if err != nil { + return nil, err + } + + var res *types.Result + + switch t := tx.(type) { + case models.DirectCall: + res, err = bv.DirectCall(t.DirectCall) + case models.TransactionCall: + res, err = bv.RunTransaction(t.Transaction) + default: + return nil, fmt.Errorf("invalid transaction type") + } + + if err != nil { + return nil, err + } + + // we should never produce invalid transaction, since if the transaction was emitted from the evm core + // it must have either been successful or failed, invalid transactions are not emitted + if res.Invalid() { + return nil, fmt.Errorf("invalid transaction %s: %w", tx.Hash(), res.ValidationError) + } + + // increment values as part of a virtual block + s.gasUsed += res.GasConsumed + s.txIndex++ + + l.Debug().Msg("transaction executed successfully") + + return res.LightReceipt().ToReceipt(), nil +} + +// blockContext produces a context that is used by the block view during the execution. +// It can be used for transaction execution and calls. Receipt is not required when +// producing the context for calls. +func (s *BlockExecutor) blockContext(receipt *models.Receipt) (types.BlockContext, error) { + ctx := types.BlockContext{ + ChainID: types.EVMChainIDFromFlowChainID(s.chainID), + BlockNumber: s.block.Height, + BlockTimestamp: s.block.Timestamp, + DirectCallBaseGasUsage: types.DefaultDirectCallBaseGasUsage, + DirectCallGasPrice: types.DefaultDirectCallGasPrice, + GasFeeCollector: types.CoinbaseAddress, + GetHashFunc: func(n uint64) common.Hash { + // For block heights greater than or equal to the current, + // return an empty block hash. + if n >= s.block.Height { + return common.Hash{} + } + // If the given block height, is more than 256 blocks + // in the past, return an empty block hash. + if s.block.Height-n > 256 { + return common.Hash{} + } + + block, err := s.blocks.GetByHeight(n) + if err != nil { + return common.Hash{} + } + blockHash, err := block.Hash() + if err != nil { + return common.Hash{} + } + + return blockHash + }, + Random: s.block.PrevRandao, + TxCountSoFar: s.txIndex, + TotalGasUsedSoFar: s.gasUsed, + Tracer: nil, + } + + // only add precompile cadence arch mocks if we have a receipt, + // in case of call and dry run we don't produce receipts + // todo when a call is made that uses cadence arch precompiles, it will fail, because + // the precompiled contracts won't be set since we don't have a receipt for them + // this failure should be detected and we should in such a case execute a call against the + // EN using an AN + if receipt != nil { + calls, err := types.AggregatedPrecompileCallsFromEncoded(receipt.PrecompiledCalls) + if err != nil { + return types.BlockContext{}, err + } + + ctx.ExtraPrecompiledContracts = precompiles.AggregatedPrecompiledCallsToPrecompiledContracts(calls) + } + + return ctx, nil +} diff --git a/services/ingestion/engine.go b/services/ingestion/engine.go index 6ca1d027..a81f08f0 100644 --- a/services/ingestion/engine.go +++ b/services/ingestion/engine.go @@ -14,6 +14,8 @@ import ( "github.com/onflow/flow-evm-gateway/services/replayer" "github.com/onflow/flow-evm-gateway/storage" "github.com/onflow/flow-evm-gateway/storage/pebble" + + "github.com/onflow/flow-go/fvm/evm/offchain/sync" ) var _ models.Engine = &Engine{} @@ -42,11 +44,13 @@ type Engine struct { receipts storage.ReceiptIndexer transactions storage.TransactionIndexer accounts storage.AccountIndexer + traces storage.TraceIndexer log zerolog.Logger evmLastHeight *models.SequentialHeight blocksPublisher *models.Publisher[*models.Block] logsPublisher *models.Publisher[[]*gethTypes.Log] collector metrics.Collector + replayerConfig replayer.Config } func NewEventIngestionEngine( @@ -57,10 +61,12 @@ func NewEventIngestionEngine( receipts storage.ReceiptIndexer, transactions storage.TransactionIndexer, accounts storage.AccountIndexer, + traces storage.TraceIndexer, blocksPublisher *models.Publisher[*models.Block], logsPublisher *models.Publisher[[]*gethTypes.Log], log zerolog.Logger, collector metrics.Collector, + replayerConfig replayer.Config, ) *Engine { log = log.With().Str("component", "ingestion").Logger() @@ -74,10 +80,12 @@ func NewEventIngestionEngine( receipts: receipts, transactions: transactions, accounts: accounts, + traces: traces, log: log, blocksPublisher: blocksPublisher, logsPublisher: logsPublisher, collector: collector, + replayerConfig: replayerConfig, } } @@ -158,8 +166,47 @@ func (e *Engine) processEvents(events *models.CadenceEvents) error { batch := e.store.NewBatch() defer batch.Close() - // we first index the block - err := e.indexBlock( + // Step 1: Re-execute all transactions on the latest EVM block + + // Step 1.1: Notify the `BlocksProvider` of the newly received EVM block + if err := e.blocksProvider.OnBlockReceived(events.Block()); err != nil { + return err + } + + storageProvider := pebble.NewRegister( + e.store, + events.Block().Height, + batch, + ) + cr := sync.NewReplayer( + e.replayerConfig.ChainID, + e.replayerConfig.RootAddr, + storageProvider, + e.blocksProvider, + e.log, + e.replayerConfig.CallTracerCollector.TxTracer(), + e.replayerConfig.ValidateResults, + ) + + // Step 1.2: Replay all block transactions + // If `ReplayBlock` returns any error, we abort the EVM events processing + res, err := cr.ReplayBlock(events.TxEventPayloads(), events.BlockEventPayload()) + if err != nil { + return fmt.Errorf("failed to replay block on height: %d, with: %w", events.Block().Height, err) + } + + // Step 2: Write all the necessary changes to each storage + + // Step 2.1: Write all the EVM state changes to `StorageProvider` + for k, v := range res.StorageRegisterUpdates() { + err = storageProvider.SetValue([]byte(k.Owner), []byte(k.Key), v) + if err != nil { + return fmt.Errorf("failed to commit state changes on block: %d", events.Block().Height) + } + } + + // Step 2.2: Write the latest EVM block to `Blocks` storage + err = e.indexBlock( events.CadenceHeight(), events.CadenceBlockID(), events.Block(), @@ -169,6 +216,8 @@ func (e *Engine) processEvents(events *models.CadenceEvents) error { return fmt.Errorf("failed to index block %d event: %w", events.Block().Height, err) } + // Step 2.3: Write all EVM transactions of the current block, + // to `Transactions` storage for i, tx := range events.Transactions() { receipt := events.Receipts()[i] @@ -178,17 +227,25 @@ func (e *Engine) processEvents(events *models.CadenceEvents) error { } } + // Step 2.4: Write all EVM transaction receipts of the current block, + // to `Receipts` storage err = e.indexReceipts(events.Receipts(), batch) if err != nil { return fmt.Errorf("failed to index receipts for block %d event: %w", events.Block().Height, err) } - if err := e.blocksProvider.OnBlockReceived(events.Block()); err != nil { - return fmt.Errorf( - "failed to call OnBlockReceived for block %d, with: %w", - events.Block().Height, - err, - ) + traceCollector := e.replayerConfig.CallTracerCollector + for _, tx := range events.Transactions() { + txHash := tx.Hash() + traceResult, err := traceCollector.Collect(txHash) + if err != nil { + return err + } + + err = e.traces.StoreTransaction(txHash, traceResult, batch) + if err != nil { + return err + } } if err := batch.Commit(pebbleDB.Sync); err != nil { diff --git a/services/ingestion/engine_test.go b/services/ingestion/engine_test.go index ff87ea6f..7eb14c67 100644 --- a/services/ingestion/engine_test.go +++ b/services/ingestion/engine_test.go @@ -3,10 +3,13 @@ package ingestion import ( "context" "encoding/hex" + "encoding/json" "math/big" "testing" pebbleDB "github.com/cockroachdb/pebble" + "github.com/onflow/flow-go/fvm/environment" + "github.com/onflow/flow-go/fvm/evm" "github.com/onflow/flow-go/fvm/evm/events" flowGo "github.com/onflow/flow-go/model/flow" @@ -32,13 +35,13 @@ import ( ) func TestSerialBlockIngestion(t *testing.T) { + t.Run("successfully ingest serial blocks", func(t *testing.T) { receipts := &storageMock.ReceiptIndexer{} transactions := &storageMock.TransactionIndexer{} latestHeight := uint64(10) - store, err := pebble.New(t.TempDir(), zerolog.Nop()) - require.NoError(t, err) + store := setupStore(t) blocks := &storageMock.BlockIndexer{} blocks. @@ -53,6 +56,8 @@ func TestSerialBlockIngestion(t *testing.T) { On("Update"). Return(func() error { return nil }) + traces := &storageMock.TraceIndexer{} + eventsChan := make(chan models.BlockEvents) subscriber := &mocks.EventSubscriber{} @@ -70,10 +75,12 @@ func TestSerialBlockIngestion(t *testing.T) { receipts, transactions, accounts, + traces, models.NewPublisher[*models.Block](), models.NewPublisher[[]*gethTypes.Log](), zerolog.Nop(), metrics.NopCollector, + defaultReplayerConfig(), ) done := make(chan struct{}) @@ -119,8 +126,7 @@ func TestSerialBlockIngestion(t *testing.T) { transactions := &storageMock.TransactionIndexer{} latestHeight := uint64(10) - store, err := pebble.New(t.TempDir(), zerolog.Nop()) - require.NoError(t, err) + store := setupStore(t) blocks := &storageMock.BlockIndexer{} blocks. @@ -135,6 +141,8 @@ func TestSerialBlockIngestion(t *testing.T) { On("Update", mock.Anything, mock.Anything). Return(func(t models.TransactionCall, r *gethTypes.Receipt) error { return nil }) + traces := &storageMock.TraceIndexer{} + eventsChan := make(chan models.BlockEvents) subscriber := &mocks.EventSubscriber{} subscriber. @@ -151,10 +159,12 @@ func TestSerialBlockIngestion(t *testing.T) { receipts, transactions, accounts, + traces, models.NewPublisher[*models.Block](), models.NewPublisher[[]*gethTypes.Log](), zerolog.Nop(), metrics.NopCollector, + defaultReplayerConfig(), ) waitErr := make(chan struct{}) @@ -162,7 +172,7 @@ func TestSerialBlockIngestion(t *testing.T) { go func() { err := engine.Run(context.Background()) assert.ErrorIs(t, err, models.ErrInvalidHeight) - assert.EqualError(t, err, "failed to index block 20 event: invalid block height, expected 11, got 20: invalid height") + assert.EqualError(t, err, "invalid height: received new block: 20, non-sequential of latest block: 11") close(waitErr) }() @@ -216,6 +226,7 @@ func TestSerialBlockIngestion(t *testing.T) { } func TestBlockAndTransactionIngestion(t *testing.T) { + t.Run("successfully ingest transaction and block", func(t *testing.T) { receipts := &storageMock.ReceiptIndexer{} transactions := &storageMock.TransactionIndexer{} @@ -223,8 +234,7 @@ func TestBlockAndTransactionIngestion(t *testing.T) { nextHeight := latestHeight + 1 blockID := flow.Identifier{0x01} - store, err := pebble.New(t.TempDir(), zerolog.Nop()) - require.NoError(t, err) + store := setupStore(t) blocks := &storageMock.BlockIndexer{} blocks. @@ -259,6 +269,14 @@ func TestBlockAndTransactionIngestion(t *testing.T) { blockCdc, block, blockEvent, err := newBlock(nextHeight, []gethCommon.Hash{result.TxHash}) require.NoError(t, err) + traces := &storageMock.TraceIndexer{} + traces. + On("StoreTransaction", mock.AnythingOfType("common.Hash"), mock.AnythingOfType("json.RawMessage"), mock.Anything). + Return(func(txID gethCommon.Hash, trace json.RawMessage, batch *pebbleDB.Batch) error { + assert.Equal(t, transaction.Hash(), txID) + return nil + }) + engine := NewEventIngestionEngine( subscriber, replayer.NewBlocksProvider(blocks, flowGo.Emulator, nil), @@ -267,10 +285,12 @@ func TestBlockAndTransactionIngestion(t *testing.T) { receipts, transactions, accounts, + traces, models.NewPublisher[*models.Block](), models.NewPublisher[[]*gethTypes.Log](), zerolog.Nop(), metrics.NopCollector, + defaultReplayerConfig(), ) done := make(chan struct{}) @@ -333,8 +353,7 @@ func TestBlockAndTransactionIngestion(t *testing.T) { latestHeight := uint64(10) nextHeight := latestHeight + 1 - store, err := pebble.New(t.TempDir(), zerolog.Nop()) - require.NoError(t, err) + store := setupStore(t) blocks := &storageMock.BlockIndexer{} blocks. @@ -358,11 +377,19 @@ func TestBlockAndTransactionIngestion(t *testing.T) { return eventsChan }) - txCdc, txEvent, _, res, err := newTransaction(nextHeight) + txCdc, txEvent, transaction, res, err := newTransaction(nextHeight) require.NoError(t, err) blockCdc, _, blockEvent, err := newBlock(nextHeight, []gethCommon.Hash{res.TxHash}) require.NoError(t, err) + traces := &storageMock.TraceIndexer{} + traces. + On("StoreTransaction", mock.AnythingOfType("common.Hash"), mock.AnythingOfType("json.RawMessage"), mock.Anything). + Return(func(txID gethCommon.Hash, trace json.RawMessage, batch *pebbleDB.Batch) error { + assert.Equal(t, transaction.Hash(), txID) + return nil + }) + engine := NewEventIngestionEngine( subscriber, replayer.NewBlocksProvider(blocks, flowGo.Emulator, nil), @@ -371,10 +398,12 @@ func TestBlockAndTransactionIngestion(t *testing.T) { receipts, transactions, accounts, + traces, models.NewPublisher[*models.Block](), models.NewPublisher[[]*gethTypes.Log](), zerolog.Nop(), metrics.NopCollector, + defaultReplayerConfig(), ) done := make(chan struct{}) @@ -434,8 +463,7 @@ func TestBlockAndTransactionIngestion(t *testing.T) { transactions := &storageMock.TransactionIndexer{} latestCadenceHeight := uint64(0) - store, err := pebble.New(t.TempDir(), zerolog.Nop()) - require.NoError(t, err) + store := setupStore(t) blocks := &storageMock.BlockIndexer{} blocks. @@ -450,6 +478,8 @@ func TestBlockAndTransactionIngestion(t *testing.T) { On("Update", mock.Anything, mock.AnythingOfType("*models.Receipt"), mock.Anything). Return(func(t models.Transaction, r *models.Receipt, _ *pebbleDB.Batch) error { return nil }) + traces := &storageMock.TraceIndexer{} + eventsChan := make(chan models.BlockEvents) subscriber := &mocks.EventSubscriber{} subscriber. @@ -467,10 +497,12 @@ func TestBlockAndTransactionIngestion(t *testing.T) { receipts, transactions, accounts, + traces, models.NewPublisher[*models.Block](), models.NewPublisher[[]*gethTypes.Log](), zerolog.Nop(), metrics.NopCollector, + defaultReplayerConfig(), ) done := make(chan struct{}) @@ -508,6 +540,13 @@ func TestBlockAndTransactionIngestion(t *testing.T) { Return(func(receipts []*models.Receipt, _ *pebbleDB.Batch) error { return nil }). Once() + traces. + On("StoreTransaction", mock.AnythingOfType("common.Hash"), mock.AnythingOfType("json.RawMessage"), mock.Anything). + Return(func(txID gethCommon.Hash, trace json.RawMessage, batch *pebbleDB.Batch) error { + assert.Equal(t, transaction.Hash(), txID) + return nil + }) + events = append(events, flow.Event{ Type: string(txEvent.Etype), Value: txCdc, @@ -609,3 +648,29 @@ func newTransaction(height uint64) (cadence.Event, *events.Event, models.Transac cdcEv, err := ev.Payload.ToCadence(flowGo.Previewnet) return cdcEv, ev, models.TransactionCall{Transaction: tx}, res, err } + +func defaultReplayerConfig() replayer.Config { + return replayer.Config{ + ChainID: flowGo.Emulator, + RootAddr: evm.StorageAccountAddress(flowGo.Emulator), + CallTracerCollector: replayer.NopTracer, + ValidateResults: false, + } +} + +func setupStore(t *testing.T) *pebble.Storage { + store, err := pebble.New(t.TempDir(), zerolog.Nop()) + require.NoError(t, err) + + storageProvider := pebble.NewRegister(store, 0, nil) + storageAddress := evm.StorageAccountAddress(flowGo.Emulator) + accountStatus := environment.NewAccountStatus() + err = storageProvider.SetValue( + storageAddress[:], + []byte(flowGo.AccountStatusKey), + accountStatus.ToBytes(), + ) + require.NoError(t, err) + + return store +} diff --git a/services/replayer/blocks_provider.go b/services/replayer/blocks_provider.go index f87d398d..d43749a0 100644 --- a/services/replayer/blocks_provider.go +++ b/services/replayer/blocks_provider.go @@ -78,7 +78,8 @@ func NewBlocksProvider( func (bp *BlocksProvider) OnBlockReceived(block *models.Block) error { if bp.latestBlock != nil && bp.latestBlock.Height != (block.Height-1) { return fmt.Errorf( - "received new block: %d, non-sequential of latest block: %d", + "%w: received new block: %d, non-sequential of latest block: %d", + models.ErrInvalidHeight, block.Height, bp.latestBlock.Height, ) diff --git a/services/replayer/call_tracer_collector.go b/services/replayer/call_tracer_collector.go new file mode 100644 index 00000000..464558b7 --- /dev/null +++ b/services/replayer/call_tracer_collector.go @@ -0,0 +1,290 @@ +package replayer + +import ( + "encoding/json" + "fmt" + "math/big" + + "github.com/onflow/go-ethereum/common" + "github.com/onflow/go-ethereum/core/tracing" + "github.com/onflow/go-ethereum/core/types" + "github.com/onflow/go-ethereum/eth/tracers" + "github.com/rs/zerolog" + + // this import is needed for side-effects, because the + // tracers.DefaultDirectory is relying on the init function + _ "github.com/onflow/go-ethereum/eth/tracers/native" +) + +const ( + tracerConfig = `{"onlyTopCall":true}` + tracerName = "callTracer" +) + +func DefaultCallTracer() (*tracers.Tracer, error) { + tracer, err := tracers.DefaultDirectory.New( + tracerName, + &tracers.Context{}, + json.RawMessage(tracerConfig), + ) + if err != nil { + return nil, err + } + + return tracer, nil +} + +type EVMTracer interface { + TxTracer() *tracers.Tracer + ResetTracer() error + Collect(txID common.Hash) (json.RawMessage, error) +} + +type CallTracerCollector struct { + tracer *tracers.Tracer + resultsByTxID map[common.Hash]json.RawMessage + logger zerolog.Logger +} + +var _ EVMTracer = (*CallTracerCollector)(nil) + +func NewCallTracerCollector(logger zerolog.Logger) ( + *CallTracerCollector, + error, +) { + tracer, err := tracers.DefaultDirectory.New( + tracerName, + &tracers.Context{}, + json.RawMessage(tracerConfig), + ) + if err != nil { + return nil, err + } + + return &CallTracerCollector{ + tracer: tracer, + resultsByTxID: make(map[common.Hash]json.RawMessage), + logger: logger.With().Str("component", "evm-tracer").Logger(), + }, nil +} + +func (t *CallTracerCollector) TxTracer() *tracers.Tracer { + return NewSafeTxTracer(t) +} + +func (t *CallTracerCollector) ResetTracer() error { + var err error + t.tracer, err = tracers.DefaultDirectory.New( + tracerName, + &tracers.Context{}, + json.RawMessage(tracerConfig), + ) + return err +} + +func (ct *CallTracerCollector) Collect(txID common.Hash) (json.RawMessage, error) { + // collect the trace result + result, found := ct.resultsByTxID[txID] + if !found { + return nil, fmt.Errorf("trace result not found") + } + + // remove the result + delete(ct.resultsByTxID, txID) + + return result, nil +} + +func NewSafeTxTracer(ct *CallTracerCollector) *tracers.Tracer { + wrapped := &tracers.Tracer{ + Hooks: &tracing.Hooks{}, + GetResult: ct.tracer.GetResult, + Stop: ct.tracer.Stop, + } + + l := ct.logger + + wrapped.OnTxStart = func( + vm *tracing.VMContext, + tx *types.Transaction, + from common.Address, + ) { + defer func() { + if r := recover(); r != nil { + err, ok := r.(error) + if !ok { + err = fmt.Errorf("panic: %v", r) + } + l.Err(err). + Stack(). + Msg("OnTxStart trace collection failed") + } + }() + l.Debug().Msg("tracing OnTxStart is called") + if ct.tracer.OnTxStart != nil { + ct.tracer.OnTxStart(vm, tx, from) + } + // reset tracing to have fresh state + if err := ct.ResetTracer(); err != nil { + l.Error().Err(err). + Msg("failed to reset tracer") + return + } + } + + wrapped.OnTxEnd = func(receipt *types.Receipt, err error) { + defer func() { + if r := recover(); r != nil { + err, ok := r.(error) + if !ok { + err = fmt.Errorf("panic: %v", r) + } + l.Err(err). + Stack(). + Msg("OnTxEnd trace collection failed") + } + }() + l.Debug().Msg("tracing OnTxEnd is called") + if ct.tracer.OnTxEnd != nil { + ct.tracer.OnTxEnd(receipt, err) + } + + // collect results for the tracer + res, err := ct.tracer.GetResult() + if err != nil { + l.Error().Err(err).Msg("failed to produce trace results") + return + } + ct.resultsByTxID[receipt.TxHash] = res + } + + wrapped.OnEnter = func( + depth int, + typ byte, + from, to common.Address, + input []byte, + gas uint64, + value *big.Int, + ) { + defer func() { + if r := recover(); r != nil { + err, ok := r.(error) + if !ok { + err = fmt.Errorf("panic: %v", r) + } + l.Err(err). + Stack(). + Msg("OnEnter trace collection failed") + } + }() + l.Debug().Int("depth", depth).Msg("tracing OnEnter is called") + if ct.tracer.OnEnter != nil { + ct.tracer.OnEnter(depth, typ, from, to, input, gas, value) + } + } + + wrapped.OnExit = func(depth int, output []byte, gasUsed uint64, err error, reverted bool) { + defer func() { + if r := recover(); r != nil { + err, ok := r.(error) + if !ok { + err = fmt.Errorf("panic: %v", r) + } + l.Err(err). + Stack(). + Msg("OnExit trace collection failed") + } + }() + l.Debug().Int("depth", depth).Msg("tracing OnExit is called") + if ct.tracer.OnExit != nil { + ct.tracer.OnExit(depth, output, gasUsed, err, reverted) + } + } + + wrapped.OnOpcode = func( + pc uint64, + op byte, + gas, cost uint64, + scope tracing.OpContext, + rData []byte, + depth int, + err error, + ) { + defer func() { + if r := recover(); r != nil { + err, ok := r.(error) + if !ok { + err = fmt.Errorf("panic: %v", r) + } + l.Err(err). + Stack(). + Msg("OnOpcode trace collection failed") + } + }() + l.Debug().Msg("tracing OnOpcode is called") + if ct.tracer.OnOpcode != nil { + ct.tracer.OnOpcode(pc, op, gas, cost, scope, rData, depth, err) + } + } + wrapped.OnFault = func( + pc uint64, + op byte, + gas, cost uint64, + scope tracing.OpContext, + depth int, + err error) { + defer func() { + if r := recover(); r != nil { + err, ok := r.(error) + if !ok { + err = fmt.Errorf("panic: %v", r) + } + l.Err(err). + Stack(). + Msg("OnFault trace collection failed") + } + }() + l.Debug().Msg("tracing OnFault is called") + if ct.tracer.OnFault != nil { + ct.tracer.OnFault(pc, op, gas, cost, scope, depth, err) + } + } + + wrapped.OnGasChange = func(old, new uint64, reason tracing.GasChangeReason) { + defer func() { + if r := recover(); r != nil { + err, ok := r.(error) + if !ok { + err = fmt.Errorf("panic: %v", r) + } + l.Err(err). + Stack(). + Msg("OnGasChange trace collection failed") + } + }() + l.Debug().Msg("tracing OnGasChange is called") + if ct.tracer.OnGasChange != nil { + ct.tracer.OnGasChange(old, new, reason) + } + } + + return wrapped +} + +var NopTracer = &nopTracer{} + +var _ EVMTracer = &nopTracer{} + +type nopTracer struct{} + +func (n nopTracer) TxTracer() *tracers.Tracer { + return nil +} + +func (n nopTracer) Collect(_ common.Hash) (json.RawMessage, error) { + return nil, nil +} + +func (n nopTracer) ResetTracer() error { + return nil +} diff --git a/services/replayer/config.go b/services/replayer/config.go new file mode 100644 index 00000000..72fb0a88 --- /dev/null +++ b/services/replayer/config.go @@ -0,0 +1,12 @@ +package replayer + +import ( + "github.com/onflow/flow-go/model/flow" +) + +type Config struct { + ChainID flow.ChainID + RootAddr flow.Address + CallTracerCollector EVMTracer + ValidateResults bool +} diff --git a/services/requester/requester.go b/services/requester/requester.go index 40751754..2df729b9 100644 --- a/services/requester/requester.go +++ b/services/requester/requester.go @@ -4,7 +4,6 @@ import ( "context" _ "embed" "encoding/hex" - "errors" "fmt" "math" "math/big" @@ -15,12 +14,10 @@ import ( "github.com/hashicorp/golang-lru/v2/expirable" "github.com/onflow/cadence" "github.com/onflow/flow-go-sdk" - "github.com/onflow/flow-go-sdk/access/grpc" "github.com/onflow/flow-go-sdk/crypto" "github.com/onflow/flow-go/fvm/evm" "github.com/onflow/flow-go/fvm/evm/emulator" - "github.com/onflow/flow-go/fvm/evm/emulator/state" - evmImpl "github.com/onflow/flow-go/fvm/evm/impl" + "github.com/onflow/flow-go/fvm/evm/offchain/query" evmTypes "github.com/onflow/flow-go/fvm/evm/types" "github.com/onflow/flow-go/fvm/systemcontracts" "github.com/onflow/go-ethereum/common" @@ -33,7 +30,11 @@ import ( "github.com/onflow/flow-evm-gateway/metrics" "github.com/onflow/flow-evm-gateway/models" errs "github.com/onflow/flow-evm-gateway/models/errors" + "github.com/onflow/flow-evm-gateway/services/replayer" "github.com/onflow/flow-evm-gateway/storage" + "github.com/onflow/flow-evm-gateway/storage/pebble" + + gethParams "github.com/onflow/go-ethereum/params" ) var ( @@ -95,12 +96,12 @@ type Requester interface { // Call executes the given signed transaction data on the state for the given EVM block height. // Note, this function doesn't make and changes in the state/blockchain and is // useful to execute and retrieve values. - Call(ctx context.Context, data []byte, from common.Address, evmHeight int64) ([]byte, error) + Call(ctx context.Context, tx *types.LegacyTx, from common.Address, evmHeight int64) ([]byte, error) // EstimateGas executes the given signed transaction data on the state for the given EVM block height. // Note, this function doesn't make any changes in the state/blockchain and is // useful to executed and retrieve the gas consumption and possible failures. - EstimateGas(ctx context.Context, data []byte, from common.Address, evmHeight int64) (uint64, error) + EstimateGas(ctx context.Context, tx *types.LegacyTx, from common.Address, evmHeight int64) (uint64, error) // GetNonce gets nonce from the network at the given EVM block height. GetNonce(ctx context.Context, address common.Address, evmHeight int64) (uint64, error) @@ -119,14 +120,16 @@ type Requester interface { var _ Requester = &EVM{} type EVM struct { - client *CrossSporkClient - config *config.Config - signer crypto.Signer - txPool *TxPool - logger zerolog.Logger - blocks storage.BlockIndexer - mux sync.Mutex - scriptCache *expirable.LRU[string, cadence.Value] + store *pebble.Storage + blocksProvider *replayer.BlocksProvider + client *CrossSporkClient + config *config.Config + signer crypto.Signer + txPool *TxPool + logger zerolog.Logger + blocks storage.BlockIndexer + mux sync.Mutex + scriptCache *expirable.LRU[string, cadence.Value] head *types.Header evmSigner types.Signer @@ -136,6 +139,8 @@ type EVM struct { } func NewEVM( + store *pebble.Storage, + blocksProvider *replayer.BlocksProvider, client *CrossSporkClient, config *config.Config, signer crypto.Signer, @@ -194,6 +199,8 @@ func NewEVM( } evm := &EVM{ + store: store, + blocksProvider: blocksProvider, client: client, config: config, signer: signer, @@ -341,45 +348,12 @@ func (e *EVM) GetBalance( address common.Address, evmHeight int64, ) (*big.Int, error) { - hexEncodedAddress, err := addressToCadenceString(address) - if err != nil { - return nil, err - } - - height, err := e.evmToCadenceHeight(evmHeight) + view, err := e.getBlockView(uint64(evmHeight)) if err != nil { return nil, err } - val, err := e.executeScriptAtHeight( - ctx, - getBalance, - height, - []cadence.Value{hexEncodedAddress}, - ) - if err != nil { - if !errors.Is(err, errs.ErrHeightOutOfRange) { - e.logger.Error(). - Err(err). - Str("address", address.String()). - Int64("evm-height", evmHeight). - Uint64("cadence-height", height). - Msg("failed to get get balance") - } - return nil, fmt.Errorf( - "failed to get balance of address: %s at height: %d, with: %w", - address, - evmHeight, - err, - ) - } - - // sanity check, should never occur - if _, ok := val.(cadence.UInt); !ok { - return nil, fmt.Errorf("failed to convert balance %v to UInt, got type: %T", val, val) - } - - return val.(cadence.UInt).Big(), nil + return view.GetBalance(address) } func (e *EVM) GetNonce( @@ -387,79 +361,12 @@ func (e *EVM) GetNonce( address common.Address, evmHeight int64, ) (uint64, error) { - hexEncodedAddress, err := addressToCadenceString(address) - if err != nil { - return 0, err - } - - height, err := e.evmToCadenceHeight(evmHeight) + view, err := e.getBlockView(uint64(evmHeight)) if err != nil { return 0, err } - val, err := e.executeScriptAtHeight( - ctx, - getNonce, - height, - []cadence.Value{hexEncodedAddress}, - ) - if err != nil { - if !errors.Is(err, errs.ErrHeightOutOfRange) { - e.logger.Error().Err(err). - Str("address", address.String()). - Int64("evm-height", evmHeight). - Uint64("cadence-height", height). - Msg("failed to get nonce") - } - return 0, fmt.Errorf( - "failed to get nonce of address: %s at height: %d, with: %w", - address, - evmHeight, - err, - ) - } - - // sanity check, should never occur - if _, ok := val.(cadence.UInt64); !ok { - return 0, fmt.Errorf("failed to convert nonce %v to UInt64, got type: %T", val, val) - } - - nonce := uint64(val.(cadence.UInt64)) - - e.logger.Debug(). - Uint64("nonce", nonce). - Int64("evm-height", evmHeight). - Uint64("cadence-height", height). - Msg("get nonce executed") - - return nonce, nil -} - -func (e *EVM) stateAt(evmHeight int64) (*state.StateDB, error) { - cadenceHeight, err := e.evmToCadenceHeight(evmHeight) - if err != nil { - return nil, err - } - - if cadenceHeight == LatestBlockHeight { - h, err := e.client.GetLatestBlockHeader(context.Background(), true) - if err != nil { - return nil, err - } - cadenceHeight = h.Height - } - - exeClient, ok := e.client.Client.(*grpc.Client) - if !ok { - return nil, fmt.Errorf("could not convert to execution client") - } - ledger, err := newRemoteLedger(exeClient.ExecutionDataRPCClient(), cadenceHeight) - if err != nil { - return nil, fmt.Errorf("could not create remote ledger for height: %d, with: %w", cadenceHeight, err) - } - - storageAddress := evm.StorageAccountAddress(e.config.FlowNetworkID) - return state.NewStateDB(ledger, storageAddress) + return view.GetNonce(address) } func (e *EVM) GetStorageAt( @@ -468,125 +375,104 @@ func (e *EVM) GetStorageAt( hash common.Hash, evmHeight int64, ) (common.Hash, error) { - stateDB, err := e.stateAt(evmHeight) + view, err := e.getBlockView(uint64(evmHeight)) if err != nil { return common.Hash{}, err } - result := stateDB.GetState(address, hash) - return result, stateDB.Error() + return view.GetSlab(address, hash) } func (e *EVM) Call( ctx context.Context, - data []byte, + tx *types.LegacyTx, from common.Address, evmHeight int64, ) ([]byte, error) { - hexEncodedTx, err := cadence.NewString(hex.EncodeToString(data)) + view, err := e.getBlockView(uint64(evmHeight)) if err != nil { return nil, err } - hexEncodedAddress, err := addressToCadenceString(from) - if err != nil { - return nil, err + to := common.Address{} + if tx.To != nil { + to = *tx.To } - - height, err := e.evmToCadenceHeight(evmHeight) - if err != nil { - return nil, err - } - - scriptResult, err := e.executeScriptAtHeight( - ctx, - dryRun, - height, - []cadence.Value{hexEncodedTx, hexEncodedAddress}, + result, err := view.DryCall( + from, + to, + tx.Data, + tx.Value, + tx.Gas, ) - if err != nil { - if !errors.Is(err, errs.ErrHeightOutOfRange) { - e.logger.Error(). - Err(err). - Uint64("cadence-height", height). - Int64("evm-height", evmHeight). - Str("from", from.String()). - Str("data", hex.EncodeToString(data)). - Msg("failed to execute call") - } - return nil, fmt.Errorf("failed to execute script at height: %d, with: %w", height, err) - } - evmResult, err := parseResult(scriptResult) - if err != nil { - return nil, err + resultSummary := result.ResultSummary() + if resultSummary.ErrorCode != 0 { + if resultSummary.ErrorCode == evmTypes.ExecutionErrCodeExecutionReverted { + return nil, errs.NewRevertError(resultSummary.ReturnedData) + } + return nil, errs.NewFailedTransactionError(resultSummary.ErrorMessage) } - result := evmResult.ReturnedData - - e.logger.Debug(). - Str("result", hex.EncodeToString(result)). - Int64("evm-height", evmHeight). - Uint64("cadence-height", height). - Msg("call executed") - - return result, nil + return result.ReturnedData, err } func (e *EVM) EstimateGas( ctx context.Context, - data []byte, + tx *types.LegacyTx, from common.Address, evmHeight int64, ) (uint64, error) { - hexEncodedTx, err := cadence.NewString(hex.EncodeToString(data)) + view, err := e.getBlockView(uint64(evmHeight)) if err != nil { return 0, err } - hexEncodedAddress, err := addressToCadenceString(from) - if err != nil { - return 0, err + to := common.Address{} + if tx.To != nil { + to = *tx.To } - - height, err := e.evmToCadenceHeight(evmHeight) + result, err := view.DryCall( + from, + to, + tx.Data, + tx.Value, + tx.Gas, + ) if err != nil { return 0, err } - scriptResult, err := e.executeScriptAtHeight( - ctx, - dryRun, - height, - []cadence.Value{hexEncodedTx, hexEncodedAddress}, - ) - if err != nil { - if !errors.Is(err, errs.ErrHeightOutOfRange) { - e.logger.Error(). - Err(err). - Uint64("cadence-height", height). - Int64("evm-height", evmHeight). - Str("from", from.String()). - Str("data", hex.EncodeToString(data)). - Msg("failed to execute estimateGas") + resultSummary := result.ResultSummary() + if resultSummary.ErrorCode != 0 { + if resultSummary.ErrorCode == evmTypes.ExecutionErrCodeExecutionReverted { + return 0, errs.NewRevertError(resultSummary.ReturnedData) } - return 0, fmt.Errorf("failed to execute script at height: %d, with: %w", height, err) + return 0, errs.NewFailedTransactionError(resultSummary.ErrorMessage) } - evmResult, err := parseResult(scriptResult) - if err != nil { - return 0, err - } + if result.Successful() { + // As mentioned in https://github.com/ethereum/EIPs/blob/master/EIPS/eip-150.md#specification + // Define "all but one 64th" of N as N - floor(N / 64). + // If a call asks for more gas than the maximum allowed amount + // (i.e. the total amount of gas remaining in the parent after subtracting + // the gas cost of the call and memory expansion), do not return an OOG error; + // instead, if a call asks for more gas than all but one 64th of the maximum + // allowed amount, call with all but one 64th of the maximum allowed amount of + // gas (this is equivalent to a version of EIP-901 plus EIP-1142). + // CREATE only provides all but one 64th of the parent gas to the child call. + result.GasConsumed = AddOne64th(result.GasConsumed) - gasConsumed := evmResult.GasConsumed + // Adding `gethParams.SstoreSentryGasEIP2200` is needed for this condition: + // https://github.com/onflow/go-ethereum/blob/master/core/vm/operations_acl.go#L29-L32 + result.GasConsumed += gethParams.SstoreSentryGasEIP2200 - e.logger.Debug(). - Uint64("gas", gasConsumed). - Int64("evm-height", evmHeight). - Uint64("cadence-height", height). - Msg("estimateGas executed") + // Take into account any gas refunds, which are calculated only after + // transaction execution. + result.GasConsumed += result.GasRefund + } - return gasConsumed, nil + return result.GasConsumed, err } func (e *EVM) GetCode( @@ -594,53 +480,12 @@ func (e *EVM) GetCode( address common.Address, evmHeight int64, ) ([]byte, error) { - hexEncodedAddress, err := addressToCadenceString(address) + view, err := e.getBlockView(uint64(evmHeight)) if err != nil { return nil, err } - height, err := e.evmToCadenceHeight(evmHeight) - if err != nil { - return nil, err - } - - value, err := e.executeScriptAtHeight( - ctx, - getCode, - height, - []cadence.Value{hexEncodedAddress}, - ) - if err != nil { - if !errors.Is(err, errs.ErrHeightOutOfRange) { - e.logger.Error(). - Err(err). - Uint64("cadence-height", height). - Int64("evm-height", evmHeight). - Str("address", address.String()). - Msg("failed to get code") - } - - return nil, fmt.Errorf( - "failed to execute script for get code of address: %s at height: %d, with: %w", - address, - height, - err, - ) - } - - code, err := cadenceStringToBytes(value) - if err != nil { - return nil, err - } - - e.logger.Debug(). - Str("address", address.Hex()). - Int64("evm-height", evmHeight). - Uint64("cadence-height", height). - Str("code size", fmt.Sprintf("%d", len(code))). - Msg("get code executed") - - return code, nil + return view.GetCode(address) } func (e *EVM) GetLatestEVMHeight(ctx context.Context) (uint64, error) { @@ -716,37 +561,6 @@ func (e *EVM) replaceAddresses(script []byte) []byte { return []byte(s) } -func (e *EVM) evmToCadenceHeight(height int64) (uint64, error) { - if height < 0 { - return LatestBlockHeight, nil - } - - evmHeight := uint64(height) - evmLatest, err := e.blocks.LatestEVMHeight() - if err != nil { - return 0, fmt.Errorf( - "failed to map evm height: %d to cadence height, getting latest evm height: %w", - evmHeight, - err, - ) - } - - // if provided evm height equals to latest evm height indexed we - // return latest height special value to signal requester to execute - // script at the latest block, not at the cadence height we get from the - // index, that is because at that point the height might already be pruned - if evmHeight == evmLatest { - return LatestBlockHeight, nil - } - - cadenceHeight, err := e.blocks.GetCadenceHeight(uint64(evmHeight)) - if err != nil { - return 0, fmt.Errorf("failed to map evm height: %d to cadence height: %w", evmHeight, err) - } - - return cadenceHeight, nil -} - // executeScriptAtHeight will execute the given script, at the given // block height, with the given arguments. A height of `LatestBlockHeight` // (math.MaxUint64 - 1) is a special value, which means the script will be @@ -807,45 +621,22 @@ func (e *EVM) executeScriptAtHeight( return res, err } -func addressToCadenceString(address common.Address) (cadence.String, error) { - return cadence.NewString( - strings.TrimPrefix(address.Hex(), "0x"), +func (e *EVM) getBlockView(evmHeight uint64) (*query.View, error) { + ledger := pebble.NewRegister(e.store, uint64(evmHeight), nil) + blocksProvider := replayer.NewBlocksProvider( + e.blocks, + e.config.FlowNetworkID, + nil, + ) + viewProvider := query.NewViewProvider( + e.config.FlowNetworkID, + evm.StorageAccountAddress(e.config.FlowNetworkID), + ledger, + blocksProvider, + 120_000_000, ) -} - -func cadenceStringToBytes(value cadence.Value) ([]byte, error) { - cdcString, ok := value.(cadence.String) - if !ok { - return nil, fmt.Errorf( - "failed to convert cadence value of type: %T to string: %v", - value, - value, - ) - } - - code, err := hex.DecodeString(string(cdcString)) - if err != nil { - return nil, fmt.Errorf("failed to hex-decode string to byte array [%s]: %w", cdcString, err) - } - - return code, nil -} - -// parseResult -func parseResult(res cadence.Value) (*evmTypes.ResultSummary, error) { - result, err := evmImpl.ResultSummaryFromEVMResultValue(res) - if err != nil { - return nil, fmt.Errorf("failed to decode EVM result of type: %s, with: %w", res.Type().ID(), err) - } - - if result.ErrorCode != 0 { - if result.ErrorCode == evmTypes.ExecutionErrCodeExecutionReverted { - return nil, errs.NewRevertError(result.ReturnedData) - } - return nil, errs.NewFailedTransactionError(result.ErrorMessage) - } - return result, err + return viewProvider.GetBlockView(uint64(evmHeight)) } // cacheKey builds the cache key from the script type, height and arguments. @@ -873,3 +664,8 @@ func cacheKey(scriptType scriptType, height uint64, args []cadence.Value) string return key } + +func AddOne64th(n uint64) uint64 { + // NOTE: Go's integer division floors, but that is desirable here + return n + (n / 64) +} diff --git a/storage/pebble/register.go b/storage/pebble/register.go new file mode 100644 index 00000000..bee82bf5 --- /dev/null +++ b/storage/pebble/register.go @@ -0,0 +1,172 @@ +package pebble + +import ( + "errors" + "fmt" + "sync" + + "github.com/cockroachdb/pebble" + "github.com/onflow/atree" + + errs "github.com/onflow/flow-evm-gateway/models/errors" + + "github.com/onflow/flow-go/fvm/evm/types" +) + +var _ atree.Ledger = &Register{} +var _ types.StorageProvider = &Register{} + +type Register struct { + store *Storage + height uint64 + batch *pebble.Batch + mux sync.RWMutex +} + +// NewRegister creates a new index instance at the provided height, all reads and +// writes of the registers will happen at that height. +func NewRegister(store *Storage, height uint64, batch *pebble.Batch) *Register { + return &Register{ + store: store, + height: height, + batch: batch, + mux: sync.RWMutex{}, + } +} + +func (r *Register) GetSnapshotAt(evmBlockHeight uint64) (types.BackendStorageSnapshot, error) { + return &Register{ + store: r.store, + height: evmBlockHeight, + mux: sync.RWMutex{}, + }, nil +} + +func (r *Register) GetValue(owner, key []byte) ([]byte, error) { + r.mux.RLock() + defer r.mux.RUnlock() + + var db pebble.Reader = r.store.db + if r.batch != nil { + db = r.batch + } + + iter, err := db.NewIter(&pebble.IterOptions{ + LowerBound: r.idLower(owner, key), + UpperBound: r.idUpper(owner, key), + }) + if err != nil { + return nil, fmt.Errorf("failed to create register range iterator: %w", err) + } + defer func() { + if err := iter.Close(); err != nil { + r.store.log.Error().Err(err).Msg("failed to close register iterator") + } + }() + + found := iter.Last() + if !found { + // as per interface expectation we need to return nil if not found + return nil, nil + } + + val, err := iter.ValueAndErr() + if err != nil { + return nil, fmt.Errorf( + "failed to get ledger value at owner %x and key %x: %w", + owner, + key, + err, + ) + } + + return val, nil +} + +func (r *Register) SetValue(owner, key, value []byte) error { + r.mux.Lock() + defer r.mux.Unlock() + + id := r.id(owner, key) + if err := r.store.set(ledgerValue, id, value, r.batch); err != nil { + return fmt.Errorf( + "failed to store ledger value for owner %x and key %x: %w", + owner, + key, + err, + ) + } + + return nil +} + +func (r *Register) ValueExists(owner, key []byte) (bool, error) { + val, err := r.GetValue(owner, key) + if err != nil { + return false, err + } + + return val != nil, nil +} + +func (r *Register) AllocateSlabIndex(owner []byte) (atree.SlabIndex, error) { + r.mux.Lock() + defer r.mux.Unlock() + + var index atree.SlabIndex + + val, err := r.store.batchGet(r.batch, ledgerSlabIndex, owner) + if err != nil { + if !errors.Is(err, errs.ErrEntityNotFound) { + return atree.SlabIndexUndefined, err + } + } + + if val != nil { + if len(val) != len(index) { + return atree.SlabIndexUndefined, fmt.Errorf( + "slab index was not stored in correct format for owner %x", + owner, + ) + } + + copy(index[:], val) + } + + index = index.Next() + if err := r.store.set(ledgerSlabIndex, owner, index[:], r.batch); err != nil { + return atree.SlabIndexUndefined, fmt.Errorf( + "slab index failed to set for owner %x: %w", + owner, + err, + ) + } + + return index, nil +} + +// id calculates a ledger id with embedded block height for owner and key. +// The key for a register has the following schema: +// {owner}{key}{height} +func (r *Register) id(owner, key []byte) []byte { + id := append(owner, key...) + h := uint64Bytes(r.height) + return append(id, h...) +} + +func (r *Register) idUpper(owner, key []byte) []byte { + id := []byte{ledgerValue} + id = append(id, owner...) + id = append(id, key...) + // increase height +1 because upper bound is exclusive + h := uint64Bytes(r.height + 1) + return append(id, h...) +} + +func (r *Register) idLower(owner, key []byte) []byte { + id := []byte{ledgerValue} + id = append(id, owner...) + id = append(id, key...) + // lower height is always 0 + return append(id, uint64Bytes(0)...) +} diff --git a/tests/e2e_web3js_test.go b/tests/e2e_web3js_test.go index 57f06e80..79d5d851 100644 --- a/tests/e2e_web3js_test.go +++ b/tests/e2e_web3js_test.go @@ -34,7 +34,6 @@ func TestWeb3_E2E(t *testing.T) { }) t.Run("test transaction traces", func(t *testing.T) { - t.Skip("not yet added back") runWeb3Test(t, "debug_traces_test") }) diff --git a/tests/go.mod b/tests/go.mod index ac19862f..c61c7ed2 100644 --- a/tests/go.mod +++ b/tests/go.mod @@ -55,7 +55,9 @@ require ( github.com/dgraph-io/ristretto v0.1.0 // indirect github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/dlclark/regexp2 v1.7.0 // indirect github.com/docker/go-units v0.5.0 // indirect + github.com/dop251/goja v0.0.0-20230806174421-c933cf95e127 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/ef-ds/deque v1.0.4 // indirect github.com/ethereum/c-kzg-4844 v1.0.0 // indirect @@ -75,6 +77,7 @@ require ( github.com/go-logr/stdr v1.2.2 // indirect github.com/go-ole/go-ole v1.3.0 // indirect github.com/go-redis/redis/v8 v8.11.5 // indirect + github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect github.com/gofrs/flock v0.8.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/glog v1.2.0 // indirect @@ -83,6 +86,7 @@ require ( github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb // indirect github.com/google/go-cmp v0.6.0 // indirect github.com/google/go-dap v0.11.0 // indirect + github.com/google/pprof v0.0.0-20231229205709-960ae82b1e42 // indirect github.com/google/s2a-go v0.1.7 // indirect github.com/google/uuid v1.6.0 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect diff --git a/tests/go.sum b/tests/go.sum index e7b09956..1311f1f0 100644 --- a/tests/go.sum +++ b/tests/go.sum @@ -141,8 +141,11 @@ github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/logex v1.2.0/go.mod h1:9+9sk7u7pGNWYMkh0hdiL++6OeibzJccyQU4p4MedaY= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/readline v1.5.0/go.mod h1:x22KAscuvRqlLoK9CsoYsmxoXZMMFVyOl86cAH8qUic= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/chzyer/test v0.0.0-20210722231415-061457976a23/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= @@ -222,8 +225,16 @@ github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUn github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= +github.com/dlclark/regexp2 v1.7.0 h1:7lJfhqlPssTb1WQx4yvTHN0uElPEv52sbaECrAQxjAo= +github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/dop251/goja v0.0.0-20211022113120-dc8c55024d06/go.mod h1:R9ET47fwRVRPZnOGvHxxhuZcbrMCuiqOz3Rlrh4KSnk= +github.com/dop251/goja v0.0.0-20230806174421-c933cf95e127 h1:qwcF+vdFrvPSEUDSX5RVoRccG8a5DhOdWdQ4zN62zzo= +github.com/dop251/goja v0.0.0-20230806174421-c933cf95e127/go.mod h1:QMWlm50DNe14hD7t24KEqZuUdC9sOTy8W6XbCU1mlw4= +github.com/dop251/goja_nodejs v0.0.0-20210225215109-d91c329300e7/go.mod h1:hn7BA7c8pLvoGndExHudxTDKZ84Pyvv+90pbBjbTz0Y= +github.com/dop251/goja_nodejs v0.0.0-20211022123610-8dd9abb0616d/go.mod h1:DngW8aVqWbuLRMHItjPUyqdj+HWPvnQe8V8y1nDpIbM= github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= @@ -330,16 +341,21 @@ github.com/go-playground/validator/v10 v10.14.1 h1:9c50NUPC30zyuKprjL3vNZ0m5oG+j github.com/go-playground/validator/v10 v10.14.1/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI= github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= +github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU= +github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= -github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0= github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= -github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8= +github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= +github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= -github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo= +github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= +github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= +github.com/gobwas/ws v1.2.1 h1:F2aeBZrm2NDsc7vbovKrWSogd4wvfAxg0FQ89/iqOTk= +github.com/gobwas/ws v1.2.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= @@ -435,6 +451,7 @@ github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hf github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20230207041349-798e818bf904/go.mod h1:uglQLonpP8qtYCYyzA+8c/9qtqgA3qsXGYqCPKARAFg= github.com/google/pprof v0.0.0-20231229205709-960ae82b1e42 h1:dHLYa5D8/Ta0aLR2XcPsrkpAgGeFs6thhMcQK0oQ0n8= github.com/google/pprof v0.0.0-20231229205709-960ae82b1e42/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= @@ -524,6 +541,7 @@ github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc= github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20220319035150-800ac71e25c2/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w= github.com/improbable-eng/grpc-web v0.15.0 h1:BN+7z6uNXZ1tQGcNAuaU1YjsLTApzkjt2tzCixLaUPQ= github.com/improbable-eng/grpc-web v0.15.0/go.mod h1:1sy9HKV4Jt9aEs9JSnkWlRJPuPtwNr0l57L4f878wP8= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= @@ -607,6 +625,8 @@ github.com/koron/go-ssdp v0.0.4/go.mod h1:oDXq+E5IL5q0U8uSBcoAXzTzInwy5lEgC91HoK github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= @@ -908,6 +928,7 @@ github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= @@ -1306,6 +1327,7 @@ golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/tests/web3js/eth_rate_limit_test.js b/tests/web3js/eth_rate_limit_test.js index f886586d..5f55c0e3 100644 --- a/tests/web3js/eth_rate_limit_test.js +++ b/tests/web3js/eth_rate_limit_test.js @@ -1,10 +1,10 @@ const { assert } = require('chai') -const {Web3} = require("web3") +const { Web3 } = require('web3') it('rate limit after X requests', async function () { this.timeout(0) setTimeout(() => process.exit(0), 5000) // make sure the process exits - let ws = new Web3("ws://127.0.0.1:8545") + let ws = new Web3('ws://127.0.0.1:8545') // wait for ws connection to establish and reset rate-limit timer await new Promise(res => setTimeout(res, 1500)) @@ -19,14 +19,14 @@ it('rate limit after X requests', async function () { try { await ws.eth.getBlockNumber() requestsMade++ - } catch(e) { + } catch (e) { assert.equal(e.innerError.message, 'limit of requests per second reached') requestsFailed++ } } - assert.equal(requestsMade, requestLimit, "more requests made than the limit") - assert.equal(requestsFailed, requests-requestLimit, "failed requests don't match expected value") + assert.equal(requestsMade, requestLimit, 'more requests made than the limit') + assert.equal(requestsFailed, requests - requestLimit, 'failed requests don\'t match expected value') await new Promise(res => setTimeout(res, 1000)) @@ -38,14 +38,14 @@ it('rate limit after X requests', async function () { try { await ws.eth.getBlockNumber() requestsMade++ - } catch(e) { + } catch (e) { assert.equal(e.innerError.message, 'limit of requests per second reached') requestsFailed++ } } - assert.equal(requestsMade, requestLimit, "more requests made than the limit") - assert.equal(requestsFailed, requests-requestLimit, "failed requests don't match expected value") + assert.equal(requestsMade, requestLimit, 'more requests made than the limit') + assert.equal(requestsFailed, requests - requestLimit, 'failed requests don\'t match expected value') await ws.currentProvider.disconnect() })