diff --git a/README.md b/README.md index b4c7e148..d68c3259 100644 --- a/README.md +++ b/README.md @@ -217,7 +217,7 @@ EVM Gateway has public RPC endpoints available for the following environments: | Name | Value | |-----------------|----------------------------------------| -| Network Name | EVM on Flow Testnet | +| Network Name | Testnet | | Description | The public RPC URL for Flow Testnet | | RPC Endpoint | https://testnet.evm.nodes.onflow.org | | Chain ID | 545 | @@ -226,7 +226,7 @@ EVM Gateway has public RPC endpoints available for the following environments: | Name | Value | |-----------------|----------------------------------------| -| Network Name | EVM on Flow | +| Network Name | Mainnet | | Description | The public RPC URL for Flow Mainnet | | RPC Endpoint | https://mainnet.evm.nodes.onflow.org | | Chain ID | 747 | diff --git a/api/api.go b/api/api.go index 2cc52ca7..a9e666a8 100644 --- a/api/api.go +++ b/api/api.go @@ -83,7 +83,7 @@ func SupportedAPIs( type BlockChainAPI struct { logger zerolog.Logger config *config.Config - evm requester.Requester + evm requester.EVMClient blocks storage.BlockIndexer transactions storage.TransactionIndexer receipts storage.ReceiptIndexer @@ -96,7 +96,7 @@ type BlockChainAPI struct { func NewBlockChainAPI( logger zerolog.Logger, config *config.Config, - evm requester.Requester, + evm requester.EVMClient, blocks storage.BlockIndexer, transactions storage.TransactionIndexer, receipts storage.ReceiptIndexer, @@ -105,7 +105,7 @@ func NewBlockChainAPI( collector metrics.Collector, ) (*BlockChainAPI, error) { // get the height from which the indexing resumed since the last restart, this is needed for syncing status. - indexingResumedHeight, err := blocks.LatestEVMHeight() + indexingResumedHeight, err := blocks.LatestIndexedHeight() if err != nil { return nil, fmt.Errorf("failed to retrieve the indexing resumed height: %w", err) } @@ -130,12 +130,12 @@ func (b *BlockChainAPI) BlockNumber(ctx context.Context) (hexutil.Uint64, error) return 0, err } - latestBlockHeight, err := b.blocks.LatestEVMHeight() + latest, err := b.blocks.LatestExecutedHeight() if err != nil { - return handleError[hexutil.Uint64](err, b.logger, b.collector) + return hexutil.Uint64(0), err } - return hexutil.Uint64(latestBlockHeight), nil + return hexutil.Uint64(latest), nil } // Syncing returns false in case the node is currently not syncing with the network. @@ -149,7 +149,7 @@ func (b *BlockChainAPI) Syncing(ctx context.Context) (interface{}, error) { return nil, err } - currentBlock, err := b.blocks.LatestEVMHeight() + currentBlock, err := b.blocks.LatestExecutedHeight() if err != nil { return handleError[any](err, b.logger, b.collector) } @@ -214,7 +214,7 @@ func (b *BlockChainAPI) GetBalance( return nil, err } - evmHeight, err := b.getBlockNumber(&blockNumberOrHash) + evmHeight, err := b.resolveBlockNumberOrHash(&blockNumberOrHash) if err != nil { return handleError[*hexutil.Big](err, l, b.collector) } @@ -246,6 +246,7 @@ func (b *BlockChainAPI) GetTransactionByHash( return handleError[*Transaction](err, l, b.collector) } + // todo what if there's no receipt yet? but tx exists rcp, err := b.receipts.GetByTransactionID(hash) if err != nil { return handleError[*Transaction](err, l, b.collector) @@ -305,15 +306,12 @@ func (b *BlockChainAPI) GetTransactionByBlockNumberAndIndex( return nil, err } - if blockNumber < rpc.EarliestBlockNumber { - latestBlockNumber, err := b.blocks.LatestEVMHeight() - if err != nil { - return handleError[*Transaction](err, l, b.collector) - } - blockNumber = rpc.BlockNumber(latestBlockNumber) + height, err := b.resolveBlockNumber(blockNumber) + if err != nil { + return nil, err } - block, err := b.blocks.GetByHeight(uint64(blockNumber)) + block, err := b.blocks.GetByHeight(uint64(height)) if err != nil { return handleError[*Transaction](err, l, b.collector) } @@ -335,7 +333,7 @@ func (b *BlockChainAPI) GetTransactionByBlockNumberAndIndex( func (b *BlockChainAPI) GetTransactionReceipt( ctx context.Context, hash common.Hash, -) (map[string]interface{}, error) { +) (map[string]any, error) { l := b.logger.With(). Str("endpoint", "getTransactionReceipt"). Str("hash", hash.String()). @@ -347,17 +345,27 @@ func (b *BlockChainAPI) GetTransactionReceipt( tx, err := b.transactions.Get(hash) if err != nil { - return handleError[map[string]interface{}](err, l, b.collector) + return handleError[map[string]any](err, l, b.collector) } receipt, err := b.receipts.GetByTransactionID(hash) if err != nil { - return handleError[map[string]interface{}](err, l, b.collector) + return handleError[map[string]any](err, l, b.collector) + } + + // we don't return receipts until local state index + // recreated the state by executing the transaction + latestExecutedHeight, err := b.blocks.LatestExecutedHeight() + if err != nil { + return handleError[map[string]any](err, l, b.collector) + } + if receipt.BlockNumber.Uint64() > latestExecutedHeight { + return nil, nil } txReceipt, err := MarshalReceipt(receipt, tx) if err != nil { - return handleError[map[string]interface{}](err, l, b.collector) + return handleError[map[string]any](err, l, b.collector) } return txReceipt, nil @@ -413,17 +421,12 @@ func (b *BlockChainAPI) GetBlockByNumber( return nil, err } - height := uint64(blockNumber) - var err error - if blockNumber < 0 { - height, err = b.blocks.LatestEVMHeight() - if err != nil { - return handleError[*Block](err, l, b.collector) - } + height, err := b.resolveBlockNumber(blockNumber) + if err != nil { + return handleError[*Block](err, l, b.collector) } - block, err := b.blocks.GetByHeight(height) - + block, err := b.blocks.GetByHeight(uint64(height)) if err != nil { return handleError[*Block](err, l, b.collector) } @@ -439,51 +442,42 @@ func (b *BlockChainAPI) GetBlockByNumber( // GetBlockReceipts returns the block receipts for the given block hash or number or tag. func (b *BlockChainAPI) GetBlockReceipts( ctx context.Context, - blockNumberOrHash rpc.BlockNumberOrHash, -) ([]map[string]interface{}, error) { + numHash rpc.BlockNumberOrHash, +) ([]map[string]any, error) { l := b.logger.With(). Str("endpoint", "getBlockReceipts"). - Str("hash", blockNumberOrHash.String()). + Str("hash", numHash.String()). Logger() if err := rateLimit(ctx, b.limiter, l); err != nil { return nil, err } - var ( - block *models.Block - err error - ) - if blockNumberOrHash.BlockHash != nil { - block, err = b.blocks.GetByID(*blockNumberOrHash.BlockHash) - } else if blockNumberOrHash.BlockNumber != nil { - block, err = b.blocks.GetByHeight(uint64(blockNumberOrHash.BlockNumber.Int64())) - } else { - return handleError[[]map[string]interface{}]( - fmt.Errorf("%w: block number or hash not provided", errs.ErrInvalid), - l, - b.collector, - ) + height, err := b.resolveBlockNumberOrHash(&numHash) + if err != nil { + return handleError[[]map[string]any](err, l, b.collector) } + + block, err := b.blocks.GetByHeight(uint64(height)) if err != nil { - return handleError[[]map[string]interface{}](err, l, b.collector) + return handleError[[]map[string]any](err, l, b.collector) } receipts := make([]map[string]interface{}, len(block.TransactionHashes)) for i, hash := range block.TransactionHashes { tx, err := b.transactions.Get(hash) if err != nil { - return handleError[[]map[string]interface{}](err, l, b.collector) + return handleError[[]map[string]any](err, l, b.collector) } receipt, err := b.receipts.GetByTransactionID(hash) if err != nil { - return handleError[[]map[string]interface{}](err, l, b.collector) + return handleError[[]map[string]any](err, l, b.collector) } receipts[i], err = MarshalReceipt(receipt, tx) if err != nil { - return handleError[[]map[string]interface{}](err, l, b.collector) + return handleError[[]map[string]any](err, l, b.collector) } } @@ -529,15 +523,12 @@ func (b *BlockChainAPI) GetBlockTransactionCountByNumber( return nil, err } - if blockNumber < rpc.EarliestBlockNumber { - latestBlockNumber, err := b.blocks.LatestEVMHeight() - if err != nil { - return handleError[*hexutil.Uint](err, l, b.collector) - } - blockNumber = rpc.BlockNumber(latestBlockNumber) + height, err := b.resolveBlockNumber(blockNumber) + if err != nil { + return handleError[*hexutil.Uint](err, l, b.collector) } - block, err := b.blocks.GetByHeight(uint64(blockNumber)) + block, err := b.blocks.GetByHeight(uint64(height)) if err != nil { return handleError[*hexutil.Uint](err, l, b.collector) } @@ -576,7 +567,7 @@ func (b *BlockChainAPI) Call( blockNumberOrHash = &latestBlockNumberOrHash } - evmHeight, err := b.getBlockNumber(blockNumberOrHash) + evmHeight, err := b.resolveBlockNumberOrHash(blockNumberOrHash) if err != nil { return handleError[hexutil.Bytes](err, l, b.collector) } @@ -637,6 +628,7 @@ func (b *BlockChainAPI) GetLogs( // otherwise we use the block range as the filter // assign default values to latest block number, unless provided + // todo should we resolve latest to specific height from := models.LatestBlockNumber if criteria.FromBlock != nil { from = criteria.FromBlock @@ -646,7 +638,7 @@ func (b *BlockChainAPI) GetLogs( to = criteria.ToBlock } - h, err := b.blocks.LatestEVMHeight() + h, err := b.blocks.LatestIndexedHeight() if err != nil { return handleError[[]*types.Log](err, l, b.collector) } @@ -694,7 +686,7 @@ func (b *BlockChainAPI) GetTransactionCount( return nil, err } - evmHeight, err := b.getBlockNumber(&blockNumberOrHash) + evmHeight, err := b.resolveBlockNumberOrHash(&blockNumberOrHash) if err != nil { return handleError[*hexutil.Uint64](err, l, b.collector) } @@ -760,7 +752,7 @@ func (b *BlockChainAPI) EstimateGas( blockNumberOrHash = &latestBlockNumberOrHash } - evmHeight, err := b.getBlockNumber(blockNumberOrHash) + evmHeight, err := b.resolveBlockNumberOrHash(blockNumberOrHash) if err != nil { return handleError[hexutil.Uint64](err, l, b.collector) } @@ -789,7 +781,7 @@ func (b *BlockChainAPI) GetCode( return nil, err } - evmHeight, err := b.getBlockNumber(&blockNumberOrHash) + evmHeight, err := b.resolveBlockNumberOrHash(&blockNumberOrHash) if err != nil { return handleError[hexutil.Bytes](err, l, b.collector) } @@ -834,7 +826,7 @@ func (b *BlockChainAPI) FeeHistory( var err error if lastBlock < 0 { // From the special block tags, we only support "latest". - lastBlockNumber, err = b.blocks.LatestEVMHeight() + lastBlockNumber, err = b.blocks.LatestIndexedHeight() if err != nil { return handleError[*FeeHistoryResult](err, l, b.collector) } @@ -914,7 +906,7 @@ func (b *BlockChainAPI) GetStorageAt( ) } - evmHeight, err := b.getBlockNumber(&blockNumberOrHash) + evmHeight, err := b.resolveBlockNumberOrHash(&blockNumberOrHash) if err != nil { return handleError[hexutil.Bytes](err, l, b.collector) } @@ -1029,27 +1021,45 @@ func (b *BlockChainAPI) prepareBlockResponse( return blockResponse, nil } -func (b *BlockChainAPI) getBlockNumber(blockNumberOrHash *rpc.BlockNumberOrHash) (int64, error) { +// resolveBlockNumberOrHash resolves the block number or hash into the evm block number. +// If block number is negative we resolve to latest executed height. +func (b *BlockChainAPI) resolveBlockNumberOrHash(block *rpc.BlockNumberOrHash) (uint64, error) { err := fmt.Errorf("%w: neither block number nor hash specified", errs.ErrInvalid) - if blockNumberOrHash == nil { + if block == nil { return 0, err } - if number, ok := blockNumberOrHash.Number(); ok { - return number.Int64(), nil + if number, ok := block.Number(); ok { + return b.resolveBlockNumber(number) } - if hash, ok := blockNumberOrHash.Hash(); ok { + if hash, ok := block.Hash(); ok { evmHeight, err := b.blocks.GetHeightByID(hash) if err != nil { - b.logger.Error().Err(err).Msg("failed to get block by hash") return 0, err } - return int64(evmHeight), nil + return evmHeight, nil } return 0, err } +// resolveBlockNumber resolves the block number into the evm block number. +// If block number is negative we resolve to latest executed height. +func (b *BlockChainAPI) resolveBlockNumber(number rpc.BlockNumber) (uint64, error) { + height := number.Int64() + + // if special values (latest) we return latest executed height + if height < 0 { + executed, err := b.blocks.LatestExecutedHeight() + 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/pull.go b/api/pull.go index bf5eb91a..f8e4b98d 100644 --- a/api/pull.go +++ b/api/pull.go @@ -173,7 +173,7 @@ func (api *PullAPI) NewPendingTransactionFilter( return "", err } - last, err := api.blocks.LatestEVMHeight() + last, err := api.blocks.LatestIndexedHeight() if err != nil { return "", err } @@ -201,7 +201,7 @@ func (api *PullAPI) NewBlockFilter(ctx context.Context) (rpc.ID, error) { return "", err } - last, err := api.blocks.LatestEVMHeight() + last, err := api.blocks.LatestIndexedHeight() if err != nil { return "", err } @@ -250,7 +250,7 @@ func (api *PullAPI) NewFilter(ctx context.Context, criteria filters.FilterCriter return "", err } - latest, err := api.blocks.LatestEVMHeight() + latest, err := api.blocks.LatestIndexedHeight() if err != nil { return "", err } @@ -316,7 +316,7 @@ func (api *PullAPI) GetFilterLogs( ) } - current, err := api.blocks.LatestEVMHeight() + current, err := api.blocks.LatestIndexedHeight() if err != nil { return nil, err } @@ -352,7 +352,7 @@ func (api *PullAPI) GetFilterChanges(ctx context.Context, id rpc.ID) (any, error return nil, fmt.Errorf("filter by id %s does not exist", id) } - current, err := api.blocks.LatestEVMHeight() + current, err := api.blocks.LatestIndexedHeight() if err != nil { return nil, err } diff --git a/bootstrap/bootstrap.go b/bootstrap/bootstrap.go index acf7f323..59d19f81 100644 --- a/bootstrap/bootstrap.go +++ b/bootstrap/bootstrap.go @@ -21,6 +21,7 @@ import ( errs "github.com/onflow/flow-evm-gateway/models/errors" "github.com/onflow/flow-evm-gateway/services/ingestion" "github.com/onflow/flow-evm-gateway/services/requester" + "github.com/onflow/flow-evm-gateway/services/state" "github.com/onflow/flow-evm-gateway/services/traces" "github.com/onflow/flow-evm-gateway/storage" "github.com/onflow/flow-evm-gateway/storage/pebble" @@ -33,6 +34,7 @@ type Storages struct { Receipts storage.ReceiptIndexer Accounts storage.AccountIndexer Traces storage.TraceIndexer + Registers *pebble.Register } type Publishers struct { @@ -44,14 +46,16 @@ type Publishers struct { type Bootstrap struct { logger zerolog.Logger config *config.Config - client *requester.CrossSporkClient - storages *Storages - publishers *Publishers + Client *requester.CrossSporkClient + EVMClient requester.EVMClient + Storages *Storages + Publishers *Publishers collector metrics.Collector - server *api.Server + Server *api.Server metrics *metrics.Server - events *ingestion.Engine - traces *traces.Engine + Events *ingestion.Engine + Traces *traces.Engine + State *state.Engine } func New(config *config.Config) (*Bootstrap, error) { @@ -69,15 +73,15 @@ func New(config *config.Config) (*Bootstrap, error) { } return &Bootstrap{ - publishers: &Publishers{ + Publishers: &Publishers{ Block: models.NewPublisher(), Transaction: models.NewPublisher(), Logs: models.NewPublisher(), }, - storages: storages, + Storages: storages, logger: logger, config: config, - client: client, + Client: client, collector: metrics.NewCollector(logger), }, nil } @@ -87,18 +91,18 @@ func (b *Bootstrap) StartEventIngestion(ctx context.Context) error { l.Info().Msg("bootstrap starting event ingestion") // get latest cadence block from the network and the database - latestCadenceBlock, err := b.client.GetLatestBlock(context.Background(), true) + latestCadenceBlock, err := b.Client.GetLatestBlock(context.Background(), true) if err != nil { return fmt.Errorf("failed to get latest cadence block: %w", err) } - latestCadenceHeight, err := b.storages.Blocks.LatestCadenceHeight() + latestCadenceHeight, err := b.Storages.Blocks.LatestCadenceHeight() if err != nil { return err } // make sure the provided block to start the indexing can be loaded - _, err = b.client.GetBlockHeaderByHeight(context.Background(), latestCadenceHeight) + _, err = b.Client.GetBlockHeaderByHeight(context.Background(), latestCadenceHeight) if err != nil { return fmt.Errorf( "failed to get provided cadence height %d: %w", @@ -115,27 +119,27 @@ func (b *Bootstrap) StartEventIngestion(ctx context.Context) error { // create event subscriber subscriber := ingestion.NewRPCSubscriber( - b.client, + b.Client, b.config.HeartbeatInterval, b.config.FlowNetworkID, b.logger, ) // initialize event ingestion engine - b.events = ingestion.NewEventIngestionEngine( + b.Events = ingestion.NewEventIngestionEngine( subscriber, - b.storages.Storage, - b.storages.Blocks, - b.storages.Receipts, - b.storages.Transactions, - b.storages.Accounts, - b.publishers.Block, - b.publishers.Logs, + b.Storages.Storage, + b.Storages.Blocks, + b.Storages.Receipts, + b.Storages.Transactions, + b.Storages.Accounts, + b.Publishers.Block, + b.Publishers.Logs, b.logger, b.collector, ) - startEngine(ctx, b.events, l) + startEngine(ctx, b.Events, l) return nil } @@ -150,39 +154,71 @@ func (b *Bootstrap) StartTraceDownloader(ctx context.Context) error { } // initialize trace downloader engine - b.traces = traces.NewTracesIngestionEngine( - b.publishers.Block, - b.storages.Blocks, - b.storages.Traces, + b.Traces = traces.NewTracesIngestionEngine( + b.Publishers.Block, + b.Storages.Blocks, + b.Storages.Traces, downloader, b.logger, b.collector, ) - startEngine(ctx, b.traces, l) + startEngine(ctx, b.Traces, l) return nil } func (b *Bootstrap) StopTraceDownloader() { - if b.traces == nil { + if b.Traces == nil { return } b.logger.Warn().Msg("stopping trace downloader engine") - b.traces.Stop() + b.Traces.Stop() } func (b *Bootstrap) StopEventIngestion() { - if b.events == nil { + if b.Events == nil { return } b.logger.Warn().Msg("stopping event ingestion engine") - b.events.Stop() + b.Events.Stop() +} + +func (b *Bootstrap) StartStateIndex(ctx context.Context) error { + l := b.logger.With().Str("component", "bootstrap-state").Logger() + l.Info().Msg("starting engine") + + execution, ok := b.Client.Client.(*grpc.Client) + if !ok { + return fmt.Errorf("execution data client not supported on the provided AN client") + } + + b.State = state.NewStateEngine( + b.config, + execution.ExecutionDataRPCClient(), + b.Publishers.Block, + b.Storages.Storage, + b.Storages.Blocks, + b.Storages.Transactions, + b.Storages.Receipts, + b.logger, + ) + + startEngine(ctx, b.State, l) + return nil +} + +func (b *Bootstrap) StopStateIndex() { + if b.State == nil { + return + } + b.logger.Warn().Msg("stopping state index engine") + b.State.Stop() } func (b *Bootstrap) StartAPIServer(ctx context.Context) error { b.logger.Info().Msg("bootstrap starting metrics server") - b.server = api.NewServer(b.logger, b.collector, b.config) + b.Server = api.NewServer(b.logger, b.collector, b.config) // create the signer based on either a single coa key being provided and using a simple in-memory // signer, or multiple keys being provided and using signer with key-rotation mechanism. @@ -207,15 +243,17 @@ func (b *Bootstrap) StartAPIServer(ctx context.Context) error { } // create transaction pool - txPool := requester.NewTxPool(b.client, b.publishers.Transaction, b.logger) + txPool := requester.NewTxPool(b.Client, b.Publishers.Transaction, b.logger) - evm, err := requester.NewEVM( - b.client, + b.EVMClient, err = requester.NewClientHandler( b.config, + b.Storages.Storage, + txPool, signer, + b.Client, + b.Storages.Blocks, + b.Storages.Receipts, b.logger, - b.storages.Blocks, - txPool, b.collector, ) if err != nil { @@ -237,11 +275,11 @@ func (b *Bootstrap) StartAPIServer(ctx context.Context) error { blockchainAPI, err := api.NewBlockChainAPI( b.logger, b.config, - evm, - b.storages.Blocks, - b.storages.Transactions, - b.storages.Receipts, - b.storages.Accounts, + b.EVMClient, + b.Storages.Blocks, + b.Storages.Transactions, + b.Storages.Receipts, + b.Storages.Accounts, ratelimiter, b.collector, ) @@ -252,27 +290,27 @@ func (b *Bootstrap) StartAPIServer(ctx context.Context) error { streamAPI := api.NewStreamAPI( b.logger, b.config, - b.storages.Blocks, - b.storages.Transactions, - b.storages.Receipts, - b.publishers.Block, - b.publishers.Transaction, - b.publishers.Logs, + b.Storages.Blocks, + b.Storages.Transactions, + b.Storages.Receipts, + b.Publishers.Block, + b.Publishers.Transaction, + b.Publishers.Logs, ratelimiter, ) pullAPI := api.NewPullAPI( b.logger, b.config, - b.storages.Blocks, - b.storages.Transactions, - b.storages.Receipts, + b.Storages.Blocks, + b.Storages.Transactions, + b.Storages.Receipts, 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.Traces, b.Storages.Blocks, b.logger, b.collector) } var walletAPI *api.WalletAPI @@ -289,34 +327,34 @@ func (b *Bootstrap) StartAPIServer(ctx context.Context) error { b.config, ) - if err := b.server.EnableRPC(supportedAPIs); err != nil { + if err := b.Server.EnableRPC(supportedAPIs); err != nil { return err } if b.config.WSEnabled { - if err := b.server.EnableWS(supportedAPIs); err != nil { + if err := b.Server.EnableWS(supportedAPIs); err != nil { return err } } - if err := b.server.SetListenAddr(b.config.RPCHost, b.config.RPCPort); err != nil { + if err := b.Server.SetListenAddr(b.config.RPCHost, b.config.RPCPort); err != nil { return err } - if err := b.server.Start(); err != nil { + if err := b.Server.Start(); err != nil { return err } - b.logger.Info().Msgf("API server started: %s", b.server.ListenAddr()) + b.logger.Info().Msgf("API server started: %s", b.Server.ListenAddr()) return nil } func (b *Bootstrap) StopAPIServer() { - if b.server == nil { + if b.Server == nil { return } b.logger.Warn().Msg("shutting down API server") - b.server.Stop() + b.Server.Stop() } func (b *Bootstrap) StartMetricsServer(_ context.Context) error { @@ -466,6 +504,14 @@ func Run(ctx context.Context, cfg *config.Config, ready chan struct{}) error { } } + if err := boot.StartStateIndex(ctx); err != nil { + return fmt.Errorf("failed to start local state index engine: %w", err) + } + + // we must wait for state index to be ready before starting the ingestion engine, + // because state index might have to catch-up executed blocks to indexed block height + <-boot.State.Ready() + if err := boot.StartEventIngestion(ctx); err != nil { return fmt.Errorf("failed to start event ingestion engine: %w", err) } @@ -478,6 +524,9 @@ func Run(ctx context.Context, cfg *config.Config, ready chan struct{}) error { return fmt.Errorf("failed to start metrics server: %w", err) } + // wait for event ingestion engine + <-boot.Events.Ready() + // mark ready close(ready) diff --git a/config/config.go b/config/config.go index 45c97296..17b52f56 100644 --- a/config/config.go +++ b/config/config.go @@ -97,6 +97,9 @@ type Config struct { IndexOnly bool // Cache size in units of items in cache, one unit in cache takes approximately 64 bytes CacheSize uint + // ValidateRegisters sets whether we should check each register set in a local state against the on-chain data + // this should not be set if the node is syncing-up since it will produce a lot of requests + ValidateRegisters bool } func FromFlags() (*Config, error) { @@ -159,6 +162,7 @@ func FromFlags() (*Config, error) { flag.StringVar(&walletKey, "wallet-api-key", "", "ECDSA private key used for wallet APIs. WARNING: This should only be used locally or for testing, never in production.") flag.IntVar(&cfg.MetricsPort, "metrics-port", 9091, "Port for the metrics server") flag.BoolVar(&cfg.IndexOnly, "index-only", false, "Run the gateway in index-only mode which only allows querying the state and indexing, but disallows sending transactions.") + flag.BoolVar(&cfg.ValidateRegisters, "validate-registers", false, "Enable checking set registers in local state against the on-chain data. Should only be set when the node is up-to-sync.") flag.Parse() if coinbase == "" { diff --git a/go.mod b/go.mod index d07f7830..087dabbb 100644 --- a/go.mod +++ b/go.mod @@ -10,9 +10,9 @@ require ( github.com/hashicorp/golang-lru/v2 v2.0.7 github.com/onflow/atree v0.8.0-rc.6 github.com/onflow/cadence v1.0.0-preview.52 - github.com/onflow/flow-go v0.37.10 + github.com/onflow/flow-go v0.37.10-util-ensure-checkpoint-exists.0.20240914104351-c2d9833c3357 github.com/onflow/flow-go-sdk v1.0.0-preview.56 - github.com/onflow/flow/protobuf/go/flow v0.4.6 + github.com/onflow/flow/protobuf/go/flow v0.4.7 github.com/onflow/go-ethereum v1.14.7 github.com/prometheus/client_golang v1.18.0 github.com/rs/cors v1.8.0 @@ -142,7 +142,6 @@ require ( github.com/onflow/flow-nft/lib/go/contracts v1.2.1 // indirect github.com/onflow/flow-nft/lib/go/templates v1.2.0 // indirect github.com/onflow/sdks v0.6.0-preview.1 // indirect - github.com/onsi/ginkgo v1.16.4 // indirect github.com/onsi/gomega v1.18.1 // indirect github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect github.com/pelletier/go-toml/v2 v2.0.6 // indirect diff --git a/go.sum b/go.sum index ef952984..b4d09324 100644 --- a/go.sum +++ b/go.sum @@ -1529,7 +1529,10 @@ github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/ad github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/graph-gophers/graphql-go v1.3.0/go.mod h1:9CQHMSxwO4MprSdzoIEobiHpoLtHm77vfxsvsIN5Vuc= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.0 h1:Iju5GlWwrvL6UBg4zJJt3btmonfrMlCDdsejg4CZE7c= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0 h1:pRhl55Yx1eC7BZ1N+BBWwnKaMyD8uC+34TLdndZMAKk= +github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0/go.mod h1:XKMd7iuf/RGPSMJ/U4HP0zS2Z9Fh8Ps9a+6X26m/tmI= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= @@ -1862,8 +1865,8 @@ github.com/onflow/flow-ft/lib/go/contracts v1.0.0 h1:mToacZ5NWqtlWwk/7RgIl/jeKB/ github.com/onflow/flow-ft/lib/go/contracts v1.0.0/go.mod h1:PwsL8fC81cjnUnTfmyL/HOIyHnyaw/JA474Wfj2tl6A= github.com/onflow/flow-ft/lib/go/templates v1.0.0 h1:6cMS/lUJJ17HjKBfMO/eh0GGvnpElPgBXx7h5aoWJhs= github.com/onflow/flow-ft/lib/go/templates v1.0.0/go.mod h1:uQ8XFqmMK2jxyBSVrmyuwdWjTEb+6zGjRYotfDJ5pAE= -github.com/onflow/flow-go v0.37.10 h1:Nz2Gp63+0ubb9FuQaEZgCsXNXM5WsXq/j0ukC74N5Vw= -github.com/onflow/flow-go v0.37.10/go.mod h1:bfOCsCk0v1J93vXd+zrYkCmRIVOaL9oAXvNFWgVOujE= +github.com/onflow/flow-go v0.37.10-util-ensure-checkpoint-exists.0.20240914104351-c2d9833c3357 h1:7gJ5RVKZEsUqPSKglpMXUBn+hceJ1cd/PsmLVsd5uzQ= +github.com/onflow/flow-go v0.37.10-util-ensure-checkpoint-exists.0.20240914104351-c2d9833c3357/go.mod h1:Gdqw1ptnAUuB0izif88PWMK8abe655Hr8iEkXXuUJl4= github.com/onflow/flow-go-sdk v1.0.0-M1/go.mod h1:TDW0MNuCs4SvqYRUzkbRnRmHQL1h4X8wURsCw9P9beo= github.com/onflow/flow-go-sdk v1.0.0-preview.56 h1:ZnFznUXI1V8iZ+cKxoJRIeQwJTHItriKpnoKf8hFFso= github.com/onflow/flow-go-sdk v1.0.0-preview.56/go.mod h1:rBRNboXaTprn7M0MeO6/R1bxNpctbrx66I2FLp0V6fM= @@ -1872,8 +1875,8 @@ github.com/onflow/flow-nft/lib/go/contracts v1.2.1/go.mod h1:2gpbza+uzs1k7x31hkp github.com/onflow/flow-nft/lib/go/templates v1.2.0 h1:JSQyh9rg0RC+D1930BiRXN8lrtMs+ubVMK6aQPon6Yc= github.com/onflow/flow-nft/lib/go/templates v1.2.0/go.mod h1:p+2hRvtjLUR3MW1NsoJe5Gqgr2eeH49QB6+s6ze00w0= github.com/onflow/flow/protobuf/go/flow v0.3.2-0.20231121210617-52ee94b830c2/go.mod h1:NA2pX2nw8zuaxfKphhKsk00kWLwfd+tv8mS23YXO4Sk= -github.com/onflow/flow/protobuf/go/flow v0.4.6 h1:KE/CsRVfyG5lGBtm1aNcjojMciQyS5GfPF3ixOWRfi0= -github.com/onflow/flow/protobuf/go/flow v0.4.6/go.mod h1:NA2pX2nw8zuaxfKphhKsk00kWLwfd+tv8mS23YXO4Sk= +github.com/onflow/flow/protobuf/go/flow v0.4.7 h1:iP6DFx4wZ3ETORsyeqzHu7neFT3d1CXF6wdK+AOOjmc= +github.com/onflow/flow/protobuf/go/flow v0.4.7/go.mod h1:NA2pX2nw8zuaxfKphhKsk00kWLwfd+tv8mS23YXO4Sk= github.com/onflow/go-ethereum v1.14.7 h1:gg3awYqI02e3AypRdpJKEvNTJ6kz/OhAqRti0h54Wlc= github.com/onflow/go-ethereum v1.14.7/go.mod h1:zV14QLrXyYu5ucvcwHUA0r6UaqveqbXaehAVQJlSW+I= github.com/onflow/sdks v0.5.1-0.20230912225508-b35402f12bba/go.mod h1:F0dj0EyHC55kknLkeD10js4mo14yTdMotnWMslPirrU= @@ -1886,8 +1889,9 @@ github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+ github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.13.0/go.mod h1:+REjRxOmWfHCjfv9TTWB1jD1Frx4XydAD3zm1lskyM0= github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= -github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= +github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= github.com/onsi/ginkgo/v2 v2.0.0/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= diff --git a/models/block.go b/models/block.go index e77a431b..a3beccaf 100644 --- a/models/block.go +++ b/models/block.go @@ -73,10 +73,6 @@ func (b *Block) Hash() (gethCommon.Hash, error) { func decodeBlockEvent(event cadence.Event) (*Block, error) { payload, err := events.DecodeBlockEventPayload(event) if err != nil { - if block, err := decodeLegacyBlockEvent(event); err == nil { - return block, nil - } - return nil, fmt.Errorf("failed to cadence decode block [%s]: %w", event.String(), err) } @@ -94,41 +90,6 @@ func decodeBlockEvent(event cadence.Event) (*Block, error) { }, nil } -// todo remove this after updated in flow-go -type blockEventPayloadV0 struct { - Height uint64 `cadence:"height"` - Hash gethCommon.Hash `cadence:"hash"` - Timestamp uint64 `cadence:"timestamp"` - TotalSupply cadence.Int `cadence:"totalSupply"` - TotalGasUsed uint64 `cadence:"totalGasUsed"` - ParentBlockHash gethCommon.Hash `cadence:"parentHash"` - ReceiptRoot gethCommon.Hash `cadence:"receiptRoot"` - TransactionHashRoot gethCommon.Hash `cadence:"transactionHashRoot"` -} - -// DecodeBlockEventPayload decodes Cadence event into block event payload. -func decodeLegacyBlockEvent(event cadence.Event) (*Block, error) { - var block blockEventPayloadV0 - err := cadence.DecodeFields(event, &block) - if err != nil { - return nil, err - } - - h := block.Hash.String() - return &Block{ - Block: &types.Block{ - ParentBlockHash: block.ParentBlockHash, - Height: block.Height, - Timestamp: block.Timestamp, - TotalSupply: block.TotalSupply.Value, - ReceiptRoot: block.ReceiptRoot, - TransactionHashRoot: block.TransactionHashRoot, - TotalGasUsed: block.TotalGasUsed, - }, - FixedHash: &h, - }, nil -} - // blockV0 is the block format, prior to adding the PrevRandao field. type blockV0 struct { Block *blockV0Fields diff --git a/models/block_test.go b/models/block_test.go index 925cd519..c2193c13 100644 --- a/models/block_test.go +++ b/models/block_test.go @@ -150,7 +150,7 @@ func Test_DecodingLegacyBlockExecutedEvent(t *testing.T) { hashToCadenceArrayValue(block.TransactionHashRoot), }).WithType(eventType) - b, err := decodeLegacyBlockEvent(legacyEvent) + b, err := decodeBlockEvent(legacyEvent) require.NoError(t, err) require.Equal(t, block.ParentBlockHash, b.ParentBlockHash) @@ -160,10 +160,6 @@ func Test_DecodingLegacyBlockExecutedEvent(t *testing.T) { dech, err := b.Hash() require.NoError(t, err) require.Equal(t, bh, dech) - - b2, err := decodeBlockEvent(legacyEvent) - require.NoError(t, err) - require.Equal(t, b, b2) } func Test_Hash(t *testing.T) { diff --git a/models/errors/errors.go b/models/errors/errors.go index ba11350e..2c452a5e 100644 --- a/models/errors/errors.go +++ b/models/errors/errors.go @@ -19,10 +19,11 @@ var ( // General errors - ErrInternal = errors.New("internal error") - ErrInvalid = errors.New("invalid") - ErrRecoverable = errors.New("recoverable") - ErrDisconnected = NewRecoverableError(errors.New("disconnected")) + ErrInternal = errors.New("internal error") + ErrInvalid = errors.New("invalid") + ErrRecoverable = errors.New("recoverable") + ErrDisconnected = NewRecoverableError(errors.New("disconnected")) + ErrStateMismatch = errors.New("state mismatch") // Transaction errors diff --git a/models/transaction.go b/models/transaction.go index 8e19474e..f563ba80 100644 --- a/models/transaction.go +++ b/models/transaction.go @@ -203,7 +203,11 @@ func decodeTransactionEvent(event cadence.Event) (Transaction, *Receipt, error) revertReason = txEvent.ReturnedData } - receipt := NewReceipt(gethReceipt, revertReason, txEvent.PrecompiledCalls) + receipt := NewReceipt( + gethReceipt, + revertReason, + txEvent.PrecompiledCalls, + ) var tx Transaction // check if the transaction payload is actually from a direct call, diff --git a/services/ingestion/engine.go b/services/ingestion/engine.go index d795687f..2f20d929 100644 --- a/services/ingestion/engine.go +++ b/services/ingestion/engine.go @@ -165,7 +165,11 @@ func (e *Engine) processEvents(events *models.CadenceEvents) error { } batch := e.store.NewBatch() - defer batch.Close() + defer func() { + if err := batch.Close(); err != nil { + e.log.Warn().Err(err).Msg("failed to close batch") + } + }() // we first index the block err := e.indexBlock( diff --git a/services/requester/client_handler.go b/services/requester/client_handler.go new file mode 100644 index 00000000..a7183c38 --- /dev/null +++ b/services/requester/client_handler.go @@ -0,0 +1,346 @@ +package requester + +import ( + "bytes" + "context" + "errors" + "fmt" + "math/big" + "reflect" + "sync" + "time" + + "github.com/onflow/flow-go-sdk/crypto" + "github.com/onflow/go-ethereum/common" + "github.com/rs/zerolog" + + "github.com/onflow/flow-evm-gateway/config" + "github.com/onflow/flow-evm-gateway/metrics" + errs "github.com/onflow/flow-evm-gateway/models/errors" + "github.com/onflow/flow-evm-gateway/services/state" + "github.com/onflow/flow-evm-gateway/storage" + "github.com/onflow/flow-evm-gateway/storage/pebble" +) + +var _ EVMClient = &ClientHandler{} + +// ClientHandler handles remote and local client for executing EVM operations. +// The handler contains logic that can switch between using local or remote client +// and implements error handling logic that can prefer either remote result or +// local result. +type ClientHandler struct { + remote *RemoteClient + config *config.Config + store *pebble.Storage + blocks storage.BlockIndexer + receipts storage.ReceiptIndexer + logger zerolog.Logger + collector metrics.Collector +} + +func NewClientHandler( + config *config.Config, + store *pebble.Storage, + txPool *TxPool, + signer crypto.Signer, + client *CrossSporkClient, + blocks storage.BlockIndexer, + receipts storage.ReceiptIndexer, + logger zerolog.Logger, + collector metrics.Collector, +) (*ClientHandler, error) { + remote, err := NewRemote(client, config, signer, logger, blocks, txPool, collector) + if err != nil { + return nil, err + } + + return &ClientHandler{ + remote: remote, + config: config, + store: store, + blocks: blocks, + receipts: receipts, + logger: logger, + collector: collector, + }, nil +} + +func (c *ClientHandler) SendRawTransaction(ctx context.Context, data []byte) (common.Hash, error) { + // always use remote client + return c.remote.SendRawTransaction(ctx, data) +} + +func (c *ClientHandler) GetBalance( + ctx context.Context, + address common.Address, + height uint64, +) (*big.Int, error) { + local, err := c.localClient(height) + if err != nil { + return nil, err + } + + return handleCall(func() (*big.Int, error) { + return local.GetBalance(ctx, address, height) + }, func() (*big.Int, error) { + return c.remote.GetBalance(ctx, address, height) + }, c.logger.With(). + Str("client-call", "get balance"). + Str("address", address.String()). + Uint64("height", height). + Logger(), + ) +} + +func (c *ClientHandler) Call( + ctx context.Context, + data []byte, + from common.Address, + height uint64, +) ([]byte, error) { + local, err := c.localClient(height) + if err != nil { + return nil, err + } + + return handleCall(func() ([]byte, error) { + return local.Call(ctx, data, from, height) + }, func() ([]byte, error) { + return c.remote.Call(ctx, data, from, height) + }, c.logger.With(). + Str("client-call", "call"). + Str("from", from.String()). + Uint64("height", height). + Str("data", fmt.Sprintf("%x", data)). + Logger()) +} + +func (c *ClientHandler) EstimateGas( + ctx context.Context, + data []byte, + from common.Address, + height uint64, +) (uint64, error) { + local, err := c.localClient(height) + if err != nil { + return 0, err + } + + return handleCall(func() (uint64, error) { + return local.EstimateGas(ctx, data, from, height) + }, func() (uint64, error) { + return c.remote.EstimateGas(ctx, data, from, height) + }, c.logger.With(). + Str("client-call", "estimate gas"). + Str("from", from.String()). + Uint64("height", height). + Str("data", fmt.Sprintf("%x", data)). + Logger()) +} + +func (c *ClientHandler) GetNonce( + ctx context.Context, + address common.Address, + height uint64, +) (uint64, error) { + local, err := c.localClient(height) + if err != nil { + return 0, err + } + + return handleCall(func() (uint64, error) { + return local.GetNonce(ctx, address, height) + }, func() (uint64, error) { + return c.remote.GetNonce(ctx, address, height) + }, c.logger.With().Str("client-call", "get nonce"). + Str("address", address.String()). + Uint64("height", height). + Logger()) +} + +func (c *ClientHandler) GetCode( + ctx context.Context, + address common.Address, + height uint64, +) ([]byte, error) { + local, err := c.localClient(height) + if err != nil { + return nil, err + } + + return handleCall(func() ([]byte, error) { + return local.GetCode(ctx, address, height) + }, func() ([]byte, error) { + return c.remote.GetCode(ctx, address, height) + }, c.logger.With().Str("client-call", "get code"). + Str("address", address.String()). + Uint64("height", height). + Logger()) +} + +func (c *ClientHandler) GetLatestEVMHeight(ctx context.Context) (uint64, error) { + // we use the remote client to get the latest height from the network + // be careful, because this height might not yet be indexed or executed locally + // so don't use this height to then query the state, always use the latest + // executed height to query the state. + return c.remote.GetLatestEVMHeight(ctx) +} + +func (c *ClientHandler) GetStorageAt( + ctx context.Context, + address common.Address, + hash common.Hash, + height uint64, +) (common.Hash, error) { + local, err := c.localClient(height) + if err != nil { + return common.Hash{}, err + } + + return handleCall(func() (common.Hash, error) { + return local.GetStorageAt(ctx, address, hash, height) + }, func() (common.Hash, error) { + return c.remote.GetStorageAt(ctx, address, hash, height) + }, c.logger.With().Str("client-call", "get storage at").Logger()) +} + +func (c *ClientHandler) localClient(height uint64) (*LocalClient, error) { + block, err := c.blocks.GetByHeight(height) + if err != nil { + return nil, err + } + + blockState, err := state.NewBlockState( + block, + pebble.NewRegister(c.store, height, nil), + c.config.FlowNetworkID, + c.blocks, + c.receipts, + c.logger, + ) + if err != nil { + return nil, err + } + + return NewLocalClient(blockState, c.blocks), nil +} + +// handleCall takes in local and remote call and implements error handling logic to return +// correct result, it also compares the results in case there are no errors and reports any differences. +func handleCall[T any]( + local func() (T, error), + remote func() (T, error), + logger zerolog.Logger, +) (T, error) { + logger.Info().Msg("executing state client call") + + var localErr, remoteErr error + var localRes, remoteRes T + + wg := sync.WaitGroup{} + wg.Add(2) + go func() { + s := time.Now() + localRes, localErr = local() + logger.Info(). + Int64("execution-ns", time.Since(s).Nanoseconds()). + Msg("local call executed") + wg.Done() + }() + + go func() { + s := time.Now() + remoteRes, remoteErr = remote() + logger.Info(). + Int64("execution-ns", time.Since(s).Nanoseconds()). + Msg("remote call executed") + wg.Done() + }() + + wg.Wait() + + // happy case, both errs are nil and results are same + if localErr == nil && remoteErr == nil { + // if results are not same log the diff + if !isEqual(localRes, remoteRes) { + logger.Error(). + Any("local", localRes). + Any("remote", remoteRes). + Msg("results from local and remote client are not the same") + } + } + + // make sure if both return an error the errors are the same + if localErr != nil && remoteErr != nil && + localErr.Error() != remoteErr.Error() { + // if error on EN is that state is pruned or AN is rate limiting the request, + // we return the local error because it's more correct + if errors.Is(remoteErr, errs.ErrHeightOutOfRange) || errors.Is(remoteErr, errs.ErrRateLimit) { + return localRes, localErr + } + + logger.Error(). + Str("local", localErr.Error()). + Str("remote", remoteErr.Error()). + Msg("errors from local and remote client are not the same") + } + + // if remote received an error but local call worked, return the local result + // this can be due to rate-limits or pruned state on AN/EN + if localErr == nil && remoteErr != nil { + if !errors.Is(remoteErr, errs.ErrHeightOutOfRange) && !errors.Is(remoteErr, errs.ErrRateLimit) { + logger.Warn(). + Str("remote-error", remoteErr.Error()). + Any("local-result", localRes). + Msg("error from remote client but not from local client") + } + + return localRes, nil + } + + // if remote succeeded but local received an error this is a bug or in case of a + // call or gas estimation that uses cadence arch a failure is expected, because + // the local state doesn't have a way to return values for cadence arch calls because + // no transaction produced a precompiled calls input/output mock for it. + // todo find a way to possibly detect such calls and ignore errors. + if localErr != nil && remoteErr == nil { + logger.Error(). + Str("local-error", localErr.Error()). + Any("remote-result", remoteRes). + Msg("error from local client but not from remote client") + } + + return remoteRes, remoteErr +} + +// isEqual compares two values of type T, supporting *big.Int, uint64, []byte, and gethCommon.Hash +func isEqual[T any](a, b T) bool { + switch aVal := any(a).(type) { + case *big.Int: + bVal, ok := any(b).(*big.Int) + if !ok { + return false + } + return aVal.Cmp(bVal) == 0 + case uint64: + bVal, ok := any(b).(uint64) + if !ok { + return false + } + return aVal == bVal + case []byte: + bVal, ok := any(b).([]byte) + if !ok { + return false + } + return bytes.Equal(aVal, bVal) + case common.Hash: + bVal, ok := any(b).(common.Hash) + if !ok { + return false + } + return aVal.Cmp(bVal) == 0 + default: + return reflect.DeepEqual(a, b) + } +} diff --git a/services/requester/client_handler_test.go b/services/requester/client_handler_test.go new file mode 100644 index 00000000..b22d5285 --- /dev/null +++ b/services/requester/client_handler_test.go @@ -0,0 +1,250 @@ +package requester + +import ( + "bytes" + "errors" + "testing" + "time" + + "github.com/rs/zerolog" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// Test data structures +type TestData struct { + Field1 int + Field2 string +} + +type TestStruct struct { + Value int +} + +// Helper function to check if log output contains a specific message +func containsLogMessage(logOutput, message string) bool { + return bytes.Contains([]byte(logOutput), []byte(message)) +} + +func TestHandleCall_BothSuccess_SameResult(t *testing.T) { + local := func() (int, error) { + time.Sleep(10 * time.Millisecond) + return 42, nil + } + + remote := func() (int, error) { + time.Sleep(20 * time.Millisecond) + return 42, nil + } + + var buf bytes.Buffer + logger := zerolog.New(&buf).With().Timestamp().Logger() + + result, err := handleCall(local, remote, logger) + require.NoError(t, err) + assert.Equal(t, 42, result) + + logOutput := buf.String() + assert.NotContains(t, logOutput, "error") +} + +func TestHandleCall_BothSuccess_DifferentResult(t *testing.T) { + local := func() (int, error) { + time.Sleep(10 * time.Millisecond) + return 42, nil + } + + remote := func() (int, error) { + time.Sleep(20 * time.Millisecond) + return 43, nil + } + + var buf bytes.Buffer + logger := zerolog.New(&buf).With().Timestamp().Logger() + + result, err := handleCall(local, remote, logger) + require.NoError(t, err) + assert.Equal(t, 43, result) + + logOutput := buf.String() + assert.Contains(t, logOutput, "results from local and remote client are not the same") +} + +func TestHandleCall_LocalSuccess_RemoteFail(t *testing.T) { + local := func() (int, error) { + time.Sleep(10 * time.Millisecond) + return 42, nil + } + + remote := func() (int, error) { + time.Sleep(20 * time.Millisecond) + return 0, errors.New("remote error") + } + + var buf bytes.Buffer + logger := zerolog.New(&buf).With().Timestamp().Logger() + + result, err := handleCall(local, remote, logger) + require.NoError(t, err) + assert.Equal(t, 42, result) + + logOutput := buf.String() + assert.Contains(t, logOutput, "error from remote client but not from local client") +} + +func TestHandleCall_LocalFail_RemoteSuccess(t *testing.T) { + local := func() (int, error) { + time.Sleep(10 * time.Millisecond) + return 0, errors.New("local error") + } + + remote := func() (int, error) { + time.Sleep(20 * time.Millisecond) + return 43, nil + } + + var buf bytes.Buffer + logger := zerolog.New(&buf).With().Timestamp().Logger() + + result, err := handleCall(local, remote, logger) + require.NoError(t, err) + assert.Equal(t, 43, result) + + logOutput := buf.String() + assert.Contains(t, logOutput, "error from local client but not from remote client") +} + +func TestHandleCall_BothFail_SameError(t *testing.T) { + local := func() (int, error) { + time.Sleep(10 * time.Millisecond) + return 0, errors.New("common error") + } + + remote := func() (int, error) { + time.Sleep(20 * time.Millisecond) + return 0, errors.New("common error") + } + + var buf bytes.Buffer + logger := zerolog.New(&buf).With().Timestamp().Logger() + + _, err := handleCall(local, remote, logger) + require.Error(t, err) + assert.Equal(t, "common error", err.Error()) + + logOutput := buf.String() + assert.NotContains(t, logOutput, "errors from local and remote client are not the same") +} + +func TestHandleCall_BothFail_DifferentErrors(t *testing.T) { + local := func() (int, error) { + time.Sleep(10 * time.Millisecond) + return 0, errors.New("local error") + } + + remote := func() (int, error) { + time.Sleep(20 * time.Millisecond) + return 0, errors.New("remote error") + } + + var buf bytes.Buffer + logger := zerolog.New(&buf).With().Timestamp().Logger() + + _, err := handleCall(local, remote, logger) + require.Error(t, err) + assert.Equal(t, "remote error", err.Error()) + + logOutput := buf.String() + assert.Contains(t, logOutput, "errors from local and remote client are not the same") +} + +func TestHandleCall_StructType_BothSuccess_SameResult(t *testing.T) { + local := func() (TestData, error) { + time.Sleep(10 * time.Millisecond) + return TestData{Field1: 1, Field2: "test"}, nil + } + + remote := func() (TestData, error) { + time.Sleep(20 * time.Millisecond) + return TestData{Field1: 1, Field2: "test"}, nil + } + + var buf bytes.Buffer + logger := zerolog.New(&buf).With().Timestamp().Logger() + + result, err := handleCall(local, remote, logger) + require.NoError(t, err) + expected := TestData{Field1: 1, Field2: "test"} + assert.Equal(t, expected, result) + + logOutput := buf.String() + assert.NotContains(t, logOutput, "error") +} + +func TestHandleCall_StructType_BothSuccess_DifferentResult(t *testing.T) { + local := func() (TestData, error) { + time.Sleep(10 * time.Millisecond) + return TestData{Field1: 1, Field2: "test"}, nil + } + + remote := func() (TestData, error) { + time.Sleep(20 * time.Millisecond) + return TestData{Field1: 2, Field2: "test"}, nil + } + + var buf bytes.Buffer + logger := zerolog.New(&buf).With().Timestamp().Logger() + + result, err := handleCall(local, remote, logger) + require.NoError(t, err) + expected := TestData{Field1: 2, Field2: "test"} + assert.Equal(t, expected, result) + + logOutput := buf.String() + assert.Contains(t, logOutput, "results from local and remote client are not the same") +} + +func TestHandleCall_PointerType_LocalNil_RemoteNonNil(t *testing.T) { + local := func() (*TestStruct, error) { + time.Sleep(10 * time.Millisecond) + return nil, nil + } + + remote := func() (*TestStruct, error) { + time.Sleep(20 * time.Millisecond) + return &TestStruct{Value: 1}, nil + } + + var buf bytes.Buffer + logger := zerolog.New(&buf).With().Timestamp().Logger() + + result, err := handleCall(local, remote, logger) + require.NoError(t, err) + require.NotNil(t, result) + assert.Equal(t, 1, result.Value) + + logOutput := buf.String() + assert.Contains(t, logOutput, "results from local and remote client are not the same") +} + +func TestHandleCall_PointerType_LocalNonNil_RemoteNil(t *testing.T) { + local := func() (*TestStruct, error) { + time.Sleep(10 * time.Millisecond) + return &TestStruct{Value: 1}, nil + } + + remote := func() (*TestStruct, error) { + time.Sleep(20 * time.Millisecond) + return nil, nil + } + + var buf bytes.Buffer + logger := zerolog.New(&buf).With().Timestamp().Logger() + + result, err := handleCall(local, remote, logger) + require.NoError(t, err) + require.Nil(t, result) + + logOutput := buf.String() + assert.Contains(t, logOutput, "results from local and remote client are not the same") +} diff --git a/services/requester/evm.go b/services/requester/evm.go new file mode 100644 index 00000000..1f169000 --- /dev/null +++ b/services/requester/evm.go @@ -0,0 +1,41 @@ +package requester + +import ( + "context" + "math/big" + + "github.com/onflow/go-ethereum/common" +) + +type EVMClient interface { + // SendRawTransaction will submit signed transaction data to the network. + // The submitted EVM transaction hash is returned. + SendRawTransaction(ctx context.Context, data []byte) (common.Hash, error) + + // GetBalance returns the amount of wei for the given address in the state of the + // given EVM block height. + GetBalance(ctx context.Context, address common.Address, height uint64) (*big.Int, error) + + // 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, height uint64) ([]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, height uint64) (uint64, error) + + // GetNonce gets nonce from the network at the given EVM block height. + GetNonce(ctx context.Context, address common.Address, height uint64) (uint64, error) + + // GetCode returns the code stored at the given address in + // the state for the given EVM block height. + GetCode(ctx context.Context, address common.Address, height uint64) ([]byte, error) + + // GetLatestEVMHeight returns the latest EVM height of the network. + GetLatestEVMHeight(ctx context.Context) (uint64, error) + + // GetStorageAt returns the storage from the state at the given address, key and block number. + GetStorageAt(ctx context.Context, address common.Address, hash common.Hash, height uint64) (common.Hash, error) +} diff --git a/services/requester/local_client.go b/services/requester/local_client.go new file mode 100644 index 00000000..5ed2ca39 --- /dev/null +++ b/services/requester/local_client.go @@ -0,0 +1,99 @@ +package requester + +import ( + "context" + "fmt" + "math/big" + + evmTypes "github.com/onflow/flow-go/fvm/evm/types" + "github.com/onflow/go-ethereum/common" + + errs "github.com/onflow/flow-evm-gateway/models/errors" + "github.com/onflow/flow-evm-gateway/services/state" + "github.com/onflow/flow-evm-gateway/storage" +) + +var _ EVMClient = &LocalClient{} + +func NewLocalClient(state *state.BlockState, blocks storage.BlockIndexer) *LocalClient { + return &LocalClient{ + state: state, + blocks: blocks, + } +} + +// LocalClient preforms read-only queries on the local state. +// The client is created with the state instance which is initialized using a +// evm height so all the methods that take evm height as parameter can ignore it +// since the state is already initialized with it. +type LocalClient struct { + state *state.BlockState + blocks storage.BlockIndexer +} + +func (l *LocalClient) SendRawTransaction( + ctx context.Context, + data []byte, +) (common.Hash, error) { + return common.Hash{}, fmt.Errorf("local client is read-only") +} + +func (l *LocalClient) GetBalance(ctx context.Context, address common.Address, evmHeight uint64) (*big.Int, error) { + bal := l.state.GetBalance(address) + return bal.ToBig(), nil +} + +func (l *LocalClient) Call(ctx context.Context, data []byte, from common.Address, evmHeight uint64) ([]byte, error) { + res, err := l.state.Call(from, data) + if err != nil { + return nil, err + } + + result := res.ResultSummary() + if result.ErrorCode != 0 { + if result.ErrorCode == evmTypes.ExecutionErrCodeExecutionReverted { + return nil, errs.NewRevertError(result.ReturnedData) + } + return nil, errs.NewFailedTransactionError(result.ErrorMessage) + } + + // make sure the nil returned data is returned as empty slice to match remote client + if res.ReturnedData == nil { + res.ReturnedData = make([]byte, 0) + } + + return res.ReturnedData, nil +} + +func (l *LocalClient) EstimateGas(ctx context.Context, data []byte, from common.Address, evmHeight uint64) (uint64, error) { + res, err := l.state.Call(from, data) + if err != nil { + return 0, err + } + + result := res.ResultSummary() + if result.ErrorCode != 0 { + if result.ErrorCode == evmTypes.ExecutionErrCodeExecutionReverted { + return 0, errs.NewRevertError(result.ReturnedData) + } + return 0, errs.NewFailedTransactionError(result.ErrorMessage) + } + + return res.GasConsumed, nil +} + +func (l *LocalClient) GetNonce(ctx context.Context, address common.Address, evmHeight uint64) (uint64, error) { + return l.state.GetNonce(address), nil +} + +func (l *LocalClient) GetCode(ctx context.Context, address common.Address, height uint64) ([]byte, error) { + return l.state.GetCode(address), nil +} + +func (l *LocalClient) GetStorageAt(ctx context.Context, address common.Address, hash common.Hash, evmHeight uint64) (common.Hash, error) { + return l.state.GetState(address, hash), nil +} + +func (l *LocalClient) GetLatestEVMHeight(ctx context.Context) (uint64, error) { + return 0, fmt.Errorf("should not be called on the local state") +} diff --git a/services/requester/requester.go b/services/requester/remote_client.go similarity index 79% rename from services/requester/requester.go rename to services/requester/remote_client.go index b0431bf3..c74fd2aa 100644 --- a/services/requester/requester.go +++ b/services/requester/remote_client.go @@ -82,42 +82,9 @@ const coaFundingBalance = minFlowBalance - 1 const LatestBlockHeight uint64 = math.MaxUint64 - 1 -type Requester interface { - // SendRawTransaction will submit signed transaction data to the network. - // The submitted EVM transaction hash is returned. - SendRawTransaction(ctx context.Context, data []byte) (common.Hash, error) +var _ EVMClient = &RemoteClient{} - // GetBalance returns the amount of wei for the given address in the state of the - // given EVM block height. - GetBalance(ctx context.Context, address common.Address, evmHeight int64) (*big.Int, error) - - // 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) - - // 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) - - // GetNonce gets nonce from the network at the given EVM block height. - GetNonce(ctx context.Context, address common.Address, evmHeight int64) (uint64, error) - - // GetCode returns the code stored at the given address in - // the state for the given EVM block height. - GetCode(ctx context.Context, address common.Address, evmHeight int64) ([]byte, error) - - // GetLatestEVMHeight returns the latest EVM height of the network. - GetLatestEVMHeight(ctx context.Context) (uint64, error) - - // GetStorageAt returns the storage from the state at the given address, key and block number. - GetStorageAt(ctx context.Context, address common.Address, hash common.Hash, evmHeight int64) (common.Hash, error) -} - -var _ Requester = &EVM{} - -type EVM struct { +type RemoteClient struct { client *CrossSporkClient config *config.Config signer crypto.Signer @@ -134,7 +101,7 @@ type EVM struct { collector metrics.Collector } -func NewEVM( +func NewRemote( client *CrossSporkClient, config *config.Config, signer crypto.Signer, @@ -142,7 +109,7 @@ func NewEVM( blocks storage.BlockIndexer, txPool *TxPool, collector metrics.Collector, -) (*EVM, error) { +) (*RemoteClient, error) { logger = logger.With().Str("component", "requester").Logger() // check that the address stores already created COA resource in the "evm" storage path. // if it doesn't check if the auto-creation boolean is true and if so create it @@ -192,7 +159,7 @@ func NewEVM( cache = expirable.NewLRU[string, cadence.Value](int(config.CacheSize), nil, time.Second) } - evm := &EVM{ + evm := &RemoteClient{ client: client, config: config, signer: signer, @@ -226,7 +193,7 @@ func NewEVM( return evm, nil } -func (e *EVM) SendRawTransaction(ctx context.Context, data []byte) (common.Hash, error) { +func (e *RemoteClient) SendRawTransaction(ctx context.Context, data []byte) (common.Hash, error) { tx := &types.Transaction{} if err := tx.UnmarshalBinary(data); err != nil { return common.Hash{}, err @@ -285,7 +252,7 @@ func (e *EVM) SendRawTransaction(ctx context.Context, data []byte) (common.Hash, // buildTransaction creates a flow transaction from the provided script with the arguments // and signs it with the configured COA account. -func (e *EVM) buildTransaction(ctx context.Context, script []byte, args ...cadence.Value) (*flow.Transaction, error) { +func (e *RemoteClient) buildTransaction(ctx context.Context, script []byte, args ...cadence.Value) (*flow.Transaction, error) { // building and signing transactions should be blocking, so we don't have keys conflict e.mux.Lock() defer e.mux.Unlock() @@ -335,11 +302,7 @@ func (e *EVM) buildTransaction(ctx context.Context, script []byte, args ...caden return flowTx, nil } -func (e *EVM) GetBalance( - ctx context.Context, - address common.Address, - evmHeight int64, -) (*big.Int, error) { +func (e *RemoteClient) GetBalance(ctx context.Context, address common.Address, evmHeight uint64) (*big.Int, error) { hexEncodedAddress, err := addressToCadenceString(address) if err != nil { return nil, err @@ -361,7 +324,7 @@ func (e *EVM) GetBalance( e.logger.Error(). Err(err). Str("address", address.String()). - Int64("evm-height", evmHeight). + Uint64("evm-height", evmHeight). Uint64("cadence-height", height). Msg("failed to get get balance") } @@ -381,11 +344,7 @@ func (e *EVM) GetBalance( return val.(cadence.UInt).Big(), nil } -func (e *EVM) GetNonce( - ctx context.Context, - address common.Address, - evmHeight int64, -) (uint64, error) { +func (e *RemoteClient) GetNonce(ctx context.Context, address common.Address, evmHeight uint64) (uint64, error) { hexEncodedAddress, err := addressToCadenceString(address) if err != nil { return 0, err @@ -406,7 +365,7 @@ func (e *EVM) GetNonce( if !errors.Is(err, errs.ErrHeightOutOfRange) { e.logger.Error().Err(err). Str("address", address.String()). - Int64("evm-height", evmHeight). + Uint64("evm-height", evmHeight). Uint64("cadence-height", height). Msg("failed to get nonce") } @@ -427,14 +386,14 @@ func (e *EVM) GetNonce( e.logger.Debug(). Uint64("nonce", nonce). - Int64("evm-height", evmHeight). + Uint64("evm-height", evmHeight). Uint64("cadence-height", height). Msg("get nonce executed") return nonce, nil } -func (e *EVM) stateAt(evmHeight int64) (*state.StateDB, error) { +func (e *RemoteClient) stateAt(evmHeight uint64) (*state.StateDB, error) { cadenceHeight, err := e.evmToCadenceHeight(evmHeight) if err != nil { return nil, err @@ -461,12 +420,7 @@ func (e *EVM) stateAt(evmHeight int64) (*state.StateDB, error) { return state.NewStateDB(ledger, storageAddress) } -func (e *EVM) GetStorageAt( - ctx context.Context, - address common.Address, - hash common.Hash, - evmHeight int64, -) (common.Hash, error) { +func (e *RemoteClient) GetStorageAt(ctx context.Context, address common.Address, hash common.Hash, evmHeight uint64) (common.Hash, error) { stateDB, err := e.stateAt(evmHeight) if err != nil { return common.Hash{}, err @@ -476,12 +430,7 @@ func (e *EVM) GetStorageAt( return result, stateDB.Error() } -func (e *EVM) Call( - ctx context.Context, - data []byte, - from common.Address, - evmHeight int64, -) ([]byte, error) { +func (e *RemoteClient) Call(ctx context.Context, data []byte, from common.Address, evmHeight uint64) ([]byte, error) { hexEncodedTx, err := cadence.NewString(hex.EncodeToString(data)) if err != nil { return nil, err @@ -508,7 +457,7 @@ func (e *EVM) Call( e.logger.Error(). Err(err). Uint64("cadence-height", height). - Int64("evm-height", evmHeight). + Uint64("evm-height", evmHeight). Str("from", from.String()). Str("data", hex.EncodeToString(data)). Msg("failed to execute call") @@ -525,18 +474,18 @@ func (e *EVM) Call( e.logger.Debug(). Str("result", hex.EncodeToString(result)). - Int64("evm-height", evmHeight). + Uint64("evm-height", evmHeight). Uint64("cadence-height", height). Msg("call executed") return result, nil } -func (e *EVM) EstimateGas( +func (e *RemoteClient) EstimateGas( ctx context.Context, data []byte, from common.Address, - evmHeight int64, + evmHeight uint64, ) (uint64, error) { hexEncodedTx, err := cadence.NewString(hex.EncodeToString(data)) if err != nil { @@ -564,7 +513,7 @@ func (e *EVM) EstimateGas( e.logger.Error(). Err(err). Uint64("cadence-height", height). - Int64("evm-height", evmHeight). + Uint64("evm-height", evmHeight). Str("from", from.String()). Str("data", hex.EncodeToString(data)). Msg("failed to execute estimateGas") @@ -581,24 +530,20 @@ func (e *EVM) EstimateGas( e.logger.Debug(). Uint64("gas", gasConsumed). - Int64("evm-height", evmHeight). + Uint64("evm-height", evmHeight). Uint64("cadence-height", height). Msg("estimateGas executed") return gasConsumed, nil } -func (e *EVM) GetCode( - ctx context.Context, - address common.Address, - evmHeight int64, -) ([]byte, error) { +func (e *RemoteClient) GetCode(ctx context.Context, address common.Address, height uint64) ([]byte, error) { hexEncodedAddress, err := addressToCadenceString(address) if err != nil { return nil, err } - height, err := e.evmToCadenceHeight(evmHeight) + cadenceHeight, err := e.evmToCadenceHeight(height) if err != nil { return nil, err } @@ -606,15 +551,15 @@ func (e *EVM) GetCode( value, err := e.executeScriptAtHeight( ctx, getCode, - height, + cadenceHeight, []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). + Uint64("cadence-height", cadenceHeight). + Uint64("evm-height", height). Str("address", address.String()). Msg("failed to get code") } @@ -622,7 +567,7 @@ func (e *EVM) GetCode( return nil, fmt.Errorf( "failed to execute script for get code of address: %s at height: %d, with: %w", address, - height, + cadenceHeight, err, ) } @@ -634,15 +579,15 @@ func (e *EVM) GetCode( e.logger.Debug(). Str("address", address.Hex()). - Int64("evm-height", evmHeight). - Uint64("cadence-height", height). + Uint64("evm-height", height). + Uint64("cadence-height", cadenceHeight). Str("code size", fmt.Sprintf("%d", len(code))). Msg("get code executed") return code, nil } -func (e *EVM) GetLatestEVMHeight(ctx context.Context) (uint64, error) { +func (e *RemoteClient) GetLatestEVMHeight(ctx context.Context) (uint64, error) { val, err := e.executeScriptAtHeight( ctx, getLatest, @@ -668,7 +613,7 @@ func (e *EVM) GetLatestEVMHeight(ctx context.Context) (uint64, error) { } // getSignerNetworkInfo loads the signer account from network and returns key index and sequence number -func (e *EVM) getSignerNetworkInfo(ctx context.Context) (uint32, uint64, error) { +func (e *RemoteClient) getSignerNetworkInfo(ctx context.Context) (uint32, uint64, error) { account, err := e.client.GetAccount(ctx, e.config.COAAddress) if err != nil { return 0, 0, fmt.Errorf( @@ -695,7 +640,7 @@ func (e *EVM) getSignerNetworkInfo(ctx context.Context) (uint32, uint64, error) } // replaceAddresses replace the addresses based on the network -func (e *EVM) replaceAddresses(script []byte) []byte { +func (e *RemoteClient) replaceAddresses(script []byte) []byte { // make the list of all contracts we should replace address for sc := systemcontracts.SystemContractsForChain(e.config.FlowNetworkID) contracts := []systemcontracts.SystemContract{sc.EVMContract, sc.FungibleToken, sc.FlowToken} @@ -715,32 +660,10 @@ 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() +func (e *RemoteClient) evmToCadenceHeight(height uint64) (uint64, error) { + cadenceHeight, err := e.blocks.GetCadenceHeight(height) 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 0, fmt.Errorf("failed to map evm height: %d to cadence height: %w", height, err) } return cadenceHeight, nil @@ -750,7 +673,7 @@ func (e *EVM) evmToCadenceHeight(height int64) (uint64, error) { // block height, with the given arguments. A height of `LatestBlockHeight` // (math.MaxUint64 - 1) is a special value, which means the script will be // executed at the latest sealed block. -func (e *EVM) executeScriptAtHeight( +func (e *RemoteClient) executeScriptAtHeight( ctx context.Context, scriptType scriptType, height uint64, @@ -793,12 +716,7 @@ func (e *EVM) executeScriptAtHeight( ) } if err != nil { - // if snapshot doesn't exist on EN, the height at which script was executed is out - // of the boundaries the EN keeps state, so return out of range - const storageError = "failed to create storage snapshot" - if strings.Contains(err.Error(), storageError) { - return nil, errs.NewHeightOutOfRangeError(height) - } + return nil, parseError(err, height) } else if key != "" && e.scriptCache != nil { // if error is nil and key is supported add to cache e.scriptCache.Add(key, res) } @@ -822,6 +740,10 @@ func cadenceStringToBytes(value cadence.Value) ([]byte, error) { ) } + if cdcString == "" { + return nil, nil + } + 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) @@ -830,7 +752,8 @@ func cadenceStringToBytes(value cadence.Value) ([]byte, error) { return code, nil } -// parseResult +// parseResult will check if the error code is present, which means there was an actual error during execution, +// the error is then returned as a typed error instead. func parseResult(res cadence.Value) (*evmTypes.ResultSummary, error) { result, err := evmImpl.ResultSummaryFromEVMResultValue(res) if err != nil { @@ -847,6 +770,22 @@ func parseResult(res cadence.Value) (*evmTypes.ResultSummary, error) { return result, err } +func parseError(err error, height uint64) error { + // if snapshot doesn't exist on EN, the height at which script was executed is out + // of the boundaries the EN keeps state, so return out of range + const storageError = "failed to create storage snapshot" + if strings.Contains(err.Error(), storageError) { + return errs.NewHeightOutOfRangeError(height) + } + // the AN rate-limited the request + const rateLimitError = "ResourceExhausted" + if strings.Contains(err.Error(), rateLimitError) { + return errs.ErrRateLimit + } + + return err +} + // cacheKey builds the cache key from the script type, height and arguments. func cacheKey(scriptType scriptType, height uint64, args []cadence.Value) string { key := fmt.Sprintf("%d%d", scriptType, height) diff --git a/services/requester/requester_test.go b/services/requester/remote_client_test.go similarity index 98% rename from services/requester/requester_test.go rename to services/requester/remote_client_test.go index f40ab864..fe8c1fd8 100644 --- a/services/requester/requester_test.go +++ b/services/requester/remote_client_test.go @@ -210,14 +210,14 @@ func Test_CacheKey(t *testing.T) { } -func createEVM(t *testing.T, cache *expirable.LRU[string, cadence.Value], mockClient *mocks.Client) *EVM { +func createEVM(t *testing.T, cache *expirable.LRU[string, cadence.Value], mockClient *mocks.Client) *RemoteClient { networkID := flowGo.Emulator log := zerolog.New(zerolog.NewTestWriter(t)) client, err := NewCrossSporkClient(mockClient, nil, log, networkID) require.NoError(t, err) - return &EVM{ + return &RemoteClient{ client: client, logger: log, scriptCache: cache, diff --git a/services/requester/remote_state.go b/services/requester/remote_ledger.go similarity index 100% rename from services/requester/remote_state.go rename to services/requester/remote_ledger.go diff --git a/services/requester/remote_state_test.go b/services/requester/remote_ledger_test.go similarity index 100% rename from services/requester/remote_state_test.go rename to services/requester/remote_ledger_test.go diff --git a/services/state/engine.go b/services/state/engine.go new file mode 100644 index 00000000..c3868258 --- /dev/null +++ b/services/state/engine.go @@ -0,0 +1,241 @@ +package state + +import ( + "context" + "errors" + "fmt" + + pebbleDB "github.com/cockroachdb/pebble" + "github.com/google/uuid" + "github.com/onflow/atree" + "github.com/onflow/flow/protobuf/go/flow/executiondata" + gethTypes "github.com/onflow/go-ethereum/core/types" + "github.com/onflow/go-ethereum/trie" + "github.com/rs/zerolog" + + "github.com/onflow/flow-evm-gateway/config" + "github.com/onflow/flow-evm-gateway/models" + errs "github.com/onflow/flow-evm-gateway/models/errors" + "github.com/onflow/flow-evm-gateway/storage" + "github.com/onflow/flow-evm-gateway/storage/pebble" +) + +var _ models.Engine = &Engine{} +var _ models.Subscriber = &Engine{} + +// Engine state engine takes care of creating a local state by +// re-executing each block against the local emulator and local +// register index. +// The engine relies on the block publisher to receive new +// block events which is done by the event ingestion engine. +// It also relies on the event ingestion engine to wait for the +// state engine to be ready before subscribing, because on startup +// we have to do a sync between last indexed and last executed block +// during which time we should not receive any other block events. +type Engine struct { + config *config.Config + execution executiondata.ExecutionDataAPIClient + logger zerolog.Logger + status *models.EngineStatus + blockPublisher *models.Publisher + store *pebble.Storage + blocks storage.BlockIndexer + transactions storage.TransactionIndexer + receipts storage.ReceiptIndexer +} + +func NewStateEngine( + config *config.Config, + execution executiondata.ExecutionDataAPIClient, + blockPublisher *models.Publisher, + store *pebble.Storage, + blocks storage.BlockIndexer, + transactions storage.TransactionIndexer, + receipts storage.ReceiptIndexer, + logger zerolog.Logger, +) *Engine { + log := logger.With().Str("component", "state").Logger() + + return &Engine{ + config: config, + execution: execution, + logger: log, + store: store, + status: models.NewEngineStatus(), + blockPublisher: blockPublisher, + blocks: blocks, + transactions: transactions, + receipts: receipts, + } +} + +// Notify will get new events for blocks from the blocks publisher, +// which is being produced by the event ingestion engine. +func (e *Engine) Notify(data any) { + block, ok := data.(*models.Block) + if !ok { + e.logger.Error().Msg("invalid event type sent to state ingestion") + return + } + + l := e.logger.With().Uint64("evm-height", block.Height).Logger() + l.Info().Msg("received new block") + + if err := e.executeBlock(block); err != nil { + panic(fmt.Errorf("failed to execute block at height %d: %w", block.Height, err)) + } + + l.Info().Msg("successfully executed block") +} + +func (e *Engine) Run(ctx context.Context) error { + // check if we need to execute any blocks that were indexed but not executed + // this could happen if after index but before execution the node crashes + indexed, err := e.blocks.LatestIndexedHeight() + if err != nil { + return err + } + + executed, err := e.blocks.LatestExecutedHeight() + if err != nil { + return err + } + + if executed < indexed { + e.logger.Info(). + Uint64("last-executed", executed). + Uint64("last-indexed", indexed). + Msg("syncing executed blocks on startup") + + for i := executed; i <= indexed; i++ { + block, err := e.blocks.GetByHeight(i) + if err != nil { + return err + } + + if err := e.executeBlock(block); err != nil { + return err + } + } + } + + // after all is up to sync we subscribe to live blocks + e.blockPublisher.Subscribe(e) + e.status.MarkReady() + return nil +} + +func (e *Engine) Stop() { + // todo cleanup + e.status.MarkStopped() +} + +func (e *Engine) Done() <-chan struct{} { + return e.status.IsDone() +} + +func (e *Engine) Ready() <-chan struct{} { + return e.status.IsReady() +} + +func (e *Engine) Error() <-chan error { + return nil +} + +func (e *Engine) ID() uuid.UUID { + return uuid.New() +} + +// executeBlock will execute all transactions in the provided block. +// If a transaction fails to execute or the result doesn't match expected +// result return an error. +// Transaction executed should match a receipt we have indexed from the network +// produced by execution nodes. This check makes sure we keep a correct state. +func (e *Engine) executeBlock(block *models.Block) error { + // start a new database batch + batch := e.store.NewBatch() + defer func() { + if err := batch.Close(); err != nil { + e.logger.Warn().Err(err).Msg("failed to close batch") + } + }() + + var registers atree.Ledger + registers = pebble.NewRegister(e.store, block.Height, batch) + + // if validation is enabled wrap the register ledger into a validator + if e.config.ValidateRegisters { + registers = NewRegisterValidator(registers, e.execution) + } + + state, err := NewBlockState(block, registers, e.config.FlowNetworkID, e.blocks, e.receipts, e.logger) + if err != nil { + return err + } + + receipts := make(gethTypes.Receipts, len(block.TransactionHashes)) + + for i, h := range block.TransactionHashes { + tx, err := e.transactions.Get(h) + if err != nil { + return err + } + + receipt, err := state.Execute(tx) + if err != nil { + return fmt.Errorf("failed to execute tx %s: %w", h, err) + } + receipts[i] = receipt + } + + executedRoot := gethTypes.DeriveSha(receipts, trie.NewStackTrie(nil)) + // make sure receipt root matches, so we know all the execution results are same + if executedRoot.Cmp(block.ReceiptRoot) != 0 { + return errs.ErrStateMismatch + } + + if e.config.ValidateRegisters { + if err := e.validateBlock(registers, block); err != nil { + return err + } + } + + if err := e.blocks.SetExecutedHeight(block.Height); err != nil { + return err + } + + if err := batch.Commit(pebbleDB.Sync); err != nil { + return fmt.Errorf("failed to commit executed data for block %d: %w", block.Height, err) + } + + return nil +} + +// validateBlock validates the block updated registers using the register validator. +// If there's any register mismatch it returns an error. +// +// todo remove: +// Currently, this is done synchronous but could be improved in the future, however this register +// validation using the AN APIs will be completely replaced with the state commitment checksum once +// the work is done on core: https://github.com/onflow/flow-go/pull/6451 +func (e *Engine) validateBlock(registers atree.Ledger, block *models.Block) error { + validator, ok := registers.(*RegisterValidator) + if !ok { + return fmt.Errorf("invalid register validator used") + } + + cadenceHeight, err := e.blocks.GetCadenceHeight(block.Height) + if err != nil { + e.logger.Error().Err(err).Msg("register validation failed, block cadence height") + } + + if err := validator.ValidateBlock(cadenceHeight); err != nil { + if errors.Is(err, errs.ErrStateMismatch) { + return err + } + // if there were issues with the client request only log the error + e.logger.Error().Err(err).Msg("register validation failed") + } + + return nil +} diff --git a/services/state/state.go b/services/state/state.go new file mode 100644 index 00000000..cec4056e --- /dev/null +++ b/services/state/state.go @@ -0,0 +1,191 @@ +package state + +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/rs/zerolog" + + "github.com/onflow/flow-evm-gateway/models" + "github.com/onflow/flow-evm-gateway/storage" +) + +// todo reuse the error from flow-go +// cadenceArchUnexpectedCall is returned by cadence arch +// var cadenceArchUnexpectedCall = fmt.Errorf("unexpected call") + +type BlockState struct { + types.StateDB // todo change to types.ReadOnlyView + emulator types.Emulator + chainID flowGo.ChainID + block *models.Block + txIndex uint + gasUsed uint64 + blocks storage.BlockIndexer + receipts storage.ReceiptIndexer + logger zerolog.Logger +} + +func NewBlockState( + block *models.Block, + registers atree.Ledger, + chainID flowGo.ChainID, + blocks storage.BlockIndexer, + receipts storage.ReceiptIndexer, + logger zerolog.Logger, +) (*BlockState, error) { + logger = logger.With().Str("component", "state-execution").Logger() + storageAddress := evm.StorageAccountAddress(chainID) + + stateDB, err := state.NewStateDB(registers, storageAddress) + if err != nil { + return nil, err + } + + return &BlockState{ + emulator: emulator.NewEmulator(registers, storageAddress), + StateDB: stateDB, + chainID: chainID, + block: block, + blocks: blocks, + receipts: receipts, + logger: logger, + }, nil +} + +func (s *BlockState) Execute(tx models.Transaction) (*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) + 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 { + // todo is this ok, the service would restart and retry? + 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 +} + +func (s *BlockState) Call(from common.Address, data []byte) (*types.Result, error) { + // todo + // executing a transaction for a call or estimate gas that uses + // cadence arch calls (https://github.com/onflow/flow-go/blob/master/fvm/evm/precompiles/arch.go) + // will fail, because there was no receipt made by EN that would contain input and output + // data we use to mock the calls to cadence arch using the replayer in the block context. + // This defer handles such a panic gracefully, so we can return the response from remote client instead. + //defer func() { + // if r := recover(); r != nil { + // if err, ok := r.(error); ok { + // if errors.Is(err, cadenceArchUnexpectedCall) { + // + // } + // } + // } + //}() + + ctx, err := s.blockContext(nil) + if err != nil { + return nil, err + } + + bv, err := s.emulator.NewBlockView(ctx) + if err != nil { + return nil, err + } + + tx := &gethTypes.Transaction{} + if err := tx.UnmarshalBinary(data); err != nil { + return nil, err + } + + return bv.DryRunTransaction(tx, from) +} + +// 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 *BlockState) 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, // todo check + DirectCallGasPrice: types.DefaultDirectCallGasPrice, + GasFeeCollector: types.CoinbaseAddress, + GetHashFunc: func(n uint64) common.Hash { + b, err := s.blocks.GetByHeight(n) + if err != nil { + panic(err) + } + h, err := b.Hash() + if err != nil { + panic(err) + } + + return h + }, + Random: s.block.PrevRandao, + TxCountSoFar: s.txIndex, + TotalGasUsedSoFar: s.gasUsed, + // todo what to do with the tracer + 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 + 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/state/validator.go b/services/state/validator.go new file mode 100644 index 00000000..c4984ffb --- /dev/null +++ b/services/state/validator.go @@ -0,0 +1,103 @@ +package state + +import ( + "bytes" + "context" + "fmt" + + "github.com/onflow/atree" + "github.com/onflow/flow-go/engine/common/rpc/convert" + "github.com/onflow/flow-go/model/flow" + "github.com/onflow/flow/protobuf/go/flow/entities" + "github.com/onflow/flow/protobuf/go/flow/executiondata" + "golang.org/x/exp/maps" + + errs "github.com/onflow/flow-evm-gateway/models/errors" +) + +// todo we should introduce a new state of the block, indexed, executed and validate +// after this validator does the validation + +var _ atree.Ledger = &RegisterValidator{} + +// RegisterValidator keeps track of all set register during execution and is checked +// once the block is executed. +type RegisterValidator struct { + atree.Ledger + execution executiondata.ExecutionDataAPIClient + updates map[flow.RegisterID][]byte +} + +// NewRegisterValidator will create a new register validator. The register validator +// should only be used once for atree.Ledger it wraps for a specific block height. +// After we must call ValidateBlock only once. +func NewRegisterValidator( + register atree.Ledger, + execution executiondata.ExecutionDataAPIClient, +) *RegisterValidator { + return &RegisterValidator{ + Ledger: register, + execution: execution, + updates: make(map[flow.RegisterID][]byte), + } +} + +func (r *RegisterValidator) SetValue(owner, key, value []byte) (err error) { + id := flow.RegisterID{ + Key: string(key), + Owner: string(owner), + } + r.updates[id] = value + + return r.Ledger.SetValue(owner, key, value) +} + +// ValidateBlock will go over all registers that were set during block execution and compare +// them against the registers stored on-chain using an execution data client for the provided +// Cadence height. +// Expected errors: +// - ErrStateMismatch error if there is a mismatch in any of the register values +// Any other error is an issue with client request or response. +func (r *RegisterValidator) ValidateBlock(cadenceHeight uint64) error { + defer func() { + // make sure we release all the data in the map after validating block + r.updates = make(map[flow.RegisterID][]byte) + }() + + if len(r.updates) == 0 { + return nil + } + + registers := make([]*entities.RegisterID, 0) + values := maps.Values(r.updates) + + for id := range r.updates { + registers = append(registers, convert.RegisterIDToMessage(id)) + } + + response, err := r.execution.GetRegisterValues( + context.Background(), + &executiondata.GetRegisterValuesRequest{ + BlockHeight: cadenceHeight, + RegisterIds: registers, + }, + ) + if err != nil { + return fmt.Errorf("invalid request for register values: %w", err) + } + + for i, val := range response.Values { + if !bytes.Equal(values[i], val) { + return fmt.Errorf( + "%w register %s with value %x does not match remote state value %x at height %d", + errs.ErrStateMismatch, + maps.Keys(r.updates)[i].String(), + values[i], + val, + cadenceHeight, + ) + } + } + + return nil +} diff --git a/storage/index.go b/storage/index.go index 38cebeb7..fde4be87 100644 --- a/storage/index.go +++ b/storage/index.go @@ -33,11 +33,6 @@ type BlockIndexer interface { // - errors.NotFound if the block is not found GetHeightByID(ID common.Hash) (uint64, error) - // LatestEVMHeight returns the latest stored EVM block height. - // Expected errors: - // - errors.NotInitialized if the storage was not initialized - LatestEVMHeight() (uint64, error) - // LatestCadenceHeight return the latest stored Cadence height. // Expected errors: // - errors.NotInitialized if the storage was not initialized @@ -59,6 +54,21 @@ type BlockIndexer interface { // Cadence block ID. // - errors.NotFound if the height is not found GetCadenceID(height uint64) (flow.Identifier, error) + + // SetExecutedHeight sets the evm block height which was re-executed by + // local state index. We use this value to keep track of progress of + // local state index. + SetExecutedHeight(evmHeight uint64) error + + // LatestExecutedHeight stores the evm block height at which the local + // state index is re-executed. Each block gets executed for rebuilding + // local state index and this height tracks the progress. + LatestExecutedHeight() (uint64, error) + + // LatestIndexedHeight returns the latest indexed EVM block height. + // Expected errors: + // - errors.NotInitialized if the storage was not initialized + LatestIndexedHeight() (uint64, error) } type ReceiptIndexer interface { diff --git a/storage/index_testsuite.go b/storage/index_testsuite.go index bdd608f2..5f674c5b 100644 --- a/storage/index_testsuite.go +++ b/storage/index_testsuite.go @@ -92,11 +92,11 @@ func (b *BlockTestSuite) TestHeights() { err := b.Blocks.Store(lastHeight+10, flow.Identifier{byte(i)}, mocks.NewBlock(lastHeight), nil) b.Require().NoError(err) - last, err := b.Blocks.LatestEVMHeight() + last, err := b.Blocks.LatestIndexedHeight() b.Require().NoError(err) b.Require().Equal(lastHeight, last) - last, err = b.Blocks.LatestEVMHeight() // second time it should get it from cache + last, err = b.Blocks.LatestIndexedHeight() // second time it should get it from cache b.Require().NoError(err) b.Require().Equal(lastHeight, last) } diff --git a/storage/mocks/BlockIndexer.go b/storage/mocks/BlockIndexer.go index 3d3b5c44..1266a1ac 100644 --- a/storage/mocks/BlockIndexer.go +++ b/storage/mocks/BlockIndexer.go @@ -18,6 +18,24 @@ type BlockIndexer struct { mock.Mock } +// ExecutedHeight provides a mock function with given fields: evmHeight +func (_m *BlockIndexer) SetExecutedHeight(evmHeight uint64) error { + ret := _m.Called(evmHeight) + + if len(ret) == 0 { + panic("no return value specified for SetExecutedHeight") + } + + var r0 error + if rf, ok := ret.Get(0).(func(uint64) error); ok { + r0 = rf(evmHeight) + } else { + r0 = ret.Error(0) + } + + return r0 +} + // GetByHeight provides a mock function with given fields: height func (_m *BlockIndexer) GetByHeight(height uint64) (*models.Block, error) { ret := _m.Called(height) @@ -193,11 +211,39 @@ func (_m *BlockIndexer) LatestCadenceHeight() (uint64, error) { } // LatestEVMHeight provides a mock function with given fields: -func (_m *BlockIndexer) LatestEVMHeight() (uint64, error) { +func (_m *BlockIndexer) LatestIndexedHeight() (uint64, error) { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for LatestIndexedHeight") + } + + var r0 uint64 + var r1 error + if rf, ok := ret.Get(0).(func() (uint64, error)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() uint64); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(uint64) + } + + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// LatestExecutedHeight provides a mock function with given fields: +func (_m *BlockIndexer) LatestExecutedHeight() (uint64, error) { ret := _m.Called() if len(ret) == 0 { - panic("no return value specified for LatestEVMHeight") + panic("no return value specified for LatestExecutedHeight") } var r0 uint64 diff --git a/storage/pebble/blocks.go b/storage/pebble/blocks.go index d4596681..4629ba7a 100644 --- a/storage/pebble/blocks.go +++ b/storage/pebble/blocks.go @@ -104,7 +104,7 @@ func (b *Blocks) Store( ) } - if err := b.store.set(latestEVMHeightKey, nil, evmHeightBytes, batch); err != nil { + if err := b.store.set(latestIndexedHeight, nil, evmHeightBytes, batch); err != nil { return fmt.Errorf( "failed to store latest EVM height: %d, with: %w", block.Height, @@ -170,7 +170,7 @@ func (b *Blocks) GetHeightByID(ID common.Hash) (uint64, error) { return binary.BigEndian.Uint64(height), nil } -func (b *Blocks) LatestEVMHeight() (uint64, error) { +func (b *Blocks) LatestIndexedHeight() (uint64, error) { b.mux.RLock() defer b.mux.RUnlock() @@ -178,7 +178,7 @@ func (b *Blocks) LatestEVMHeight() (uint64, error) { } func (b *Blocks) latestEVMHeight() (uint64, error) { - val, err := b.store.get(latestEVMHeightKey) + val, err := b.store.get(latestIndexedHeight) if err != nil { if errors.Is(err, errs.ErrEntityNotFound) { return 0, errs.ErrStorageNotInitialized @@ -218,7 +218,7 @@ func (b *Blocks) SetLatestCadenceHeight(height uint64, batch *pebble.Batch) erro // InitHeights sets the Cadence height to zero as well as EVM heights. Used for empty database init. func (b *Blocks) InitHeights(cadenceHeight uint64, cadenceID flow.Identifier) error { // sanity check, make sure we don't have any heights stored, disable overwriting the database - _, err := b.LatestEVMHeight() + _, err := b.LatestIndexedHeight() if !errors.Is(err, errs.ErrStorageNotInitialized) { return fmt.Errorf("can't init the database that already has data stored") } @@ -227,8 +227,12 @@ func (b *Blocks) InitHeights(cadenceHeight uint64, cadenceID flow.Identifier) er return fmt.Errorf("failed to init latest Cadence height at: %d, with: %w", cadenceHeight, err) } - if err := b.store.set(latestEVMHeightKey, nil, uint64Bytes(0), nil); err != nil { - return fmt.Errorf("failed to init latest EVM height at: %d, with: %w", 0, err) + if err := b.store.set(latestIndexedHeight, nil, uint64Bytes(0), nil); err != nil { + return fmt.Errorf("failed to init latest indexed EVM height at: %d, with: %w", 0, err) + } + + if err := b.store.set(latestExecutedHeight, nil, uint64Bytes(0), nil); err != nil { + return fmt.Errorf("failed to init latest executed EVM height at: %d, with: %w", 0, err) } // we store genesis block because it isn't emitted over the network @@ -264,6 +268,28 @@ func (b *Blocks) GetCadenceID(evmHeight uint64) (flow.Identifier, error) { return flow.BytesToID(val), nil } +func (b *Blocks) SetExecutedHeight(evmHeight uint64) error { + b.mux.Lock() + defer b.mux.Unlock() + + return b.store.set(latestExecutedHeight, nil, uint64Bytes(evmHeight), nil) +} + +func (b *Blocks) LatestExecutedHeight() (uint64, error) { + b.mux.RLock() + defer b.mux.RUnlock() + + val, err := b.store.get(latestExecutedHeight) + if err != nil { + if errors.Is(err, errs.ErrEntityNotFound) { + return 0, errs.ErrStorageNotInitialized + } + return 0, err + } + + return binary.BigEndian.Uint64(val), nil +} + func (b *Blocks) getBlock(keyCode byte, key []byte) (*models.Block, error) { data, err := b.store.get(keyCode, key) if err != nil { diff --git a/storage/pebble/keys.go b/storage/pebble/keys.go index 77411c7a..469f7f26 100644 --- a/storage/pebble/keys.go +++ b/storage/pebble/keys.go @@ -8,6 +8,7 @@ const ( blockIDToHeightKey = byte(2) evmHeightToCadenceHeightKey = byte(3) evmHeightToCadenceIDKey = byte(4) + latestExecutedHeight = byte(5) // transaction keys txIDKey = byte(10) @@ -29,7 +30,7 @@ const ( ledgerSlabIndex = byte(51) // special keys - latestEVMHeightKey = byte(100) + latestIndexedHeight = byte(100) latestCadenceHeightKey = byte(102) ) diff --git a/storage/pebble/ledger.go b/storage/pebble/ledger.go deleted file mode 100644 index 6669570e..00000000 --- a/storage/pebble/ledger.go +++ /dev/null @@ -1,114 +0,0 @@ -package pebble - -import ( - "errors" - "fmt" - "sync" - - "github.com/onflow/atree" - - errs "github.com/onflow/flow-evm-gateway/models/errors" -) - -var _ atree.Ledger = &Ledger{} - -// todo we need to support historic data, -// we likely need to create ledger with the context of block height -// and then prepend all keys with that height - -type Ledger struct { - store *Storage - mux sync.RWMutex -} - -func NewLedger(store *Storage) *Ledger { - return &Ledger{ - store: store, - mux: sync.RWMutex{}, - } -} - -func (l *Ledger) GetValue(owner, key []byte) ([]byte, error) { - l.mux.RLock() - defer l.mux.RUnlock() - - id := append(owner, key...) - val, err := l.store.get(ledgerValue, id) - if err != nil { - // as per interface expectation we need to remove nil if not found - if errors.Is(err, errs.ErrEntityNotFound) { - return nil, nil - } - - return nil, fmt.Errorf( - "failed to get ledger value at owner %x and key %x: %w", - owner, - key, - err, - ) - } - - return val, nil -} - -func (l *Ledger) SetValue(owner, key, value []byte) error { - l.mux.Lock() - defer l.mux.Unlock() - - id := append(owner, key...) - if err := l.store.set(ledgerValue, id, value, nil); err != nil { - return fmt.Errorf( - "failed to store ledger value for owner %x and key %x: %w", - owner, - key, - err, - ) - } - - return nil -} - -func (l *Ledger) ValueExists(owner, key []byte) (bool, error) { - val, err := l.GetValue(owner, key) - if err != nil { - return false, err - } - - return val != nil, nil -} - -func (l *Ledger) AllocateSlabIndex(owner []byte) (atree.SlabIndex, error) { - l.mux.Lock() - defer l.mux.Unlock() - - var index atree.SlabIndex - - val, err := l.store.get(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.Next() - if err := l.store.set(ledgerSlabIndex, owner, index[:], nil); err != nil { - return atree.SlabIndexUndefined, fmt.Errorf( - "slab index failed to set for owner %x: %w", - owner, - err, - ) - } - - return index, nil -} diff --git a/storage/pebble/receipts.go b/storage/pebble/receipts.go index be45a32e..5c45a1bf 100644 --- a/storage/pebble/receipts.go +++ b/storage/pebble/receipts.go @@ -243,7 +243,7 @@ func (r *Receipts) BloomsForBlockRange(start, end uint64) ([]*models.BloomsHeigh } func (r *Receipts) getLast() (uint64, error) { - l, err := r.store.get(latestEVMHeightKey) + l, err := r.store.get(latestIndexedHeight) if err != nil { return 0, fmt.Errorf("failed getting latest EVM height: %w", err) } diff --git a/storage/pebble/register.go b/storage/pebble/register.go new file mode 100644 index 00000000..253d53e9 --- /dev/null +++ b/storage/pebble/register.go @@ -0,0 +1,166 @@ +package pebble + +import ( + "errors" + "fmt" + "sync" + + "github.com/cockroachdb/pebble" + "github.com/onflow/atree" + + errs "github.com/onflow/flow-evm-gateway/models/errors" +) + +var _ atree.Ledger = &Register{} + +type Register struct { + height uint64 + store *Storage + 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. +// +// Batch is an optional argument, if provided the operations will be performed +// inside that batch that later needs to be committed by the provider of the batch. +func NewRegister(store *Storage, height uint64, batch *pebble.Batch) *Register { + return &Register{ + store: store, + height: height, + batch: batch, + mux: sync.RWMutex{}, + } +} + +func (l *Register) GetValue(owner, key []byte) ([]byte, error) { + l.mux.RLock() + defer l.mux.RUnlock() + + var db pebble.Reader = l.store.db + if l.batch != nil { + db = l.batch + } + + iter, err := db.NewIter(&pebble.IterOptions{ + LowerBound: l.idLower(owner, key), + UpperBound: l.idUpper(owner, key), + }) + if err != nil { + return nil, fmt.Errorf("failed to create register range itterator: %w", err) + } + defer func() { + if err := iter.Close(); err != nil { + if err != nil { + l.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 (l *Register) SetValue(owner, key, value []byte) error { + l.mux.Lock() + defer l.mux.Unlock() + + id := l.id(owner, key) + if err := l.store.set(ledgerValue, id, value, l.batch); err != nil { + return fmt.Errorf( + "failed to store ledger value for owner %x and key %x: %w", + owner, + key, + err, + ) + } + + return nil +} + +func (l *Register) ValueExists(owner, key []byte) (bool, error) { + val, err := l.GetValue(owner, key) + if err != nil { + return false, err + } + + return val != nil, nil +} + +func (l *Register) AllocateSlabIndex(owner []byte) (atree.SlabIndex, error) { + l.mux.Lock() + defer l.mux.Unlock() + + var index atree.SlabIndex + + val, err := l.store.batchGet(l.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 := l.store.set(ledgerSlabIndex, owner, index[:], l.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 (l *Register) id(owner, key []byte) []byte { + id := append(owner, key...) + h := uint64Bytes(l.height) + return append(id, h...) +} + +func (l *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(l.height + 1) + return append(id, h...) +} + +func (l *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/storage/pebble/register_test.go b/storage/pebble/register_test.go new file mode 100644 index 00000000..04ff1851 --- /dev/null +++ b/storage/pebble/register_test.go @@ -0,0 +1,122 @@ +package pebble + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func Test_RegisterSetGet(t *testing.T) { + + runDB("set and get at same height", t, func(t *testing.T, db *Storage) { + height := uint64(1) + reg := NewRegister(db, height, nil) + + owner := uint64Bytes(1337) + key := uint64Bytes(32) + val := uint64Bytes(124932) + + err := reg.SetValue(owner, key, val) + require.NoError(t, err) + + retVal, err := reg.GetValue(owner, key) + require.NoError(t, err) + + require.Equal(t, val, retVal) + }) + + runDB("set and get at latest height", t, func(t *testing.T, db *Storage) { + owner := uint64Bytes(1337) + key := uint64Bytes(32) + + count := 100 + for i := 0; i < count; i++ { + h := uint64(i) + reg := NewRegister(db, h, nil) + val := uint64Bytes(h) + + err := reg.SetValue(owner, key, val) + require.NoError(t, err) + } + + for i := 0; i < count; i++ { + h := uint64(i) + reg := NewRegister(db, h, nil) + val := uint64Bytes(h) + + retVal, err := reg.GetValue(owner, key) + require.NoError(t, err) + + require.Equal(t, val, retVal) + } + + }) + + runDB("multiple sets for different accounts and get at latest height", t, func(t *testing.T, db *Storage) { + + height1 := uint64(1) + owner11 := uint64Bytes(101) + key11 := uint64Bytes(500) + val11 := uint64Bytes(1001) + key15 := uint64Bytes(500) + val15 := uint64Bytes(2002) + + owner21 := uint64Bytes(105) + key21 := uint64Bytes(600) + val21 := uint64Bytes(1002) + key22 := uint64Bytes(500) + val22 := uint64Bytes(2002) + + reg := NewRegister(db, height1, nil) + err := reg.SetValue(owner11, key11, val11) + require.NoError(t, err) + + height2 := uint64(3) + reg = NewRegister(db, height2, nil) + err = reg.SetValue(owner21, key21, val21) + require.NoError(t, err) + err = reg.SetValue(owner21, key22, val22) + require.NoError(t, err) + + height3 := uint64(5) + reg = NewRegister(db, height3, nil) + err = reg.SetValue(owner11, key15, val15) + require.NoError(t, err) + err = reg.SetValue(owner21, key22, val22) + require.NoError(t, err) + + reg = NewRegister(db, uint64(0), nil) + // not found + val, err := reg.GetValue(owner11, key11) + require.Nil(t, err) + require.Nil(t, val) + + val, err = reg.GetValue(owner21, key21) + require.NoError(t, err) + require.Nil(t, val) + + reg = NewRegister(db, uint64(1), nil) + val, err = reg.GetValue(owner11, key11) + require.NoError(t, err) + require.Equal(t, val11, val) + + reg = NewRegister(db, uint64(2), nil) + val, err = reg.GetValue(owner11, key11) + require.NoError(t, err) + require.Equal(t, val11, val) + + reg = NewRegister(db, uint64(3), nil) + val, err = reg.GetValue(owner11, key11) + require.NoError(t, err) + require.Equal(t, val11, val) + + reg = NewRegister(db, uint64(5), nil) + val, err = reg.GetValue(owner11, key15) + require.NoError(t, err) + require.Equal(t, val15, val) + + // todo write more examples + + }) + +} diff --git a/storage/pebble/storage_test.go b/storage/pebble/storage_test.go index 4a031b3c..1a7faa51 100644 --- a/storage/pebble/storage_test.go +++ b/storage/pebble/storage_test.go @@ -201,7 +201,7 @@ func TestBatch(t *testing.T) { require.NoError(t, err) require.Equal(t, bl, dbBlock) - dbEVM, err := blocks.LatestEVMHeight() + dbEVM, err := blocks.LatestIndexedHeight() require.NoError(t, err) require.Equal(t, evmHeight, dbEVM) diff --git a/tests/e2e_web3js_test.go b/tests/e2e_web3js_test.go index 70276762..60f9c0ae 100644 --- a/tests/e2e_web3js_test.go +++ b/tests/e2e_web3js_test.go @@ -28,6 +28,10 @@ func TestWeb3_E2E(t *testing.T) { runWeb3Test(t, "build_evm_state_test") }) + t.Run("test cadence arch and environment calls", func(t *testing.T) { + runWeb3Test(t, "cadence_arch_env_test") + }) + t.Run("test setup sanity check", func(t *testing.T) { runWeb3Test(t, "setup_test") }) @@ -150,7 +154,7 @@ func TestWeb3_E2E(t *testing.T) { require.NoError(t, err) // contract deployment transaction - deployPayload, _, err := evmSign(big.NewInt(0), 1_250_000, accountKey, nonce, nil, contractCode) + deployPayload, _, err := evmSign(big.NewInt(0), 1_550_000, accountKey, nonce, nil, contractCode) require.NoError(t, err) nonce += 1 diff --git a/tests/fixtures/storage.byte b/tests/fixtures/storage.byte index 30561faa..a9da90dc 100644 --- a/tests/fixtures/storage.byte +++ b/tests/fixtures/storage.byte @@ -1 +1 @@ -60806040526105395f81905550611326806100195f395ff3fe608060405234801561000f575f80fd5b5060043610610114575f3560e01c8063911007b4116100a0578063c550f90f1161006f578063c550f90f146102ba578063cbaff5f9146102d8578063d0d250bd146102e2578063dda3a7bd14610300578063ea8d8ccf1461030a57610114565b8063911007b41461021e5780639967062d1461024e578063adc879e91461027e578063b2821c8f1461029c57610114565b80635ec01e4d116100e75780635ec01e4d1461018e5780636057361d146101ac5780636babb224146101c857806383197ef0146101e457806385df51fd146101ee57610114565b80632e64cec11461011857806348b15166146101365780634cbefa6a1461015457806357e871e714610170575b5f80fd5b61012061033a565b60405161012d9190610a63565b60405180910390f35b61013e610342565b60405161014b9190610a63565b60405180910390f35b61016e60048036038101906101699190610ab7565b610349565b005b610178610352565b6040516101859190610a63565b60405180910390f35b610196610359565b6040516101a39190610a63565b60405180910390f35b6101c660048036038101906101c19190610ab7565b610360565b005b6101e260048036038101906101dd9190610ab7565b610369565b005b6101ec6103b6565b005b61020860048036038101906102039190610ab7565b6103cf565b6040516102159190610afa565b60405180910390f35b61023860048036038101906102339190610b50565b6103d9565b6040516102459190610afa565b60405180910390f35b61026860048036038101906102639190610bae565b61053b565b6040516102759190610bfb565b60405180910390f35b6102866105a5565b6040516102939190610a63565b60405180910390f35b6102a46105ac565b6040516102b19190610c23565b60405180910390f35b6102c2610701565b6040516102cf9190610c23565b60405180910390f35b6102e0610856565b005b6102ea610898565b6040516102f79190610c7b565b60405180910390f35b6103086108a5565b005b610324600480360381019061031f9190610e24565b6108e3565b6040516103319190610eaa565b60405180910390f35b5f8054905090565b5f42905090565b805f8190555f80fd5b5f43905090565b5f44905090565b805f8190555050565b803373ffffffffffffffffffffffffffffffffffffffff167f043cc306157a91d747b36aba0e235bbbc5771d75aba162f6e5540767d22673c660405160405180910390a3805f8190555050565b3373ffffffffffffffffffffffffffffffffffffffff16ff5b5f81409050919050565b5f805f6801000000000000000173ffffffffffffffffffffffffffffffffffffffff168460405160240161040d9190610c23565b6040516020818303038152906040527f78a75fbe000000000000000000000000000000000000000000000000000000007bffffffffffffffffffffffffffffffffffffffffffffffffffffffff19166020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff83818316178352505050506040516104979190610f15565b5f60405180830381855afa9150503d805f81146104cf576040519150601f19603f3d011682016040523d82523d5f602084013e6104d4565b606091505b509150915081610519576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161051090610f85565b60405180910390fd5b5f8180602001905181019061052e9190610fb7565b9050809350505050919050565b5f808284610549919061100f565b905082843373ffffffffffffffffffffffffffffffffffffffff167f76efea95e5da1fa661f235b2921ae1d89b99e457ec73fb88e34a1d150f95c64b846040516105939190610bfb565b60405180910390a48091505092915050565b5f46905090565b5f805f6801000000000000000173ffffffffffffffffffffffffffffffffffffffff166040516024016040516020818303038152906040527f705fab20000000000000000000000000000000000000000000000000000000007bffffffffffffffffffffffffffffffffffffffffffffffffffffffff19166020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff838183161783525050505060405161065f9190610f15565b5f60405180830381855afa9150503d805f8114610697576040519150601f19603f3d011682016040523d82523d5f602084013e61069c565b606091505b5091509150816106e1576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016106d89061109a565b60405180910390fd5b5f818060200190518101906106f691906110cc565b905080935050505090565b5f805f6801000000000000000173ffffffffffffffffffffffffffffffffffffffff166040516024016040516020818303038152906040527f53e87d66000000000000000000000000000000000000000000000000000000007bffffffffffffffffffffffffffffffffffffffffffffffffffffffff19166020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff83818316178352505050506040516107b49190610f15565b5f60405180830381855afa9150503d805f81146107ec576040519150601f19603f3d011682016040523d82523d5f602084013e6107f1565b606091505b509150915081610836576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161082d90610f85565b60405180910390fd5b5f8180602001905181019061084b91906110cc565b905080935050505090565b5f610896576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161088d90611141565b60405180910390fd5b565b6801000000000000000181565b60056040517f9195785a0000000000000000000000000000000000000000000000000000000081526004016108da91906111eb565b60405180910390fd5b5f805f6801000000000000000173ffffffffffffffffffffffffffffffffffffffff1686868660405160240161091b9392919061125f565b6040516020818303038152906040527f5ee837e7000000000000000000000000000000000000000000000000000000007bffffffffffffffffffffffffffffffffffffffffffffffffffffffff19166020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff83818316178352505050506040516109a59190610f15565b5f60405180830381855afa9150503d805f81146109dd576040519150601f19603f3d011682016040523d82523d5f602084013e6109e2565b606091505b509150915081610a27576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401610a1e9061109a565b60405180910390fd5b5f81806020019051810190610a3c91906112c5565b90508093505050509392505050565b5f819050919050565b610a5d81610a4b565b82525050565b5f602082019050610a765f830184610a54565b92915050565b5f604051905090565b5f80fd5b5f80fd5b610a9681610a4b565b8114610aa0575f80fd5b50565b5f81359050610ab181610a8d565b92915050565b5f60208284031215610acc57610acb610a85565b5b5f610ad984828501610aa3565b91505092915050565b5f819050919050565b610af481610ae2565b82525050565b5f602082019050610b0d5f830184610aeb565b92915050565b5f67ffffffffffffffff82169050919050565b610b2f81610b13565b8114610b39575f80fd5b50565b5f81359050610b4a81610b26565b92915050565b5f60208284031215610b6557610b64610a85565b5b5f610b7284828501610b3c565b91505092915050565b5f819050919050565b610b8d81610b7b565b8114610b97575f80fd5b50565b5f81359050610ba881610b84565b92915050565b5f8060408385031215610bc457610bc3610a85565b5b5f610bd185828601610b9a565b9250506020610be285828601610b9a565b9150509250929050565b610bf581610b7b565b82525050565b5f602082019050610c0e5f830184610bec565b92915050565b610c1d81610b13565b82525050565b5f602082019050610c365f830184610c14565b92915050565b5f73ffffffffffffffffffffffffffffffffffffffff82169050919050565b5f610c6582610c3c565b9050919050565b610c7581610c5b565b82525050565b5f602082019050610c8e5f830184610c6c565b92915050565b610c9d81610c5b565b8114610ca7575f80fd5b50565b5f81359050610cb881610c94565b92915050565b610cc781610ae2565b8114610cd1575f80fd5b50565b5f81359050610ce281610cbe565b92915050565b5f80fd5b5f80fd5b5f601f19601f8301169050919050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52604160045260245ffd5b610d3682610cf0565b810181811067ffffffffffffffff82111715610d5557610d54610d00565b5b80604052505050565b5f610d67610a7c565b9050610d738282610d2d565b919050565b5f67ffffffffffffffff821115610d9257610d91610d00565b5b610d9b82610cf0565b9050602081019050919050565b828183375f83830152505050565b5f610dc8610dc384610d78565b610d5e565b905082815260208101848484011115610de457610de3610cec565b5b610def848285610da8565b509392505050565b5f82601f830112610e0b57610e0a610ce8565b5b8135610e1b848260208601610db6565b91505092915050565b5f805f60608486031215610e3b57610e3a610a85565b5b5f610e4886828701610caa565b9350506020610e5986828701610cd4565b925050604084013567ffffffffffffffff811115610e7a57610e79610a89565b5b610e8686828701610df7565b9150509250925092565b5f8115159050919050565b610ea481610e90565b82525050565b5f602082019050610ebd5f830184610e9b565b92915050565b5f81519050919050565b5f81905092915050565b8281835e5f83830152505050565b5f610eef82610ec3565b610ef98185610ecd565b9350610f09818560208601610ed7565b80840191505092915050565b5f610f208284610ee5565b915081905092915050565b5f82825260208201905092915050565b7f756e7375636365737366756c2063616c6c20746f2061726368200000000000005f82015250565b5f610f6f601a83610f2b565b9150610f7a82610f3b565b602082019050919050565b5f6020820190508181035f830152610f9c81610f63565b9050919050565b5f81519050610fb181610cbe565b92915050565b5f60208284031215610fcc57610fcb610a85565b5b5f610fd984828501610fa3565b91505092915050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b5f61101982610b7b565b915061102483610b7b565b92508282019050828112155f8312168382125f84121516171561104a57611049610fe2565b5b92915050565b7f756e7375636365737366756c2063616c6c20746f2061726368000000000000005f82015250565b5f611084601983610f2b565b915061108f82611050565b602082019050919050565b5f6020820190508181035f8301526110b181611078565b9050919050565b5f815190506110c681610b26565b92915050565b5f602082840312156110e1576110e0610a85565b5b5f6110ee848285016110b8565b91505092915050565b7f417373657274204572726f72204d6573736167650000000000000000000000005f82015250565b5f61112b601483610f2b565b9150611136826110f7565b602082019050919050565b5f6020820190508181035f8301526111588161111f565b9050919050565b5f819050919050565b5f819050919050565b5f61118b6111866111818461115f565b611168565b610a4b565b9050919050565b61119b81611171565b82525050565b7f56616c756520697320746f6f206c6f77000000000000000000000000000000005f82015250565b5f6111d5601083610f2b565b91506111e0826111a1565b602082019050919050565b5f6040820190506111fe5f830184611192565b818103602083015261120f816111c9565b905092915050565b5f82825260208201905092915050565b5f61123182610ec3565b61123b8185611217565b935061124b818560208601610ed7565b61125481610cf0565b840191505092915050565b5f6060820190506112725f830186610c6c565b61127f6020830185610aeb565b81810360408301526112918184611227565b9050949350505050565b6112a481610e90565b81146112ae575f80fd5b50565b5f815190506112bf8161129b565b92915050565b5f602082840312156112da576112d9610a85565b5b5f6112e7848285016112b1565b9150509291505056fea26469706673582212203b936902323e60d2e9997e4d5c9b8feb664f99fe5d425c9a1a4830dd36cc92fa64736f6c634300081a0033 \ No newline at end of file +60806040526105395f819055506117c9806100195f395ff3fe608060405234801561000f575f80fd5b50600436106101c2575f3560e01c80639075ef95116100f7578063bc31124b11610095578063d0d250bd1161006f578063d0d250bd14610420578063dda3a7bd1461043e578063ea8d8ccf14610448578063fe027d1114610478576101c2565b8063bc31124b146103dc578063c550f90f146103f8578063cbaff5f914610416576101c2565b8063adc879e9116100d1578063adc879e91461038c578063af32a363146103aa578063b2821c8f146103b4578063b7ec3e5f146103d2576101c2565b80639075ef9514610322578063911007b41461032c5780639967062d1461035c576101c2565b80635ec01e4d1161016457806364fc013a1161013e57806364fc013a146102b05780636babb224146102cc57806383197ef0146102e857806385df51fd146102f2576101c2565b80635ec01e4d1461025a5780635f0f73fb146102785780636057361d14610294576101c2565b80634cbefa6a116101a05780634cbefa6a1461020c57806357e871e7146102285780635b5764f4146102465780635e4268e614610250576101c2565b80632e64cec1146101c657806346c38ab0146101e457806348b15166146101ee575b5f80fd5b6101ce610482565b6040516101db9190610f06565b60405180910390f35b6101ec61048a565b005b6101f66104da565b6040516102039190610f06565b60405180910390f35b61022660048036038101906102219190610f5a565b6104e1565b005b6102306104ea565b60405161023d9190610f06565b60405180910390f35b61024e6104f1565b005b61025861054d565b005b61026261059d565b60405161026f9190610f06565b60405180910390f35b610292600480360381019061028d9190610f5a565b6105a4565b005b6102ae60048036038101906102a99190610f5a565b6105f6565b005b6102ca60048036038101906102c59190610fc2565b6105ff565b005b6102e660048036038101906102e19190610f5a565b61065d565b005b6102f06106aa565b005b61030c60048036038101906103079190610f5a565b6106c3565b6040516103199190611005565b60405180910390f35b61032a6106cd565b005b61034660048036038101906103419190610fc2565b61071d565b6040516103539190611005565b60405180910390f35b61037660048036038101906103719190611051565b61087f565b604051610383919061109e565b60405180910390f35b6103946108e9565b6040516103a19190610f06565b60405180910390f35b6103b26108f0565b005b6103bc61094c565b6040516103c991906110c6565b60405180910390f35b6103da610aa1565b005b6103f660048036038101906103f1919061129f565b610af1565b005b610400610b53565b60405161040d91906110c6565b60405180910390f35b61041e610ca8565b005b610428610cea565b604051610435919061131a565b60405180910390f35b610446610cf7565b005b610462600480360381019061045d919061129f565b610d35565b60405161046f919061134d565b60405180910390f35b610480610e9d565b005b5f8054905090565b3373ffffffffffffffffffffffffffffffffffffffff167fcdda07d20845760c8f1960f9992eb7b5253a2e6a68eb2340137c70a30e3af38b436040516104d09190610f06565b60405180910390a2565b5f42905090565b805f8190555f80fd5b5f43905090565b5f6104fa610b53565b90503373ffffffffffffffffffffffffffffffffffffffff167f226e31c8dfdcc17bec5aa64ce3cd9856d9bb4637e48450d27f62e0bda5bca6498260405161054291906110c6565b60405180910390a250565b3373ffffffffffffffffffffffffffffffffffffffff167fb8a2de765c79a4dd09c7a683c268e826303e1bbd5425c29706963329538a7534466040516105939190610f06565b60405180910390a2565b5f44905090565b3373ffffffffffffffffffffffffffffffffffffffff167f9c5f2f6f83b58de8294bc8af0de4c9e4fdd2c335eaf0d9a2461b5a5e4b014e8682406040516105eb9190611005565b60405180910390a250565b805f8190555050565b5f6106098261071d565b90503373ffffffffffffffffffffffffffffffffffffffff167f3c1e946213ca4a4f826f561e68d2e244c2db896d9612980bf786cd7da2c6ccdf826040516106519190611005565b60405180910390a25050565b803373ffffffffffffffffffffffffffffffffffffffff167f043cc306157a91d747b36aba0e235bbbc5771d75aba162f6e5540767d22673c660405160405180910390a3805f8190555050565b3373ffffffffffffffffffffffffffffffffffffffff16ff5b5f81409050919050565b3373ffffffffffffffffffffffffffffffffffffffff167f52481872d5402d9c11fcd282d57bfa9af8a0edcc9115a4ba1936f3bb45e286bb446040516107139190610f06565b60405180910390a2565b5f805f6801000000000000000173ffffffffffffffffffffffffffffffffffffffff168460405160240161075191906110c6565b6040516020818303038152906040527f78a75fbe000000000000000000000000000000000000000000000000000000007bffffffffffffffffffffffffffffffffffffffffffffffffffffffff19166020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff83818316178352505050506040516107db91906113b8565b5f60405180830381855afa9150503d805f8114610813576040519150601f19603f3d011682016040523d82523d5f602084013e610818565b606091505b50915091508161085d576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161085490611428565b60405180910390fd5b5f81806020019051810190610872919061145a565b9050809350505050919050565b5f80828461088d91906114b2565b905082843373ffffffffffffffffffffffffffffffffffffffff167f76efea95e5da1fa661f235b2921ae1d89b99e457ec73fb88e34a1d150f95c64b846040516108d7919061109e565b60405180910390a48091505092915050565b5f46905090565b5f6108f961094c565b90503373ffffffffffffffffffffffffffffffffffffffff167f7f3eb80b1815b51402bef9f6393921916f5c7391ff4f5e82e2f5d7f866c65fb78260405161094191906110c6565b60405180910390a250565b5f805f6801000000000000000173ffffffffffffffffffffffffffffffffffffffff166040516024016040516020818303038152906040527f705fab20000000000000000000000000000000000000000000000000000000007bffffffffffffffffffffffffffffffffffffffffffffffffffffffff19166020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff83818316178352505050506040516109ff91906113b8565b5f60405180830381855afa9150503d805f8114610a37576040519150601f19603f3d011682016040523d82523d5f602084013e610a3c565b606091505b509150915081610a81576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401610a789061153d565b60405180910390fd5b5f81806020019051810190610a96919061156f565b905080935050505090565b3373ffffffffffffffffffffffffffffffffffffffff167f05a74f03de8d43b274ce924e4542bad549dea8f3572574a2b81d6ba62fc717b442604051610ae79190610f06565b60405180910390a2565b5f610afd848484610d35565b90503373ffffffffffffffffffffffffffffffffffffffff167f19481a9edff48c370d941b1f368fd2198a0f3117e18748a56d5e193b36dc569a82604051610b45919061134d565b60405180910390a250505050565b5f805f6801000000000000000173ffffffffffffffffffffffffffffffffffffffff166040516024016040516020818303038152906040527f53e87d66000000000000000000000000000000000000000000000000000000007bffffffffffffffffffffffffffffffffffffffffffffffffffffffff19166020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff8381831617835250505050604051610c0691906113b8565b5f60405180830381855afa9150503d805f8114610c3e576040519150601f19603f3d011682016040523d82523d5f602084013e610c43565b606091505b509150915081610c88576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401610c7f90611428565b60405180910390fd5b5f81806020019051810190610c9d919061156f565b905080935050505090565b5f610ce8576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401610cdf906115e4565b60405180910390fd5b565b6801000000000000000181565b60056040517f9195785a000000000000000000000000000000000000000000000000000000008152600401610d2c919061168e565b60405180910390fd5b5f805f6801000000000000000173ffffffffffffffffffffffffffffffffffffffff16868686604051602401610d6d93929190611702565b6040516020818303038152906040527f5ee837e7000000000000000000000000000000000000000000000000000000007bffffffffffffffffffffffffffffffffffffffffffffffffffffffff19166020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff8381831617835250505050604051610df791906113b8565b5f60405180830381855afa9150503d805f8114610e2f576040519150601f19603f3d011682016040523d82523d5f602084013e610e34565b606091505b509150915081610e79576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401610e709061153d565b60405180910390fd5b5f81806020019051810190610e8e9190611768565b90508093505050509392505050565b3373ffffffffffffffffffffffffffffffffffffffff167f094703abe6f34e06c217139505bcc40e39f606853962e503a74d776a28f1148f5f54604051610ee49190610f06565b60405180910390a2565b5f819050919050565b610f0081610eee565b82525050565b5f602082019050610f195f830184610ef7565b92915050565b5f604051905090565b5f80fd5b5f80fd5b610f3981610eee565b8114610f43575f80fd5b50565b5f81359050610f5481610f30565b92915050565b5f60208284031215610f6f57610f6e610f28565b5b5f610f7c84828501610f46565b91505092915050565b5f67ffffffffffffffff82169050919050565b610fa181610f85565b8114610fab575f80fd5b50565b5f81359050610fbc81610f98565b92915050565b5f60208284031215610fd757610fd6610f28565b5b5f610fe484828501610fae565b91505092915050565b5f819050919050565b610fff81610fed565b82525050565b5f6020820190506110185f830184610ff6565b92915050565b5f819050919050565b6110308161101e565b811461103a575f80fd5b50565b5f8135905061104b81611027565b92915050565b5f806040838503121561106757611066610f28565b5b5f6110748582860161103d565b92505060206110858582860161103d565b9150509250929050565b6110988161101e565b82525050565b5f6020820190506110b15f83018461108f565b92915050565b6110c081610f85565b82525050565b5f6020820190506110d95f8301846110b7565b92915050565b5f73ffffffffffffffffffffffffffffffffffffffff82169050919050565b5f611108826110df565b9050919050565b611118816110fe565b8114611122575f80fd5b50565b5f813590506111338161110f565b92915050565b61114281610fed565b811461114c575f80fd5b50565b5f8135905061115d81611139565b92915050565b5f80fd5b5f80fd5b5f601f19601f8301169050919050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52604160045260245ffd5b6111b18261116b565b810181811067ffffffffffffffff821117156111d0576111cf61117b565b5b80604052505050565b5f6111e2610f1f565b90506111ee82826111a8565b919050565b5f67ffffffffffffffff82111561120d5761120c61117b565b5b6112168261116b565b9050602081019050919050565b828183375f83830152505050565b5f61124361123e846111f3565b6111d9565b90508281526020810184848401111561125f5761125e611167565b5b61126a848285611223565b509392505050565b5f82601f83011261128657611285611163565b5b8135611296848260208601611231565b91505092915050565b5f805f606084860312156112b6576112b5610f28565b5b5f6112c386828701611125565b93505060206112d48682870161114f565b925050604084013567ffffffffffffffff8111156112f5576112f4610f2c565b5b61130186828701611272565b9150509250925092565b611314816110fe565b82525050565b5f60208201905061132d5f83018461130b565b92915050565b5f8115159050919050565b61134781611333565b82525050565b5f6020820190506113605f83018461133e565b92915050565b5f81519050919050565b5f81905092915050565b8281835e5f83830152505050565b5f61139282611366565b61139c8185611370565b93506113ac81856020860161137a565b80840191505092915050565b5f6113c38284611388565b915081905092915050565b5f82825260208201905092915050565b7f756e7375636365737366756c2063616c6c20746f2061726368200000000000005f82015250565b5f611412601a836113ce565b915061141d826113de565b602082019050919050565b5f6020820190508181035f83015261143f81611406565b9050919050565b5f8151905061145481611139565b92915050565b5f6020828403121561146f5761146e610f28565b5b5f61147c84828501611446565b91505092915050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b5f6114bc8261101e565b91506114c78361101e565b92508282019050828112155f8312168382125f8412151617156114ed576114ec611485565b5b92915050565b7f756e7375636365737366756c2063616c6c20746f2061726368000000000000005f82015250565b5f6115276019836113ce565b9150611532826114f3565b602082019050919050565b5f6020820190508181035f8301526115548161151b565b9050919050565b5f8151905061156981610f98565b92915050565b5f6020828403121561158457611583610f28565b5b5f6115918482850161155b565b91505092915050565b7f417373657274204572726f72204d6573736167650000000000000000000000005f82015250565b5f6115ce6014836113ce565b91506115d98261159a565b602082019050919050565b5f6020820190508181035f8301526115fb816115c2565b9050919050565b5f819050919050565b5f819050919050565b5f61162e61162961162484611602565b61160b565b610eee565b9050919050565b61163e81611614565b82525050565b7f56616c756520697320746f6f206c6f77000000000000000000000000000000005f82015250565b5f6116786010836113ce565b915061168382611644565b602082019050919050565b5f6040820190506116a15f830184611635565b81810360208301526116b28161166c565b905092915050565b5f82825260208201905092915050565b5f6116d482611366565b6116de81856116ba565b93506116ee81856020860161137a565b6116f78161116b565b840191505092915050565b5f6060820190506117155f83018661130b565b6117226020830185610ff6565b818103604083015261173481846116ca565b9050949350505050565b61174781611333565b8114611751575f80fd5b50565b5f815190506117628161173e565b92915050565b5f6020828403121561177d5761177c610f28565b5b5f61178a84828501611754565b9150509291505056fea264697066735822122088b3fe8589df5bd1c9d7d46b543867f57d179028bd99eeec4aca768ba9581a3f64736f6c634300081a0033 \ No newline at end of file diff --git a/tests/fixtures/storage.sol b/tests/fixtures/storage.sol index affce522..665deb79 100644 --- a/tests/fixtures/storage.sol +++ b/tests/fixtures/storage.sol @@ -3,10 +3,19 @@ pragma solidity >=0.8.2 <0.9.0; contract Storage { - address constant public cadenceArch = 0x0000000000000000000000010000000000000001; event NewStore(address indexed caller, uint256 indexed value); event Calculated(address indexed caller, int indexed numA, int indexed numB, int sum); + event Retrieved(address indexed caller, uint256 value); + event BlockNumber(address indexed caller, uint256 value); + event BlockTime(address indexed caller, uint value); + event BlockHash(address indexed caller, bytes32 hash); + event Random(address indexed caller, uint256 value); + event ChainID(address indexed caller, uint256 value); + event VerifyArchCallToRandomSource(address indexed caller, bytes32 output); + event VerifyArchCallToRevertibleRandom(address indexed caller, uint64 output); + event VerifyArchCallToFlowBlockHeight(address indexed caller, uint64 output); + event VerifyArchCallToVerifyCOAOwnershipProof(address indexed caller, bool output); error MyCustomError(uint value, string message); @@ -30,12 +39,16 @@ contract Storage { revert(); } - function retrieve() public view returns (uint256){ + function retrieve() public view returns (uint256) { return number; } + function emitRetrieve() public { + emit Retrieved(msg.sender, number); + } + function sum(int A, int B) public returns (int) { - int s = A+B; + int s = A + B; emit Calculated(msg.sender, A, B, s); return s; } @@ -44,32 +57,52 @@ contract Storage { return block.number; } + function emitBlockNumber() public { + emit BlockNumber(msg.sender, block.number); + } + function blockTime() public view returns (uint) { return block.timestamp; } - function blockHash(uint num) public view returns (bytes32) { + function emitBlockTime() public { + emit BlockTime(msg.sender, block.timestamp); + } + + function blockHash(uint num) public view returns (bytes32) { return blockhash(num); } + function emitBlockHash(uint num) public { + emit BlockHash(msg.sender, blockhash(num)); + } + function random() public view returns (uint256) { return block.prevrandao; } + function emitRandom() public { + emit Random(msg.sender, block.prevrandao); + } + function chainID() public view returns (uint256) { return block.chainid; } + function emitChainID() public { + emit ChainID(msg.sender, block.chainid); + } + function destroy() public { selfdestruct(payable(msg.sender)); } - function assertError() public pure{ + function assertError() public pure { require(false, "Assert Error Message"); } - function customError() public pure{ - revert MyCustomError(5, "Value is too low"); + function customError() public pure { + revert MyCustomError(5, "Value is too low"); } function verifyArchCallToRandomSource(uint64 height) public view returns (bytes32) { @@ -79,6 +112,11 @@ contract Storage { return output; } + function emitVerifyArchCallToRandomSource(uint64 height) public { + bytes32 output = verifyArchCallToRandomSource(height); + emit VerifyArchCallToRandomSource(msg.sender, output); + } + function verifyArchCallToRevertibleRandom() public view returns (uint64) { (bool ok, bytes memory data) = cadenceArch.staticcall(abi.encodeWithSignature("revertibleRandom()")); require(ok, "unsuccessful call to arch"); @@ -86,17 +124,32 @@ contract Storage { return output; } - function verifyArchCallToFlowBlockHeight() public view returns (uint64){ + function emitVerifyArchCallToRevertibleRandom() public { + uint64 output = verifyArchCallToRevertibleRandom(); + emit VerifyArchCallToRevertibleRandom(msg.sender, output); + } + + function verifyArchCallToFlowBlockHeight() public view returns (uint64) { (bool ok, bytes memory data) = cadenceArch.staticcall(abi.encodeWithSignature("flowBlockHeight()")); require(ok, "unsuccessful call to arch "); uint64 output = abi.decode(data, (uint64)); return output; } - function verifyArchCallToVerifyCOAOwnershipProof(address arg0 , bytes32 arg1 , bytes memory arg2 ) public view returns (bool){ + function emitVerifyArchCallToFlowBlockHeight() public { + uint64 output = verifyArchCallToFlowBlockHeight(); + emit VerifyArchCallToFlowBlockHeight(msg.sender, output); + } + + function verifyArchCallToVerifyCOAOwnershipProof(address arg0, bytes32 arg1, bytes memory arg2) public view returns (bool) { (bool ok, bytes memory data) = cadenceArch.staticcall(abi.encodeWithSignature("verifyCOAOwnershipProof(address,bytes32,bytes)", arg0, arg1, arg2)); require(ok, "unsuccessful call to arch"); bool output = abi.decode(data, (bool)); return output; } -} + + function emitVerifyArchCallToVerifyCOAOwnershipProof(address arg0, bytes32 arg1, bytes memory arg2) public { + bool output = verifyArchCallToVerifyCOAOwnershipProof(arg0, arg1, arg2); + emit VerifyArchCallToVerifyCOAOwnershipProof(msg.sender, output); + } +} \ No newline at end of file diff --git a/tests/fixtures/storageABI.json b/tests/fixtures/storageABI.json index 9afce605..2672475c 100644 --- a/tests/fixtures/storageABI.json +++ b/tests/fixtures/storageABI.json @@ -20,6 +20,63 @@ "name": "MyCustomError", "type": "error" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "caller", + "type": "address" + }, + { + "indexed": false, + "internalType": "bytes32", + "name": "hash", + "type": "bytes32" + } + ], + "name": "BlockHash", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "caller", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "BlockNumber", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "caller", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "BlockTime", + "type": "event" + }, { "anonymous": false, "inputs": [ @@ -51,6 +108,25 @@ "name": "Calculated", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "caller", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "ChainID", + "type": "event" + }, { "anonymous": false, "inputs": [ @@ -70,6 +146,120 @@ "name": "NewStore", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "caller", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Random", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "caller", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Retrieved", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "caller", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint64", + "name": "output", + "type": "uint64" + } + ], + "name": "VerifyArchCallToFlowBlockHeight", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "caller", + "type": "address" + }, + { + "indexed": false, + "internalType": "bytes32", + "name": "output", + "type": "bytes32" + } + ], + "name": "VerifyArchCallToRandomSource", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "caller", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint64", + "name": "output", + "type": "uint64" + } + ], + "name": "VerifyArchCallToRevertibleRandom", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "caller", + "type": "address" + }, + { + "indexed": false, + "internalType": "bool", + "name": "output", + "type": "bool" + } + ], + "name": "VerifyArchCallToVerifyCOAOwnershipProof", + "type": "event" + }, { "inputs": [], "name": "assertError", @@ -162,6 +352,104 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "num", + "type": "uint256" + } + ], + "name": "emitBlockHash", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "emitBlockNumber", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "emitBlockTime", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "emitChainID", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "emitRandom", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "emitRetrieve", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "emitVerifyArchCallToFlowBlockHeight", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "height", + "type": "uint64" + } + ], + "name": "emitVerifyArchCallToRandomSource", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "emitVerifyArchCallToRevertibleRandom", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "arg0", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "arg1", + "type": "bytes32" + }, + { + "internalType": "bytes", + "name": "arg2", + "type": "bytes" + } + ], + "name": "emitVerifyArchCallToVerifyCOAOwnershipProof", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [], "name": "random", diff --git a/tests/go.mod b/tests/go.mod index da7a4bbe..40d04a6a 100644 --- a/tests/go.mod +++ b/tests/go.mod @@ -6,9 +6,9 @@ require ( github.com/goccy/go-json v0.10.2 github.com/onflow/cadence v1.0.0-preview.52 github.com/onflow/crypto v0.25.2 - github.com/onflow/flow-emulator v1.0.0 + github.com/onflow/flow-emulator v1.0.0-preview.42 github.com/onflow/flow-evm-gateway v0.0.0-20240201154855-4d4d3d3f19c7 - github.com/onflow/flow-go v0.37.10 + github.com/onflow/flow-go v0.37.10-util-ensure-checkpoint-exists.0.20240914104351-c2d9833c3357 github.com/onflow/flow-go-sdk v1.0.0-preview.56 github.com/onflow/go-ethereum v1.14.7 github.com/rs/zerolog v1.31.0 @@ -153,7 +153,7 @@ require ( github.com/onflow/flow-ft/lib/go/templates v1.0.0 // indirect github.com/onflow/flow-nft/lib/go/contracts v1.2.1 // indirect github.com/onflow/flow-nft/lib/go/templates v1.2.0 // indirect - github.com/onflow/flow/protobuf/go/flow v0.4.6 // indirect + github.com/onflow/flow/protobuf/go/flow v0.4.7 // indirect github.com/onflow/sdks v0.6.0-preview.1 // indirect github.com/onflow/wal v1.0.2 // indirect github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect diff --git a/tests/go.sum b/tests/go.sum index 6d4239c4..a426d521 100644 --- a/tests/go.sum +++ b/tests/go.sum @@ -2087,14 +2087,14 @@ github.com/onflow/flow-core-contracts/lib/go/contracts v1.3.1 h1:q9tXLIALwQ76bO4 github.com/onflow/flow-core-contracts/lib/go/contracts v1.3.1/go.mod h1:u/mkP/B+PbV33tEG3qfkhhBlydSvAKxfLZSfB4lsJHg= github.com/onflow/flow-core-contracts/lib/go/templates v1.3.1 h1:FfhMBAb78p6VAWkJ+iqdKLErGQVQgxk5w6DP5ZruWX8= github.com/onflow/flow-core-contracts/lib/go/templates v1.3.1/go.mod h1:NgbMOYnMh0GN48VsNKZuiwK7uyk38Wyo8jN9+C9QE30= -github.com/onflow/flow-emulator v1.0.0 h1:CCE9mFUYidb4YPQWFSBHzcBGggs5bXVqIh02wF2wRr0= -github.com/onflow/flow-emulator v1.0.0/go.mod h1:sHbe9e1RG7Y6LA/dFyLEoBnKyjJ4iHeOdkXIobMjjrE= +github.com/onflow/flow-emulator v1.0.0-preview.42 h1:2uMsoKo7wfZOd50GanR7wIoRxpDFErV17wt6/YaeVRo= +github.com/onflow/flow-emulator v1.0.0-preview.42/go.mod h1:qCT9cAsrtqKHjTmEujihHPH2RfEiL6wNbMqCbmN7HMo= github.com/onflow/flow-ft/lib/go/contracts v1.0.0 h1:mToacZ5NWqtlWwk/7RgIl/jeKB/Sy/tIXdw90yKHcV0= github.com/onflow/flow-ft/lib/go/contracts v1.0.0/go.mod h1:PwsL8fC81cjnUnTfmyL/HOIyHnyaw/JA474Wfj2tl6A= github.com/onflow/flow-ft/lib/go/templates v1.0.0 h1:6cMS/lUJJ17HjKBfMO/eh0GGvnpElPgBXx7h5aoWJhs= github.com/onflow/flow-ft/lib/go/templates v1.0.0/go.mod h1:uQ8XFqmMK2jxyBSVrmyuwdWjTEb+6zGjRYotfDJ5pAE= -github.com/onflow/flow-go v0.37.10 h1:Nz2Gp63+0ubb9FuQaEZgCsXNXM5WsXq/j0ukC74N5Vw= -github.com/onflow/flow-go v0.37.10/go.mod h1:bfOCsCk0v1J93vXd+zrYkCmRIVOaL9oAXvNFWgVOujE= +github.com/onflow/flow-go v0.37.10-util-ensure-checkpoint-exists.0.20240914104351-c2d9833c3357 h1:7gJ5RVKZEsUqPSKglpMXUBn+hceJ1cd/PsmLVsd5uzQ= +github.com/onflow/flow-go v0.37.10-util-ensure-checkpoint-exists.0.20240914104351-c2d9833c3357/go.mod h1:Gdqw1ptnAUuB0izif88PWMK8abe655Hr8iEkXXuUJl4= github.com/onflow/flow-go-sdk v1.0.0-M1/go.mod h1:TDW0MNuCs4SvqYRUzkbRnRmHQL1h4X8wURsCw9P9beo= github.com/onflow/flow-go-sdk v1.0.0-preview.56 h1:ZnFznUXI1V8iZ+cKxoJRIeQwJTHItriKpnoKf8hFFso= github.com/onflow/flow-go-sdk v1.0.0-preview.56/go.mod h1:rBRNboXaTprn7M0MeO6/R1bxNpctbrx66I2FLp0V6fM= @@ -2103,8 +2103,8 @@ github.com/onflow/flow-nft/lib/go/contracts v1.2.1/go.mod h1:2gpbza+uzs1k7x31hkp github.com/onflow/flow-nft/lib/go/templates v1.2.0 h1:JSQyh9rg0RC+D1930BiRXN8lrtMs+ubVMK6aQPon6Yc= github.com/onflow/flow-nft/lib/go/templates v1.2.0/go.mod h1:p+2hRvtjLUR3MW1NsoJe5Gqgr2eeH49QB6+s6ze00w0= github.com/onflow/flow/protobuf/go/flow v0.3.2-0.20231121210617-52ee94b830c2/go.mod h1:NA2pX2nw8zuaxfKphhKsk00kWLwfd+tv8mS23YXO4Sk= -github.com/onflow/flow/protobuf/go/flow v0.4.6 h1:KE/CsRVfyG5lGBtm1aNcjojMciQyS5GfPF3ixOWRfi0= -github.com/onflow/flow/protobuf/go/flow v0.4.6/go.mod h1:NA2pX2nw8zuaxfKphhKsk00kWLwfd+tv8mS23YXO4Sk= +github.com/onflow/flow/protobuf/go/flow v0.4.7 h1:iP6DFx4wZ3ETORsyeqzHu7neFT3d1CXF6wdK+AOOjmc= +github.com/onflow/flow/protobuf/go/flow v0.4.7/go.mod h1:NA2pX2nw8zuaxfKphhKsk00kWLwfd+tv8mS23YXO4Sk= github.com/onflow/go-ethereum v1.14.7 h1:gg3awYqI02e3AypRdpJKEvNTJ6kz/OhAqRti0h54Wlc= github.com/onflow/go-ethereum v1.14.7/go.mod h1:zV14QLrXyYu5ucvcwHUA0r6UaqveqbXaehAVQJlSW+I= github.com/onflow/sdks v0.5.1-0.20230912225508-b35402f12bba/go.mod h1:F0dj0EyHC55kknLkeD10js4mo14yTdMotnWMslPirrU= diff --git a/tests/helpers.go b/tests/helpers.go index 91eb05aa..9a7dae83 100644 --- a/tests/helpers.go +++ b/tests/helpers.go @@ -62,7 +62,10 @@ func testLogWriter() io.Writer { return zerolog.Nop() } - return zerolog.NewConsoleWriter() + writer := zerolog.NewConsoleWriter() + zerolog.TimeFieldFormat = time.RFC3339Nano + writer.TimeFormat = "04:05.0000" + return writer } func startEmulator(createTestAccounts bool) (*server.EmulatorServer, error) { @@ -76,20 +79,21 @@ func startEmulator(createTestAccounts bool) (*server.EmulatorServer, error) { return nil, err } - log := logger.With().Timestamp().Str("component", "emulator").Logger().Level(zerolog.DebugLevel) + log := zerolog.New(testLogWriter()).With().Timestamp().Str("component", "emulator").Logger().Level(zerolog.DebugLevel) if logOutput == "false" { log = zerolog.Nop() } srv := server.NewEmulatorServer(&log, &server.Config{ - ServicePrivateKey: pkey, - ServiceKeySigAlgo: sigAlgo, - ServiceKeyHashAlgo: hashAlgo, - GenesisTokenSupply: genesisToken, - WithContracts: true, - Host: "localhost", - TransactionExpiry: 10, - TransactionMaxGasLimit: flow.DefaultMaxTransactionGasLimit, + ServicePrivateKey: pkey, + ServiceKeySigAlgo: sigAlgo, + ServiceKeyHashAlgo: hashAlgo, + GenesisTokenSupply: genesisToken, + WithContracts: true, + Host: "localhost", + TransactionExpiry: 10, + TransactionMaxGasLimit: flow.DefaultMaxTransactionGasLimit, + SkipTransactionValidation: true, }) go func() { @@ -332,7 +336,8 @@ func evmSign( signer *ecdsa.PrivateKey, nonce uint64, to *common.Address, - data []byte) ([]byte, common.Hash, error) { + data []byte, +) ([]byte, common.Hash, error) { gasPrice := big.NewInt(0) evmTx := types.NewTx(&types.LegacyTx{Nonce: nonce, To: to, Value: weiValue, Gas: gasLimit, GasPrice: gasPrice, Data: data}) diff --git a/tests/state_integration_test.go b/tests/state_integration_test.go new file mode 100644 index 00000000..f7d46921 --- /dev/null +++ b/tests/state_integration_test.go @@ -0,0 +1,170 @@ +package tests + +import ( + "context" + "math/big" + "testing" + "time" + + "github.com/onflow/flow-go/fvm/evm/types" + flowGo "github.com/onflow/flow-go/model/flow" + "github.com/onflow/go-ethereum/common" + "github.com/onflow/go-ethereum/crypto" + "github.com/rs/zerolog" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/onflow/flow-evm-gateway/bootstrap" + "github.com/onflow/flow-evm-gateway/config" + "github.com/onflow/flow-evm-gateway/services/state" + "github.com/onflow/flow-evm-gateway/storage/pebble" +) + +func Test_StateExecution_Transfers(t *testing.T) { + srv, err := startEmulator(true) + require.NoError(t, err) + + emu := srv.Emulator() + service := emu.ServiceKey() + require.NoError(t, err) + + ctx, cancel := context.WithCancel(context.Background()) + defer func() { + cancel() + }() + + cfg := &config.Config{ + InitCadenceHeight: 0, + DatabaseDir: t.TempDir(), + FlowNetworkID: flowGo.Emulator, + HeartbeatInterval: 50, + EVMNetworkID: types.FlowEVMPreviewNetChainID, + AccessNodeHost: "localhost:3569", + Coinbase: common.HexToAddress(eoaTestAddress), + COAAddress: service.Address, + COAKey: service.PrivateKey, + CreateCOAResource: true, + GasPrice: new(big.Int).SetUint64(0), + LogLevel: zerolog.DebugLevel, + LogWriter: zerolog.NewConsoleWriter(), + } + + b, err := bootstrap.New(cfg) + require.NoError(t, err) + + require.NoError(t, b.StartStateIndex(ctx)) + require.NoError(t, b.StartAPIServer(ctx)) + require.NoError(t, b.StartEventIngestion(ctx)) + + blocks := b.Storages.Blocks + receipts := b.Storages.Receipts + store := b.Storages.Storage + requester := b.EVMClient + + latest, err := blocks.LatestIndexedHeight() + require.NoError(t, err) + + block, err := blocks.GetByHeight(latest) + require.NoError(t, err) + + // wait for emulator to boot + time.Sleep(time.Second) + + height0 := latest + + registers := pebble.NewRegister(store, height0, nil) + st, err := state.NewBlockState(block, registers, cfg.FlowNetworkID, blocks, receipts, logger) + require.NoError(t, err) + + testAddr := common.HexToAddress("55253ed90B70b96C73092D8680915aaF50081194") + eoaKey, err := crypto.HexToECDSA(eoaTestPrivateKey) + + balance := st.GetBalance(testAddr) + assert.Equal(t, uint64(0), balance.Uint64()) + + amount := big.NewInt(1) + nonce := uint64(0) + evmTx, _, err := evmSign(amount, 21000, eoaKey, nonce, &testAddr, nil) + require.NoError(t, err) + + hash, err := requester.SendRawTransaction(ctx, evmTx) + require.NoError(t, err) + require.NotEmpty(t, hash) + + // wait for new block event + time.Sleep(time.Second) + latest, err = blocks.LatestIndexedHeight() + require.NoError(t, err) + + block, err = blocks.GetByHeight(latest) + require.NoError(t, err) + + height1 := latest + amount1 := amount.Uint64() + + registers = pebble.NewRegister(store, height1, nil) + st, err = state.NewBlockState(block, registers, cfg.FlowNetworkID, blocks, receipts, logger) + require.NoError(t, err) + + balance = st.GetBalance(testAddr) + assert.Equal(t, amount.Uint64(), balance.Uint64()) + + amount2 := big.NewInt(2) + nonce++ + evmTx, _, err = evmSign(amount2, 21000, eoaKey, nonce, &testAddr, nil) + require.NoError(t, err) + + hash, err = requester.SendRawTransaction(ctx, evmTx) + require.NoError(t, err) + require.NotEmpty(t, hash) + + // wait for new block event, todo replace with better method + time.Sleep(time.Second) + latest, err = blocks.LatestIndexedHeight() + require.NoError(t, err) + + block, err = blocks.GetByHeight(latest) + require.NoError(t, err) + + height2 := latest + registers = pebble.NewRegister(store, height2, nil) + st, err = state.NewBlockState(block, registers, cfg.FlowNetworkID, blocks, receipts, logger) + require.NoError(t, err) + + balance = st.GetBalance(testAddr) + assert.Equal(t, amount.Uint64()+amount2.Uint64(), balance.Uint64()) + + // read balance at historic heights + // height 0 + block, err = blocks.GetByHeight(height0) + require.NoError(t, err) + + registers = pebble.NewRegister(store, height0, nil) + st, err = state.NewBlockState(block, registers, cfg.FlowNetworkID, blocks, receipts, logger) + require.NoError(t, err) + + balance = st.GetBalance(testAddr) + assert.Equal(t, uint64(0), balance.Uint64()) + + // height 1 + block, err = blocks.GetByHeight(height1) + require.NoError(t, err) + + registers = pebble.NewRegister(store, height1, nil) + st, err = state.NewBlockState(block, registers, cfg.FlowNetworkID, blocks, receipts, logger) + require.NoError(t, err) + + balance = st.GetBalance(testAddr) + assert.Equal(t, amount1, balance.Uint64()) + + // height 2 + block, err = blocks.GetByHeight(height2) + require.NoError(t, err) + + registers = pebble.NewRegister(store, height2, nil) + st, err = state.NewBlockState(block, registers, cfg.FlowNetworkID, blocks, receipts, logger) + require.NoError(t, err) + + balance = st.GetBalance(testAddr) + assert.Equal(t, amount1+amount2.Uint64(), balance.Uint64()) +} diff --git a/tests/web3js/build_evm_state_test.js b/tests/web3js/build_evm_state_test.js index 8c510bc6..dab17d59 100644 --- a/tests/web3js/build_evm_state_test.js +++ b/tests/web3js/build_evm_state_test.js @@ -155,14 +155,14 @@ it('should handle a large number of EVM interactions', async () => { gas: 55_000, gasPrice: conf.minGasPrice }, latest) - assert.equal(estimatedGas, 29292n) + assert.equal(estimatedGas, 29338n) // Add calls to verify correctness of eth_getCode on historical heights let code = await web3.eth.getCode(contractAddress, 82n) assert.equal(code, '0x') code = await web3.eth.getCode(contractAddress, latest) - assert.lengthOf(code, 9806) + assert.lengthOf(code, 12180) // Add calls to verify correctness of eth_call on historical heights let callRetrieve = await deployed.contract.methods.retrieve().encodeABI() @@ -173,140 +173,7 @@ it('should handle a large number of EVM interactions', async () => { let storedNumber = web3.eth.abi.decodeParameter('uint256', result) assert.isTrue(storedNumber != 1337n) // this is the initial value - // 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, - ) -}) +}).timeout(180*1000) function randomItem(items) { return items[Math.floor(Math.random() * items.length)] diff --git a/tests/web3js/cadence_arch_env_test.js b/tests/web3js/cadence_arch_env_test.js new file mode 100644 index 00000000..8f9f5bc6 --- /dev/null +++ b/tests/web3js/cadence_arch_env_test.js @@ -0,0 +1,158 @@ +const utils = require('web3-utils') +const { assert } = require('chai') +const conf = require('./config') +const helpers = require('./helpers') +const web3 = conf.web3 + +// this test calls different environment and cadenc arch functions. It uses view +// function to call and return the value which it compares with the block on the network, +// behind the scene these causes the local client to make such a call and return the value +// and this test makes sure the on-chain data is same as local-index data. Secondly, this +// test also submits a transaction that emits the result, which checks the local state +// re-execution of transaction, and makes sure both receipt matches (remote and local), +// this in turn tests the local state re-execution. + +describe('calls cadence arch functions and block environment functions', function () { + + async function testEmitTx(method) { + let res = await helpers.signAndSend({ + from: conf.eoa.address, + to: contractAddress, + data: method.encodeABI(), + value: '0', + gasPrice: conf.minGasPrice, + }) + assert.equal(res.receipt.status, conf.successStatus) + assert.equal(res.receipt.logs.length, 1) + + return res + } + + async function testCall(method) { + let block = await web3.eth.getBlock('latest') + + let value = await web3.eth.call({ + to: contractAddress, + data: method.encodeABI() + }, block.number) + + assert.isAbove(value.length, 0) + + return { + value: value, + block: block, + } + } + + var methods + var contractAddress + + before(async function() { + let deployed = await helpers.deployContract('storage') + contractAddress = deployed.receipt.contractAddress + methods = deployed.contract.methods + }) + + it('calls blockNumber', async () => { + await testEmitTx(methods.emitBlockNumber()) + + let res = await testCall(methods.blockNumber()) + // todo eth calls are executed at the provided block height, but at that height + // block environment functions (number, hash etc), will already point to the block proposal + // which is the next block, not the block provided by height, discuss this problem! + assert.equal( + web3.eth.abi.decodeParameter('uint256', res.value), + res.block.number+1n, + ) + }) + + it('calls blockTime', async function() { + await testEmitTx(methods.emitBlockTime()) + + let res = await testCall(methods.blockTime()) + + // todo eth calls are executed at the provided block height, but at that height + // block environment functions (number, hash etc), will already point to the block proposal + // which is the next block, not the block provided by height, discuss this problem! + let prev = await web3.eth.getBlock(res.block.number) + + assert.equal( + web3.eth.abi.decodeParameter('uint', res.value).toString(), + (prev.timestamp+1n).toString(), // investigate why timestamp is increasing by 1 + ) + }) + + it('calls blockHash', async function() { + let b = await web3.eth.getBlock('latest') + + await testEmitTx(methods.emitBlockHash(b.number)) + + // todo eth calls are executed at the provided block height, but at that height + // block environment functions (number, hash etc), will already point to the block proposal + // which is the next block, not the block provided by height, discuss this problem! + let res = await testCall(methods.blockHash(b.number+1n)) + assert.equal( + web3.eth.abi.decodeParameter('bytes32', res.value).toString(), + res.block.hash.toString(), + ) + }) + + it('calls random', async function() { + await testEmitTx(methods.emitRandom()) + + let res = await testCall(methods.random()) + assert.isNotEmpty(web3.eth.abi.decodeParameter('uint256', res.value).toString()) + }) + + it('calls chainID', async function() { + await testEmitTx(methods.emitChainID()) + await testCall(methods.chainID()) + }) + + it('calls verifyArchCallToFlowBlockHeight', async function() { + await testEmitTx(methods.emitVerifyArchCallToFlowBlockHeight()) + + let res = await testCall(methods.verifyArchCallToFlowBlockHeight()) + assert.equal( + web3.eth.abi.decodeParameter('uint64', res.value), + res.block.number, + ) + }) + + it('calls verifyArchCallToRandomSource', async function() { + await testEmitTx(methods.emitVerifyArchCallToRandomSource(1)) + + let res = await testCall(methods.verifyArchCallToRandomSource(1)) + assert.notEqual( + res.value, + '0x0000000000000000000000000000000000000000000000000000000000000000' + ) + assert.lengthOf(res.value, 66) + }) + + + it('calls verifyArchCallToRevertibleRandom', async function() { + await testEmitTx(methods.emitVerifyArchCallToRevertibleRandom()) + + let res = await testCall(methods.verifyArchCallToRevertibleRandom()) + assert.notEqual( + res.value, + '0x0000000000000000000000000000000000000000000000000000000000000000' + ) + assert.lengthOf(res.value, 66) + }) + + it('calls verifyArchCallToVerifyCOAOwnershipProof', async function() { + let tx = await web3.eth.getTransactionFromBlock(conf.startBlockHeight, 1) + let bytes = web3.utils.hexToBytes('f853c18088f8d6e0586b0a20c78365766df842b840b90448f4591df2639873be2914c5560149318b7e2fcf160f7bb8ed13cfd97be2f54e6889606f18e50b2c37308386f840e03a9fff915f57b2164cba27f0206a95') + let addr = '0x1bacdb569847f31ade07e83d6bb7cefba2b9290b35d5c2964663215e73519cff' + + await testEmitTx(methods.emitVerifyArchCallToVerifyCOAOwnershipProof(tx.to, addr, bytes)) + + let res = await testCall(methods.verifyArchCallToVerifyCOAOwnershipProof(tx.to, addr, bytes)) + assert.equal( + web3.eth.abi.decodeParameter('bool', res.value), + false, + ) + }) +}) \ No newline at end of file diff --git a/tests/web3js/eth_deploy_contract_and_interact_test.js b/tests/web3js/eth_deploy_contract_and_interact_test.js index 4fcbfcb1..53fb58ee 100644 --- a/tests/web3js/eth_deploy_contract_and_interact_test.js +++ b/tests/web3js/eth_deploy_contract_and_interact_test.js @@ -18,7 +18,7 @@ it('deploy contract and interact', async () => { assert.equal(rcp.contractAddress, contractAddress) assert.equal(rcp.status, conf.successStatus) assert.isUndefined(rcp.to) - assert.equal(rcp.gasUsed, 1130512n) + assert.equal(rcp.gasUsed, 1387107n) assert.equal(rcp.gasUsed, rcp.cumulativeGasUsed) // check if latest block contains the deploy results diff --git a/tests/web3js/eth_filter_endpoints_test.js b/tests/web3js/eth_filter_endpoints_test.js index 4efcdafa..aae68e73 100644 --- a/tests/web3js/eth_filter_endpoints_test.js +++ b/tests/web3js/eth_filter_endpoints_test.js @@ -361,7 +361,7 @@ describe('eth_getFilterChanges', async () => { assert.equal(txHashes[0], res.receipt.transactionHash) assert.equal( txHashes[1], - '0xb1b9deb629374d7c6df6becb7011282c8b733922b664a74ea9cd5bcb333d193e' + '0x337fca3f04eed2a5abd38dea6856848ace94102ad6c9a77ed288c8ac93135ce4' ) }) @@ -414,12 +414,12 @@ describe('eth_getFilterChanges', async () => { from: '0x0000000000000000000000030000000000000000', gas: '0x5b04', gasPrice: '0x0', - hash: '0x71201dbf66271cedb6e87a5364b2cb84f6170e282f2b3f676196687bdf4babe0', + hash: '0x4c2aa69a0080917a60d2f9258db77de5abaf298b8aefbcc8ae4bb45ef80c35b5', input: '0x', nonce: '0x9', to: '0x658Bdf435d810C91414eC09147DAA6DB62406379', transactionIndex: '0x1', - value: '0x388fb0', + value: '0x38aa0e', type: '0x0', chainId: '0x286', v: '0xff', diff --git a/tests/web3js/eth_revert_reason_test.js b/tests/web3js/eth_revert_reason_test.js index 46c77631..e31818e6 100644 --- a/tests/web3js/eth_revert_reason_test.js +++ b/tests/web3js/eth_revert_reason_test.js @@ -41,11 +41,7 @@ it('store revertReason field in transaction receipts', async () => { ) assert.equal(200, response.status) - let latestHeight = await web3.eth.getBlockNumber() - let block = await web3.eth.getBlock(latestHeight) - assert.equal(block.number, 4n) - - let revertedTx = await web3.eth.getTransactionFromBlock(latestHeight, 0) + let revertedTx = await web3.eth.getTransaction(signedTx.transactionHash) // Give some time to the engine to ingest the latest transaction await new Promise(res => setTimeout(res, 1500)) rcp = await helpers.callRPCMethod( @@ -75,11 +71,7 @@ it('store revertReason field in transaction receipts', async () => { ) assert.equal(200, response.status) - latestHeight = await web3.eth.getBlockNumber() - block = await web3.eth.getBlock(latestHeight) - assert.equal(block.number, 5n) - - revertedTx = await web3.eth.getTransactionFromBlock(latestHeight, 0) + revertedTx = await web3.eth.getTransaction(signedTx.transactionHash) // Give some time to the engine to ingest the latest transaction await new Promise(res => setTimeout(res, 1500)) rcp = await helpers.callRPCMethod( diff --git a/tests/web3js/eth_transaction_type_fees_test.js b/tests/web3js/eth_transaction_type_fees_test.js index d56a57ac..ad7b443f 100644 --- a/tests/web3js/eth_transaction_type_fees_test.js +++ b/tests/web3js/eth_transaction_type_fees_test.js @@ -21,7 +21,7 @@ before(async () => { assert.equal(rcp.contractAddress, contractAddress) assert.equal(rcp.status, conf.successStatus) assert.isUndefined(rcp.to) - assert.equal(rcp.gasUsed, 1130512n) + assert.equal(rcp.gasUsed, 1387107n) assert.equal(rcp.gasUsed, rcp.cumulativeGasUsed) })