diff --git a/api/api.go b/api/api.go index 1c0d8404..1e827b13 100644 --- a/api/api.go +++ b/api/api.go @@ -1101,7 +1101,11 @@ func (b *BlockChainAPI) getBlockNumber(blockNumberOrHash *rpc.BlockNumberOrHash) return 0, err } if number, ok := blockNumberOrHash.Number(); ok { - return number.Int64(), nil + height, err := resolveBlockNumber(number, b.blocks) + if err != nil { + return 0, err + } + return int64(height), nil } if hash, ok := blockNumberOrHash.Hash(); ok { @@ -1116,6 +1120,24 @@ func (b *BlockChainAPI) getBlockNumber(blockNumberOrHash *rpc.BlockNumberOrHash) return 0, err } +func resolveBlockNumber( + number rpc.BlockNumber, + blocksDB storage.BlockIndexer, +) (uint64, error) { + height := number.Int64() + + // if special values (latest) we return latest executed height + if height < 0 { + executed, err := blocksDB.LatestEVMHeight() + if err != nil { + return 0, err + } + height = int64(executed) + } + + return uint64(height), nil +} + // handleError takes in an error and in case the error is of type ErrEntityNotFound // it returns nil instead of an error since that is according to the API spec, // if the error is not of type ErrEntityNotFound it will return the error and the generic 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 17da8425..5a6540a2 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" @@ -128,11 +131,13 @@ 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, ) @@ -146,9 +151,15 @@ func (b *Bootstrap) StartEventIngestion(ctx context.Context) error { } blocksProvider := replayer.NewBlocksProvider( b.storages.Blocks, - b.config.FlowNetworkID, + chainID, tracer, ) + replayerConfig := replayer.Config{ + ChainID: chainID, + RootAddr: evm.StorageAccountAddress(chainID), + Tracer: tracer, + ValidateResults: true, + } // initialize event ingestion engine b.events = ingestion.NewEventIngestionEngine( @@ -163,6 +174,7 @@ func (b *Bootstrap) StartEventIngestion(ctx context.Context) error { b.publishers.Logs, b.logger, b.collector, + replayerConfig, ) StartEngine(ctx, b.events, l) @@ -276,7 +288,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, @@ -336,10 +364,16 @@ func (b *Bootstrap) StartAPIServer(ctx context.Context) error { ratelimiter, ) - var debugAPI *api.DebugAPI - if b.config.TracesEnabled { - 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 { @@ -532,6 +566,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 462b1956..569f72d4 100644 --- a/go.mod +++ b/go.mod @@ -60,6 +60,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 @@ -75,12 +77,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..f9e3bc81 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{} @@ -47,6 +49,7 @@ type Engine struct { blocksPublisher *models.Publisher[*models.Block] logsPublisher *models.Publisher[[]*gethTypes.Log] collector metrics.Collector + replayerConfig replayer.Config } func NewEventIngestionEngine( @@ -61,6 +64,7 @@ func NewEventIngestionEngine( logsPublisher *models.Publisher[[]*gethTypes.Log], log zerolog.Logger, collector metrics.Collector, + replayerConfig replayer.Config, ) *Engine { log = log.With().Str("component", "ingestion").Logger() @@ -78,6 +82,7 @@ func NewEventIngestionEngine( blocksPublisher: blocksPublisher, logsPublisher: logsPublisher, collector: collector, + replayerConfig: replayerConfig, } } @@ -158,8 +163,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.Tracer, + 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 +213,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,19 +224,13 @@ 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, - ) - } - if err := batch.Commit(pebbleDB.Sync); err != nil { return fmt.Errorf("failed to commit indexed data for Cadence block %d: %w", events.CadenceHeight(), err) } diff --git a/services/ingestion/engine_test.go b/services/ingestion/engine_test.go index ff87ea6f..01e50a98 100644 --- a/services/ingestion/engine_test.go +++ b/services/ingestion/engine_test.go @@ -7,6 +7,8 @@ import ( "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 +34,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. @@ -74,6 +76,7 @@ func TestSerialBlockIngestion(t *testing.T) { models.NewPublisher[[]*gethTypes.Log](), zerolog.Nop(), metrics.NopCollector, + defaultReplayerConfig(), ) done := make(chan struct{}) @@ -119,8 +122,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. @@ -155,6 +157,7 @@ func TestSerialBlockIngestion(t *testing.T) { models.NewPublisher[[]*gethTypes.Log](), zerolog.Nop(), metrics.NopCollector, + defaultReplayerConfig(), ) waitErr := make(chan struct{}) @@ -162,7 +165,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 +219,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 +227,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. @@ -271,6 +274,7 @@ func TestBlockAndTransactionIngestion(t *testing.T) { models.NewPublisher[[]*gethTypes.Log](), zerolog.Nop(), metrics.NopCollector, + defaultReplayerConfig(), ) done := make(chan struct{}) @@ -333,8 +337,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. @@ -375,6 +378,7 @@ func TestBlockAndTransactionIngestion(t *testing.T) { models.NewPublisher[[]*gethTypes.Log](), zerolog.Nop(), metrics.NopCollector, + defaultReplayerConfig(), ) done := make(chan struct{}) @@ -434,8 +438,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. @@ -471,6 +474,7 @@ func TestBlockAndTransactionIngestion(t *testing.T) { models.NewPublisher[[]*gethTypes.Log](), zerolog.Nop(), metrics.NopCollector, + defaultReplayerConfig(), ) done := make(chan struct{}) @@ -609,3 +613,28 @@ 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), + 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/config.go b/services/replayer/config.go new file mode 100644 index 00000000..f5a196ed --- /dev/null +++ b/services/replayer/config.go @@ -0,0 +1,13 @@ +package replayer + +import ( + "github.com/onflow/flow-go/model/flow" + "github.com/onflow/go-ethereum/eth/tracers" +) + +type Config struct { + ChainID flow.ChainID + RootAddr flow.Address + Tracer *tracers.Tracer + ValidateResults bool +} diff --git a/services/requester/requester.go b/services/requester/requester.go index b0431bf3..3812ca37 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 ( @@ -94,12 +95,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) @@ -118,14 +119,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 @@ -135,6 +138,8 @@ type EVM struct { } func NewEVM( + store *pebble.Storage, + blocksProvider *replayer.BlocksProvider, client *CrossSporkClient, config *config.Config, signer crypto.Signer, @@ -193,6 +198,8 @@ func NewEVM( } evm := &EVM{ + store: store, + blocksProvider: blocksProvider, client: client, config: config, signer: signer, @@ -340,45 +347,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( @@ -386,79 +360,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( @@ -467,125 +374,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( @@ -593,53 +479,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) { @@ -715,37 +560,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 @@ -806,45 +620,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. @@ -872,3 +663,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 70276762..a9f6a464 100644 --- a/tests/e2e_web3js_test.go +++ b/tests/e2e_web3js_test.go @@ -28,6 +28,15 @@ func TestWeb3_E2E(t *testing.T) { runWeb3Test(t, "build_evm_state_test") }) + t.Run("verify Cadence arch calls", func(t *testing.T) { + t.Skip() + runWeb3Test(t, "verify_cadence_arch_calls_test") + }) + + t.Run("test transaction traces", func(t *testing.T) { + runWeb3Test(t, "debug_traces_test") + }) + t.Run("test setup sanity check", func(t *testing.T) { runWeb3Test(t, "setup_test") }) @@ -56,6 +65,10 @@ func TestWeb3_E2E(t *testing.T) { runWeb3Test(t, "eth_deploy_contract_and_interact_test") }) + t.Run("test eth_getStorageAt", func(t *testing.T) { + runWeb3Test(t, "eth_get_storage_at_test") + }) + t.Run("deploy multicall3 contract and call methods", func(t *testing.T) { runWeb3Test(t, "eth_multicall3_contract_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/helpers.go b/tests/helpers.go index a62bb2e1..c85caec8 100644 --- a/tests/helpers.go +++ b/tests/helpers.go @@ -177,7 +177,7 @@ func servicesSetup(t *testing.T) (emulator.Emulator, func()) { // and will report failure or success of the test. func executeTest(t *testing.T, testFile string) { command := fmt.Sprintf( - "./web3js/node_modules/.bin/mocha ./web3js/%s.js --timeout 120s", + "./web3js/node_modules/.bin/mocha ./web3js/%s.js --timeout 360s", testFile, ) parts := strings.Fields(command) diff --git a/tests/web3js/build_evm_state_test.js b/tests/web3js/build_evm_state_test.js index d52eb715..a9490492 100644 --- a/tests/web3js/build_evm_state_test.js +++ b/tests/web3js/build_evm_state_test.js @@ -237,85 +237,6 @@ it('should handle a large number of EVM interactions', async () => { gasPrice: conf.minGasPrice, }) assert.equal(res.receipt.status, conf.successStatus) - - // submit a transaction that calls verifyArchCallToRandomSource(uint64 height) - let getRandomSourceData = deployed.contract.methods.verifyArchCallToRandomSource(120).encodeABI() - res = await helpers.signAndSend({ - from: conf.eoa.address, - to: contractAddress, - data: getRandomSourceData, - value: '0', - gasPrice: conf.minGasPrice, - }) - assert.equal(res.receipt.status, conf.successStatus) - - // make a contract call for verifyArchCallToRandomSource(uint64 height) - res = await web3.eth.call({ to: contractAddress, data: getRandomSourceData }, latest) - assert.notEqual( - res, - '0x0000000000000000000000000000000000000000000000000000000000000000' - ) - assert.lengthOf(res, 66) - - // submit a transaction that calls verifyArchCallToRevertibleRandom() - let revertibleRandomData = deployed.contract.methods.verifyArchCallToRevertibleRandom().encodeABI() - res = await helpers.signAndSend({ - from: conf.eoa.address, - to: contractAddress, - data: revertibleRandomData, - value: '0', - gasPrice: conf.minGasPrice, - }) - assert.equal(res.receipt.status, conf.successStatus) - - // make a contract call for verifyArchCallToRevertibleRandom() - res = await web3.eth.call({ to: contractAddress, data: revertibleRandomData }, latest) - assert.notEqual( - res, - '0x0000000000000000000000000000000000000000000000000000000000000000' - ) - assert.lengthOf(res, 66) - - // submit a transaction that calls verifyArchCallToFlowBlockHeight() - let flowBlockHeightData = deployed.contract.methods.verifyArchCallToFlowBlockHeight().encodeABI() - res = await helpers.signAndSend({ - from: conf.eoa.address, - to: contractAddress, - data: flowBlockHeightData, - value: '0', - gasPrice: conf.minGasPrice, - }) - assert.equal(res.receipt.status, conf.successStatus) - - // make a contract call for verifyArchCallToFlowBlockHeight() - res = await web3.eth.call({ to: contractAddress, data: flowBlockHeightData }, latest) - assert.equal( - web3.eth.abi.decodeParameter('uint64', res), - latest, - ) - - // submit a transaction that calls verifyArchCallToVerifyCOAOwnershipProof(address,bytes32,bytes) - let tx = await web3.eth.getTransactionFromBlock(conf.startBlockHeight, 1) - let verifyCOAOwnershipProofData = deployed.contract.methods.verifyArchCallToVerifyCOAOwnershipProof( - tx.to, - '0x1bacdb569847f31ade07e83d6bb7cefba2b9290b35d5c2964663215e73519cff', - web3.utils.hexToBytes('f853c18088f8d6e0586b0a20c78365766df842b840b90448f4591df2639873be2914c5560149318b7e2fcf160f7bb8ed13cfd97be2f54e6889606f18e50b2c37308386f840e03a9fff915f57b2164cba27f0206a95') - ).encodeABI() - res = await helpers.signAndSend({ - from: conf.eoa.address, - to: contractAddress, - data: verifyCOAOwnershipProofData, - value: '0', - gasPrice: conf.minGasPrice, - }) - assert.equal(res.receipt.status, conf.successStatus) - - // make a contract call for verifyArchCallToVerifyCOAOwnershipProof(address,bytes32,bytes) - res = await web3.eth.call({ to: contractAddress, data: verifyCOAOwnershipProofData }, latest) - assert.equal( - web3.eth.abi.decodeParameter('bool', res), - false, - ) }) function randomItem(items) { diff --git a/tests/web3js/debug_traces_test.js b/tests/web3js/debug_traces_test.js new file mode 100644 index 00000000..75794cd0 --- /dev/null +++ b/tests/web3js/debug_traces_test.js @@ -0,0 +1,455 @@ +const { assert } = require('chai') +const conf = require('./config') +const helpers = require('./helpers') +const web3 = conf.web3 + +let deployed = null +let contractAddress = null + +before(async () => { + deployed = await helpers.deployContract('storage') + contractAddress = deployed.receipt.contractAddress + + assert.equal(deployed.receipt.status, conf.successStatus) +}) + +it('should retrieve transaction traces', async () => { + assert.equal(deployed.receipt.status, conf.successStatus) + + let receipt = await web3.eth.getTransactionReceipt(deployed.receipt.transactionHash) + assert.equal(receipt.contractAddress, contractAddress) + + let callTracer = { + tracer: 'callTracer', + tracerConfig: { + onlyTopCall: true + } + } + response = await helpers.callRPCMethod( + 'debug_traceTransaction', + [receipt.transactionHash, callTracer] + ) + assert.equal(response.status, 200) + assert.isDefined(response.body.result) + + // Assert proper response for `callTracer` + let txTrace = response.body.result + assert.equal(txTrace.from, '0xfacf71692421039876a5bb4f10ef7a439d8ef61e') + assert.equal(txTrace.gas, '0x118e0c') + assert.equal(txTrace.gasUsed, '0x114010') + assert.equal(txTrace.to, '0x99a64c993965f8d69f985b5171bc20065cc32fab') + assert.lengthOf(txTrace.input, 9856n) + assert.lengthOf(txTrace.output, 9806n) + assert.equal(txTrace.value, '0x0') + assert.equal(txTrace.type, 'CREATE') + + let jsTracer = '{hist: {}, nops: 0, step: function(log, db) { var op = log.op.toString(); if (this.hist[op]){ this.hist[op]++; } else { this.hist[op] = 1; } this.nops++; }, fault: function(log, db) {}, result: function(ctx) { return this.hist; }}' + response = await helpers.callRPCMethod( + 'debug_traceTransaction', + [receipt.transactionHash, { tracer: jsTracer }] + ) + assert.equal(response.status, 200) + assert.isDefined(response.body.result) + + // Assert proper response for custom JavaScript tracer + txTrace = response.body.result + assert.deepEqual( + txTrace, + { + PUSH1: 2, + MSTORE: 1, + PUSH2: 3, + PUSH0: 3, + DUP2: 1, + SWAP1: 1, + SSTORE: 1, + POP: 1, + DUP1: 1, + CODECOPY: 1, + RETURN: 1 + } + ) + + let updateData = deployed.contract.methods.store(100n).encodeABI() + let res = await helpers.signAndSend({ + from: conf.eoa.address, + to: contractAddress, + data: updateData, + value: '0', + gasPrice: conf.minGasPrice, + }) + assert.equal(res.receipt.status, conf.successStatus) + + receipt = await web3.eth.getTransactionReceipt(res.receipt.transactionHash) + + response = await helpers.callRPCMethod( + 'debug_traceTransaction', + [receipt.transactionHash, callTracer] + ) + assert.equal(response.status, 200) + assert.isDefined(response.body.result) + + // Assert proper response for `callTracer` + txTrace = response.body.result + assert.equal(txTrace.from, '0xfacf71692421039876a5bb4f10ef7a439d8ef61e') + assert.equal(txTrace.gas, '0x72c3') + assert.equal(txTrace.gasUsed, '0x6827') + assert.equal(txTrace.to, '0x99a64c993965f8d69f985b5171bc20065cc32fab') + assert.equal( + txTrace.input, + updateData + ) + assert.equal(txTrace.value, '0x0') + assert.equal(txTrace.type, 'CALL') + + let prestateTracer = { + tracer: 'prestateTracer', + tracerConfig: { + diffMode: true + } + } + response = await helpers.callRPCMethod( + 'debug_traceTransaction', + [receipt.transactionHash, prestateTracer] + ) + assert.equal(response.status, 200) + assert.isDefined(response.body.result) + + // Assert proper response for `prestateTracer` + txTrace = response.body.result + assert.deepEqual( + txTrace.pre['0x0000000000000000000000030000000000000000'], + { balance: '0x0', nonce: 1 } + ) + assert.deepEqual( + txTrace.pre['0xfacf71692421039876a5bb4f10ef7a439d8ef61e'], + { balance: '0x456391823ad876a0', nonce: 1 } + ) + assert.deepEqual( + txTrace.post['0x0000000000000000000000030000000000000000'], + { balance: '0x3d06da' } + ) + assert.deepEqual( + txTrace.post['0xfacf71692421039876a5bb4f10ef7a439d8ef61e'], + { balance: '0x456391823a9b6fc6', nonce: 2 } + ) + + response = await helpers.callRPCMethod( + 'debug_traceTransaction', + [receipt.transactionHash, { tracer: '4byteTracer' }] + ) + assert.equal(response.status, 200) + assert.isDefined(response.body.result) + + // Assert proper response for `4byteTracer` + txTrace = response.body.result + assert.deepEqual( + txTrace, + { '0x6057361d-32': 1 } + ) + + response = await helpers.callRPCMethod( + 'debug_traceBlockByNumber', + [web3.utils.toHex(receipt.blockNumber), callTracer] + ) + assert.equal(response.status, 200) + assert.isDefined(response.body.result) + + let txTraces = response.body.result + assert.lengthOf(txTraces, 2) // the 2nd tx trace is from the transfer of fees to coinbase + assert.deepEqual( + txTraces, + [ + { + txHash: '0x87449feedc004c75c0e8b12d01656f2e28366c7d73b1b5336beae20aaa5033dd', + result: { + from: '0xfacf71692421039876a5bb4f10ef7a439d8ef61e', + gas: '0x72c3', + gasUsed: '0x6827', + to: '0x99a64c993965f8d69f985b5171bc20065cc32fab', + input: '0x6057361d0000000000000000000000000000000000000000000000000000000000000064', + value: '0x0', + type: 'CALL' + } + }, + { + txHash: '0x6039ef1f7dc8d40b74f58e502f5b0b535a46c1b4ddd780c23cb97cf4d681bb47', + result: { + from: '0x0000000000000000000000030000000000000000', + gas: '0x5b04', + gasUsed: '0x5208', + to: '0x658bdf435d810c91414ec09147daa6db62406379', + input: '0x', + value: '0x3d06da', + type: 'CALL' + } + } + ] + ) + + response = await helpers.callRPCMethod( + 'debug_traceBlockByHash', + [web3.utils.toHex(receipt.blockHash), callTracer] + ) + assert.equal(response.status, 200) + assert.isDefined(response.body.result) + + txTraces = response.body.result + assert.lengthOf(txTraces, 2) // the 2nd tx trace is from the transfer of fees to coinbase + assert.deepEqual( + txTraces, + [ + { + txHash: '0x87449feedc004c75c0e8b12d01656f2e28366c7d73b1b5336beae20aaa5033dd', + result: { + from: '0xfacf71692421039876a5bb4f10ef7a439d8ef61e', + gas: '0x72c3', + gasUsed: '0x6827', + to: '0x99a64c993965f8d69f985b5171bc20065cc32fab', + input: '0x6057361d0000000000000000000000000000000000000000000000000000000000000064', + value: '0x0', + type: 'CALL' + } + }, + { + txHash: '0x6039ef1f7dc8d40b74f58e502f5b0b535a46c1b4ddd780c23cb97cf4d681bb47', + result: { + from: '0x0000000000000000000000030000000000000000', + gas: '0x5b04', + gasUsed: '0x5208', + to: '0x658bdf435d810c91414ec09147daa6db62406379', + input: '0x', + value: '0x3d06da', + type: 'CALL' + } + } + ] + ) +}) + +it('should retrieve call traces', async () => { + let receipt = await web3.eth.getTransactionReceipt(deployed.receipt.transactionHash) + assert.equal(receipt.contractAddress, contractAddress) + + let callTracer = { + tracer: 'callTracer', + tracerConfig: { + onlyTopCall: true + } + } + + let callData = deployed.contract.methods.store(500).encodeABI() + let traceCall = { + from: conf.eoa.address, + to: contractAddress, + data: callData, + value: '0x0', + gasPrice: web3.utils.toHex(conf.minGasPrice), + gas: '0x95ab' + } + response = await helpers.callRPCMethod( + 'debug_traceCall', + [traceCall, 'latest', callTracer] + ) + assert.equal(response.status, 200) + assert.isDefined(response.body) + + let updateTrace = response.body.result + assert.equal(updateTrace.from, '0xfacf71692421039876a5bb4f10ef7a439d8ef61e') + assert.equal(updateTrace.gas, '0x95ab') + assert.equal(updateTrace.gasUsed, '0x6833') + assert.equal(updateTrace.to, '0x99a64c993965f8d69f985b5171bc20065cc32fab') + assert.equal( + updateTrace.input, + '0x6057361d00000000000000000000000000000000000000000000000000000000000001f4' + ) + assert.equal(updateTrace.value, '0x0') + assert.equal(updateTrace.type, 'CALL') + + callData = deployed.contract.methods.retrieve().encodeABI() + traceCall = { + from: conf.eoa.address, + to: contractAddress, + gas: '0x75ab', + gasPrice: web3.utils.toHex(conf.minGasPrice), + value: '0x0', + data: callData, + } + response = await helpers.callRPCMethod( + 'debug_traceCall', + [traceCall, 'latest', callTracer] + ) + assert.equal(response.status, 200) + assert.isDefined(response.body) + + let callTrace = response.body.result + assert.equal(callTrace.from, '0xfacf71692421039876a5bb4f10ef7a439d8ef61e') + assert.equal(callTrace.gas, '0x75ab') + assert.equal(callTrace.gasUsed, '0x5be0') + assert.equal(callTrace.to, '0x99a64c993965f8d69f985b5171bc20065cc32fab') + assert.equal(callTrace.input, '0x2e64cec1') + assert.equal( + callTrace.output, + '0x0000000000000000000000000000000000000000000000000000000000000064' + ) + assert.equal(callTrace.value, '0x0') + assert.equal(callTrace.type, 'CALL') + + let prestateTracer = { + tracer: 'prestateTracer', + tracerConfig: { + diffMode: true + } + } + response = await helpers.callRPCMethod( + 'debug_traceCall', + [traceCall, 'latest', prestateTracer] + ) + assert.equal(response.status, 200) + assert.isDefined(response.body) + + // Assert proper response for `prestateTracer` + txTrace = response.body.result + assert.deepEqual( + txTrace, + { + post: { '0xfacf71692421039876a5bb4f10ef7a439d8ef61e': { nonce: 3 } }, + pre: { + '0xfacf71692421039876a5bb4f10ef7a439d8ef61e': { balance: '0x456391823a9b6fc6', nonce: 2 } + } + } + ) + + response = await helpers.callRPCMethod( + 'debug_traceCall', + [traceCall, 'latest', { tracer: '4byteTracer' }] + ) + assert.equal(response.status, 200) + assert.isDefined(response.body) + + // Assert proper response for `4byteTracer` + txTrace = response.body.result + assert.deepEqual( + txTrace, + { '0x2e64cec1-0': 1 } + ) + + let jsTracer = '{hist: {}, nops: 0, step: function(log, db) { var op = log.op.toString(); if (this.hist[op]){ this.hist[op]++; } else { this.hist[op] = 1; } this.nops++; }, fault: function(log, db) {}, result: function(ctx) { return this.hist; }}' + response = await helpers.callRPCMethod( + 'debug_traceCall', + [traceCall, 'latest', { tracer: jsTracer }] + ) + assert.equal(response.status, 200) + assert.isDefined(response.body) + + // Assert proper response for custom JavaScript tracer + txTrace = response.body.result + assert.deepEqual( + txTrace, + { + PUSH1: 7, + MSTORE: 2, + CALLVALUE: 1, + DUP1: 6, + ISZERO: 1, + PUSH2: 13, + JUMPI: 5, + JUMPDEST: 12, + POP: 9, + CALLDATASIZE: 1, + LT: 1, + PUSH0: 5, + CALLDATALOAD: 1, + SHR: 1, + PUSH4: 3, + GT: 2, + EQ: 1, + JUMP: 8, + SLOAD: 1, + SWAP1: 7, + MLOAD: 2, + SWAP2: 4, + DUP3: 2, + ADD: 2, + DUP4: 1, + DUP5: 1, + DUP2: 2, + SWAP3: 1, + SUB: 1, + RETURN: 1 + } + ) + + let callTracerWithStateOverrides = { + tracer: 'callTracer', + tracerConfig: { + onlyTopCall: true + }, + stateOverrides: { + [contractAddress]: { + stateDiff: { + '0x0000000000000000000000000000000000000000000000000000000000000000': '0x00000000000000000000000000000000000000000000000000000000000003e8' + } + } + } + } + response = await helpers.callRPCMethod( + 'debug_traceCall', + [traceCall, 'latest', callTracerWithStateOverrides] + ) + assert.equal(response.status, 200) + assert.isDefined(response.body) + + callTrace = response.body.result + assert.equal(callTrace.from, '0xfacf71692421039876a5bb4f10ef7a439d8ef61e') + assert.equal(callTrace.gas, '0x75ab') + assert.equal(callTrace.gasUsed, '0x5be0') + assert.equal(callTrace.to, '0x99a64c993965f8d69f985b5171bc20065cc32fab') + assert.equal(callTrace.input, '0x2e64cec1') + assert.equal( + callTrace.output, + '0x00000000000000000000000000000000000000000000000000000000000003e8' + ) + assert.equal(callTrace.value, '0x0') + assert.equal(callTrace.type, 'CALL') + + let updateData = deployed.contract.methods.store(1500).encodeABI() + let res = await helpers.signAndSend({ + from: conf.eoa.address, + to: contractAddress, + data: updateData, + value: '0', + gasPrice: conf.minGasPrice, + }) + assert.equal(res.receipt.status, conf.successStatus) + + let latestHeight = await web3.eth.getBlockNumber() + + // Assert value on previous block + response = await helpers.callRPCMethod( + 'debug_traceCall', + [traceCall, web3.utils.toHex(latestHeight - 1n), callTracer] + ) + assert.equal(response.status, 200) + assert.isDefined(response.body) + + callTrace = response.body.result + assert.equal( + callTrace.output, + '0x0000000000000000000000000000000000000000000000000000000000000064' + ) + + // Assert value on latest block + response = await helpers.callRPCMethod( + 'debug_traceCall', + [traceCall, web3.utils.toHex(latestHeight), callTracer] + ) + assert.equal(response.status, 200) + assert.isDefined(response.body) + + callTrace = response.body.result + assert.equal( + callTrace.output, + '0x00000000000000000000000000000000000000000000000000000000000005dc' + ) +}) diff --git a/tests/web3js/eth_get_storage_at_test.js b/tests/web3js/eth_get_storage_at_test.js new file mode 100644 index 00000000..2969f0aa --- /dev/null +++ b/tests/web3js/eth_get_storage_at_test.js @@ -0,0 +1,259 @@ +const { assert } = require('chai') +const conf = require('./config') +const helpers = require('./helpers') +const web3 = conf.web3 + +it('deploy contract and interact', async () => { + let deployed = await helpers.deployContract('storage') + let contractAddress = deployed.receipt.contractAddress + + // make sure deploy results are correct + assert.equal(deployed.receipt.status, conf.successStatus) + assert.isString(deployed.receipt.transactionHash) + assert.isString(contractAddress) + assert.equal(deployed.receipt.from, conf.eoa.address) + assert.isUndefined(deployed.receipt.to) + + let rcp = await web3.eth.getTransactionReceipt(deployed.receipt.transactionHash) + assert.equal(rcp.contractAddress, contractAddress) + assert.equal(rcp.status, conf.successStatus) + assert.isUndefined(rcp.to) + assert.equal(rcp.gasUsed, 1130512n) + assert.equal(rcp.gasUsed, rcp.cumulativeGasUsed) + + // check if latest block contains the deploy results + let latestHeight = await web3.eth.getBlockNumber() + let deployTx = await web3.eth.getTransactionFromBlock(latestHeight, 0) + assert.equal(deployTx.hash, deployed.receipt.transactionHash) + assert.isUndefined(deployTx.to) + + // check that getCode supports specific block heights + let code = await web3.eth.getCode(contractAddress, latestHeight - 1n) + assert.equal(code, '0x') // empty at previous height + + code = await web3.eth.getCode(contractAddress) + // deploy data has more than just the contract + // since it contains the initialization code, + // but subset of the data is the contract code + assert.isTrue(deployTx.data.includes(code.replace('0x', ''))) + + let deployReceipt = await web3.eth.getTransactionReceipt(deployed.receipt.transactionHash) + assert.deepEqual(deployReceipt, deployed.receipt) + + // get the default deployed value on contract + const initValue = 1337 + let callRetrieve = await deployed.contract.methods.retrieve().encodeABI() + result = await web3.eth.call({ to: contractAddress, data: callRetrieve }, 'latest') + assert.equal(result, initValue) + + let slot = 0 // The slot for the 'number' variable + let stored = await web3.eth.getStorageAt(contractAddress, slot, latestHeight) + let value = web3.utils.hexToNumberString(stored) + assert.equal(value, initValue) + + // set the value on the contract, to its current value + let updateData = deployed.contract.methods.store(initValue).encodeABI() + // store a value in the contract + let res = await helpers.signAndSend({ + from: conf.eoa.address, + to: contractAddress, + data: updateData, + value: '0', + gasPrice: conf.minGasPrice, + }) + assert.equal(res.receipt.status, conf.successStatus) + + // check the new value on contract + result = await web3.eth.call({ to: contractAddress, data: callRetrieve }, "latest") + assert.equal(result, initValue) + + // update the value on the contract + newValue = 100 + updateData = deployed.contract.methods.store(newValue).encodeABI() + // store a value in the contract + res = await helpers.signAndSend({ + from: conf.eoa.address, + to: contractAddress, + data: updateData, + value: '0', + gasPrice: conf.minGasPrice, + }) + assert.equal(res.receipt.status, conf.successStatus) + + stored = await web3.eth.getStorageAt(contractAddress, slot, res.receipt.blockNumber) + value = web3.utils.hexToNumberString(stored) + assert.equal(value, '100') + + stored = await web3.eth.getStorageAt(contractAddress, slot, res.receipt.blockNumber - 1n) + value = web3.utils.hexToNumberString(stored) + assert.equal(value, '1337') + + stored = await web3.eth.getStorageAt(contractAddress, slot, res.receipt.blockNumber - 2n) + value = web3.utils.hexToNumberString(stored) + assert.equal(value, '1337') + + stored = await web3.eth.getStorageAt(contractAddress, slot, res.receipt.blockNumber - 3n) + value = web3.utils.hexToNumberString(stored) + assert.equal(value, '0') + + // check the new value on contract + result = await web3.eth.call({ to: contractAddress, data: callRetrieve }, "latest") + assert.equal(result, newValue) + + // make sure receipts and txs are indexed + latestHeight = await web3.eth.getBlockNumber() + let updateTx = await web3.eth.getTransactionFromBlock(latestHeight, 0) + let updateRcp = await web3.eth.getTransactionReceipt(updateTx.hash) + assert.equal(updateRcp.status, conf.successStatus) + assert.equal(updateTx.data, updateData) + + // check that call can handle specific block heights + result = await web3.eth.call({ to: contractAddress, data: callRetrieve }, latestHeight - 1n) + assert.equal(result, initValue) + + // submit a transaction that emits logs + res = await helpers.signAndSend({ + from: conf.eoa.address, + to: contractAddress, + data: deployed.contract.methods.sum(100, 200).encodeABI(), + gas: 1_000_000, + gasPrice: conf.minGasPrice + }) + assert.equal(res.receipt.status, conf.successStatus) + + // assert that logsBloom from transaction receipt and block match + latestHeight = await web3.eth.getBlockNumber() + let block = await web3.eth.getBlock(latestHeight) + assert.equal(block.logsBloom, res.receipt.logsBloom) + + // check that revert reason for custom error is correctly returned for signed transaction + try { + let callCustomError = deployed.contract.methods.customError().encodeABI() + result = await helpers.signAndSend({ + from: conf.eoa.address, + to: contractAddress, + data: callCustomError, + gas: 1_000_000, + gasPrice: conf.minGasPrice + }) + } catch (error) { + assert.equal(error.reason, 'execution reverted') + assert.equal(error.signature, '0x9195785a') + assert.equal( + error.data, + '00000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000001056616c756520697320746f6f206c6f7700000000000000000000000000000000' + ) + } + + // check that revert reason for custom error is correctly returned for contract call + // and it is properly ABI decoded. + try { + result = await deployed.contract.methods.customError().call({ from: conf.eoa.address }) + } catch (err) { + let error = err.innerError + assert.equal( + error.data, + '0x9195785a00000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000001056616c756520697320746f6f206c6f7700000000000000000000000000000000' + ) + assert.equal(error.errorName, 'MyCustomError') + assert.equal(error.errorSignature, 'MyCustomError(uint256,string)') + assert.equal(error.errorArgs.value, 5n) + assert.equal(error.errorArgs.message, 'Value is too low') + } + + // check that assertion error is correctly returned for signed transaction + try { + let callAssertError = deployed.contract.methods.assertError().encodeABI() + result = await helpers.signAndSend({ + from: conf.eoa.address, + to: contractAddress, + data: callAssertError, + gas: 1_000_000, + gasPrice: conf.minGasPrice + }) + } catch (error) { + assert.equal(error.reason, 'execution reverted: Assert Error Message') + assert.equal(error.signature, '0x08c379a0') + assert.equal( + error.data, + '00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000014417373657274204572726f72204d657373616765000000000000000000000000' + ) + } + + // check that assertion error is correctly returned for contract call + // and it is properly ABI decoded. + try { + result = await deployed.contract.methods.assertError().call({ from: conf.eoa.address }) + } catch (err) { + let error = err.innerError + assert.equal( + error.message, + 'execution reverted: Assert Error Message' + ) + assert.equal( + error.data, + '0x08c379a000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000014417373657274204572726f72204d657373616765000000000000000000000000' + ) + } + + // check that revert reason for custom error is correctly returned for gas estimation + try { + let callCustomError = deployed.contract.methods.customError().encodeABI() + result = await web3.eth.estimateGas({ + from: conf.eoa.address, + to: contractAddress, + data: callCustomError, + gas: 1_000_000, + gasPrice: conf.minGasPrice + }) + } catch (error) { + assert.equal(error.innerError.message, 'execution reverted') + assert.equal( + error.innerError.data, + '0x9195785a00000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000001056616c756520697320746f6f206c6f7700000000000000000000000000000000' + ) + } + + // check that assertion error is correctly returned for gas estimation + try { + let callAssertError = deployed.contract.methods.assertError().encodeABI() + result = await web3.eth.estimateGas({ + from: conf.eoa.address, + to: contractAddress, + data: callAssertError, + gas: 1_000_000, + gasPrice: conf.minGasPrice + }) + } catch (error) { + assert.equal(error.innerError.message, 'execution reverted: Assert Error Message') + assert.equal( + error.innerError.data, + '0x08c379a000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000014417373657274204572726f72204d657373616765000000000000000000000000' + ) + } + + let gasEstimate = await web3.eth.estimateGas( + { + from: conf.eoa.address, + to: contractAddress, + data: deployed.contract.methods.sum(100, 200).encodeABI(), + gas: 1_000_000, + gasPrice: 0 + }, + '0x1' + ) + assert.equal(gasEstimate, 23977n) + + gasEstimate = await web3.eth.estimateGas( + { + from: conf.eoa.address, + to: contractAddress, + data: deployed.contract.methods.sum(100, 200).encodeABI(), + gas: 1_000_000, + gasPrice: 0 + }, + 'latest' + ) + assert.equal(gasEstimate, 27398n) + +}) diff --git a/tests/web3js/eth_revert_reason_test.js b/tests/web3js/eth_revert_reason_test.js index 4578abfa..75113160 100644 --- a/tests/web3js/eth_revert_reason_test.js +++ b/tests/web3js/eth_revert_reason_test.js @@ -40,18 +40,19 @@ it('store revertReason field in transaction receipts', async () => { [signedTx.rawTransaction] ) assert.equal(200, response.status) + let txHash = response.body.result - let latestHeight = await web3.eth.getBlockNumber() - let block = await web3.eth.getBlock(latestHeight) - assert.equal(block.number, conf.startBlockHeight + 2n) + let rcp = null + while (rcp == null) { + rcp = await helpers.callRPCMethod( + 'eth_getTransactionReceipt', + [txHash] + ) + if (rcp.body.result == null) { + rcp = null + } + } - let revertedTx = await web3.eth.getTransactionFromBlock(latestHeight, 0) - // Give some time to the engine to ingest the latest transaction - await new Promise(res => setTimeout(res, 1500)) - rcp = await helpers.callRPCMethod( - 'eth_getTransactionReceipt', - [revertedTx.hash] - ) // make sure the `revertReason` field is included in the response assert.equal( rcp.body['result'].revertReason, @@ -74,22 +75,22 @@ it('store revertReason field in transaction receipts', async () => { [signedTx.rawTransaction] ) assert.equal(200, response.status) + txHash = response.body.result - latestHeight = await web3.eth.getBlockNumber() - block = await web3.eth.getBlock(latestHeight) - assert.equal(block.number, conf.startBlockHeight + 3n) + rcp = null + while (rcp == null) { + rcp = await helpers.callRPCMethod( + 'eth_getTransactionReceipt', + [txHash] + ) + if (rcp.body.result == null) { + rcp = null + } + } - revertedTx = await web3.eth.getTransactionFromBlock(latestHeight, 0) - // Give some time to the engine to ingest the latest transaction - await new Promise(res => setTimeout(res, 1500)) - rcp = await helpers.callRPCMethod( - 'eth_getTransactionReceipt', - [revertedTx.hash] - ) // make sure the `revertReason` field is included in the response assert.equal( rcp.body['result'].revertReason, '0x9195785a00000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000001056616c756520697320746f6f206c6f7700000000000000000000000000000000' ) - }) diff --git a/tests/web3js/verify_cadence_arch_calls_test.js b/tests/web3js/verify_cadence_arch_calls_test.js new file mode 100644 index 00000000..7188cfac --- /dev/null +++ b/tests/web3js/verify_cadence_arch_calls_test.js @@ -0,0 +1,148 @@ +const utils = require('web3-utils') +const { assert } = require('chai') +const conf = require('./config') +const helpers = require('./helpers') +const web3 = conf.web3 + +it('should be able to use Cadence Arch calls', async () => { + let latest = await web3.eth.getBlockNumber() + let expectedBlockHeight = conf.startBlockHeight + assert.equal(latest, expectedBlockHeight) + + let deployed = await helpers.deployContract('storage') + let contractAddress = deployed.receipt.contractAddress + + // submit a transaction that calls blockNumber() + let blockNumberData = deployed.contract.methods.blockNumber().encodeABI() + let res = await helpers.signAndSend({ + from: conf.eoa.address, + to: contractAddress, + data: blockNumberData, + value: '0', + gasPrice: conf.minGasPrice, + }) + assert.equal(res.receipt.status, conf.successStatus) + + // submit a transaction that calls blockTime() + let blockTimeData = deployed.contract.methods.blockNumber().encodeABI() + res = await helpers.signAndSend({ + from: conf.eoa.address, + to: contractAddress, + data: blockTimeData, + value: '0', + gasPrice: conf.minGasPrice, + }) + assert.equal(res.receipt.status, conf.successStatus) + + // submit a transaction that calls blockHash(uint num) + let blockHashData = deployed.contract.methods.blockHash(110).encodeABI() + res = await helpers.signAndSend({ + from: conf.eoa.address, + to: contractAddress, + data: blockHashData, + value: '0', + gasPrice: conf.minGasPrice, + }) + assert.equal(res.receipt.status, conf.successStatus) + + // submit a transaction that calls random() + let randomData = deployed.contract.methods.random().encodeABI() + res = await helpers.signAndSend({ + from: conf.eoa.address, + to: contractAddress, + data: randomData, + value: '0', + gasPrice: conf.minGasPrice, + }) + assert.equal(res.receipt.status, conf.successStatus) + + // submit a transaction that calls chainID() + let chainIDData = deployed.contract.methods.chainID().encodeABI() + res = await helpers.signAndSend({ + from: conf.eoa.address, + to: contractAddress, + data: chainIDData, + value: '0', + gasPrice: conf.minGasPrice, + }) + assert.equal(res.receipt.status, conf.successStatus) + + // submit a transaction that calls verifyArchCallToRandomSource(uint64 height) + let getRandomSourceData = deployed.contract.methods.verifyArchCallToRandomSource(120).encodeABI() + res = await helpers.signAndSend({ + from: conf.eoa.address, + to: contractAddress, + data: getRandomSourceData, + value: '0', + gasPrice: conf.minGasPrice, + }) + assert.equal(res.receipt.status, conf.successStatus) + + // make a contract call for verifyArchCallToRandomSource(uint64 height) + res = await web3.eth.call({ to: contractAddress, data: getRandomSourceData }, latest) + assert.notEqual( + res, + '0x0000000000000000000000000000000000000000000000000000000000000000' + ) + assert.lengthOf(res, 66) + + // submit a transaction that calls verifyArchCallToRevertibleRandom() + let revertibleRandomData = deployed.contract.methods.verifyArchCallToRevertibleRandom().encodeABI() + res = await helpers.signAndSend({ + from: conf.eoa.address, + to: contractAddress, + data: revertibleRandomData, + value: '0', + gasPrice: conf.minGasPrice, + }) + assert.equal(res.receipt.status, conf.successStatus) + + // make a contract call for verifyArchCallToRevertibleRandom() + res = await web3.eth.call({ to: contractAddress, data: revertibleRandomData }, latest) + assert.notEqual( + res, + '0x0000000000000000000000000000000000000000000000000000000000000000' + ) + assert.lengthOf(res, 66) + + // submit a transaction that calls verifyArchCallToFlowBlockHeight() + let flowBlockHeightData = deployed.contract.methods.verifyArchCallToFlowBlockHeight().encodeABI() + res = await helpers.signAndSend({ + from: conf.eoa.address, + to: contractAddress, + data: flowBlockHeightData, + value: '0', + gasPrice: conf.minGasPrice, + }) + assert.equal(res.receipt.status, conf.successStatus) + + // make a contract call for verifyArchCallToFlowBlockHeight() + res = await web3.eth.call({ to: contractAddress, data: flowBlockHeightData }, latest) + assert.equal( + web3.eth.abi.decodeParameter('uint64', res), + latest, + ) + + // submit a transaction that calls verifyArchCallToVerifyCOAOwnershipProof(address,bytes32,bytes) + let tx = await web3.eth.getTransactionFromBlock(conf.startBlockHeight, 1) + let verifyCOAOwnershipProofData = deployed.contract.methods.verifyArchCallToVerifyCOAOwnershipProof( + tx.to, + '0x1bacdb569847f31ade07e83d6bb7cefba2b9290b35d5c2964663215e73519cff', + web3.utils.hexToBytes('f853c18088f8d6e0586b0a20c78365766df842b840b90448f4591df2639873be2914c5560149318b7e2fcf160f7bb8ed13cfd97be2f54e6889606f18e50b2c37308386f840e03a9fff915f57b2164cba27f0206a95') + ).encodeABI() + res = await helpers.signAndSend({ + from: conf.eoa.address, + to: contractAddress, + data: verifyCOAOwnershipProofData, + value: '0', + gasPrice: conf.minGasPrice, + }) + assert.equal(res.receipt.status, conf.successStatus) + + // make a contract call for verifyArchCallToVerifyCOAOwnershipProof(address,bytes32,bytes) + res = await web3.eth.call({ to: contractAddress, data: verifyCOAOwnershipProofData }, latest) + assert.equal( + web3.eth.abi.decodeParameter('bool', res), + false, + ) +})