diff --git a/api/api.go b/api/api.go index d40c03ed5..ba12bf34e 100644 --- a/api/api.go +++ b/api/api.go @@ -165,7 +165,6 @@ func (b *BlockChainAPI) GetBalance( balance, err := b.evm.GetBalance(ctx, address, cadenceHeight) if err != nil { - b.logger.Error().Err(err).Msg("failed to get balance") return handleError[*hexutil.Big](b.logger, err) } @@ -427,6 +426,7 @@ func (b *BlockChainAPI) Call( res, err := b.evm.Call(ctx, tx, from, cadenceHeight) if err != nil { + // we debug output this error because the execution error is related to user input b.logger.Debug().Err(err).Msg("failed to execute call") return nil, err } diff --git a/bootstrap/bootstrap.go b/bootstrap/bootstrap.go index 8e1848092..4cc596d1e 100644 --- a/bootstrap/bootstrap.go +++ b/bootstrap/bootstrap.go @@ -57,10 +57,33 @@ func Start(ctx context.Context, cfg *config.Config) error { } } + // create access client with cross-spork capabilities + currentSporkClient, err := grpc.NewClient(cfg.AccessNodeHost) + if err != nil { + return fmt.Errorf("failed to create client connection for host: %s, with error: %w", cfg.AccessNodeHost, err) + } + + // if we provided access node previous spork hosts add them to the client + pastSporkClients := make([]access.Client, len(cfg.AccessNodePreviousSporkHosts)) + for i, host := range cfg.AccessNodePreviousSporkHosts { + grpcClient, err := grpc.NewClient(host) + if err != nil { + return fmt.Errorf("failed to create client connection for host: %s, with error: %w", host, err) + } + + pastSporkClients[i] = grpcClient + } + + client, err := requester.NewCrossSporkClient(currentSporkClient, pastSporkClients, logger) + if err != nil { + return err + } + go func() { err := startServer( ctx, cfg, + client, blocks, transactions, receipts, @@ -79,6 +102,7 @@ func Start(ctx context.Context, cfg *config.Config) error { err = startIngestion( ctx, cfg, + client, blocks, transactions, receipts, @@ -98,6 +122,7 @@ func Start(ctx context.Context, cfg *config.Config) error { func startIngestion( ctx context.Context, cfg *config.Config, + client *requester.CrossSporkClient, blocks storage.BlockIndexer, transactions storage.TransactionIndexer, receipts storage.ReceiptIndexer, @@ -109,27 +134,6 @@ func startIngestion( ) error { logger.Info().Msg("starting up event ingestion") - currentSporkClient, err := grpc.NewClient(cfg.AccessNodeHost) - if err != nil { - return fmt.Errorf("failed to create client connection for host: %s, with error: %w", cfg.AccessNodeHost, err) - } - - // if we provided access node previous spork hosts add them to the client - pastSporkClients := make([]access.Client, len(cfg.AccessNodePreviousSporkHosts)) - for i, host := range cfg.AccessNodePreviousSporkHosts { - grpcClient, err := grpc.NewClient(host) - if err != nil { - return fmt.Errorf("failed to create client connection for host: %s, with error: %w", host, err) - } - - pastSporkClients[i] = grpcClient - } - - client, err := requester.NewCrossSporkClient(currentSporkClient, pastSporkClients, logger) - if err != nil { - return err - } - blk, err := client.GetLatestBlock(context.Background(), false) if err != nil { return fmt.Errorf("failed to get latest cadence block: %w", err) @@ -147,8 +151,8 @@ func startIngestion( } logger.Info(). - Uint64("start-height", latestCadenceHeight). - Uint64("latest-network-height", blk.Height). + Uint64("start-cadence-height", latestCadenceHeight). + Uint64("latest-cadence-height", blk.Height). Uint64("missed-heights", blk.Height-latestCadenceHeight). Msg("indexing cadence height information") @@ -184,6 +188,7 @@ func startIngestion( func startServer( ctx context.Context, cfg *config.Config, + client *requester.CrossSporkClient, blocks storage.BlockIndexer, transactions storage.TransactionIndexer, receipts storage.ReceiptIndexer, @@ -198,14 +203,10 @@ func startServer( srv := api.NewHTTPServer(l, rpc.DefaultHTTPTimeouts) - client, err := grpc.NewClient(cfg.AccessNodeHost) - if err != nil { - return err - } - // 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. var signer crypto.Signer + var err error switch { case cfg.COAKey != nil: signer, err = crypto.NewInMemorySigner(cfg.COAKey, crypto.SHA3_256) diff --git a/go.mod b/go.mod index 56ed1880b..b5607dc8c 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/hashicorp/golang-lru/v2 v2.0.7 github.com/onflow/cadence v1.0.0-preview.29 github.com/onflow/flow-go v0.35.5-0.20240517202625-55f862b45dfd - github.com/onflow/flow-go-sdk v1.0.0-preview.30 + github.com/onflow/flow-go-sdk v1.0.0-preview.30.0.20240523120036-f9d51677b347 github.com/onflow/go-ethereum v1.13.4 github.com/rs/cors v1.8.0 github.com/rs/zerolog v1.31.0 diff --git a/go.sum b/go.sum index 383adb2a4..24c83e34c 100644 --- a/go.sum +++ b/go.sum @@ -1002,7 +1002,7 @@ github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5 github.com/aws/aws-sdk-go-v2 v1.2.0/go.mod h1:zEQs02YRBw1DjK0PoJv3ygDYOFTre1ejlJWl8FwAuQo= github.com/aws/aws-sdk-go-v2 v1.21.2/go.mod h1:ErQhvNuEMhJjweavOYhxVkn2RUx7kQXVATHrjKtxIpM= github.com/aws/aws-sdk-go-v2 v1.23.1/go.mod h1:i1XDttT4rnf6vxc9AuskLc6s7XBee8rlLilKlc03uAA= -github.com/aws/aws-sdk-go-v2 v1.26.1 h1:5554eUqIYVWpU0YmeeYZ0wU64H2VLBs8TlhRB2L+EkA= +github.com/aws/aws-sdk-go-v2 v1.27.0 h1:7bZWKoXhzI+mMR/HjdMx8ZCC5+6fY0lS5tr0bbgiLlo= github.com/aws/aws-sdk-go-v2/config v1.1.1/go.mod h1:0XsVy9lBI/BCXm+2Tuvt39YmdHwS5unDQmxZOYe8F5Y= github.com/aws/aws-sdk-go-v2/config v1.18.45/go.mod h1:ZwDUgFnQgsazQTnWfeLWk5GjeqTQTL8lMkoE1UXzxdE= github.com/aws/aws-sdk-go-v2/config v1.25.5/go.mod h1:Bf4gDvy4ZcFIK0rqDu1wp9wrubNba2DojiPB2rt6nvI= @@ -1024,7 +1024,7 @@ github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2 h1:Ji0DY1x github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.0.2/go.mod h1:45MfaXZ0cNbeuT0KQ1XJylq8A6+OpVV2E5kvY/Kq+u8= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.37/go.mod h1:vBmDnwWXWxNPFRMmG2m/3MKOe+xEcMDo1tanpaWCcck= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.4/go.mod h1:aYCGNjyUCUelhofxlZyj63srdxWUSsBSGg5l6MCuXuE= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.7 h1:ogRAwT1/gxJBcSWDMZlgyFUM962F51A5CRhDLbxLdmo= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.9 h1:Wx0rlZoEJR7JwlSZcHnEa7CNjrSIyVxMFWGAaXy4fJY= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.7.0 h1:HWsM0YQWX76V6MOp07YuTYacm8k7h69ObJuw7Nck+og= github.com/aws/aws-sdk-go-v2/service/kms v1.26.3/go.mod h1:N3++/sLV97B8Zliz7KRqNcojOX7iMBZWKiuit5FKtH0= github.com/aws/aws-sdk-go-v2/service/route53 v1.1.1/go.mod h1:rLiOUrPLW/Er5kRcQ7NkwbjlijluLsrIbu/iyl35RO4= @@ -1885,8 +1885,8 @@ github.com/onflow/flow-ft/lib/go/templates v1.0.0/go.mod h1:uQ8XFqmMK2jxyBSVrmyu github.com/onflow/flow-go v0.35.5-0.20240517202625-55f862b45dfd h1:bSoQMARSC4dk6sQPv6SRkV7QvovKoNksTnydK9e5hL4= github.com/onflow/flow-go v0.35.5-0.20240517202625-55f862b45dfd/go.mod h1:5ysH9wggXlvJqbALEBZc2uNx6DIE+QsBoocuM8bSGC0= github.com/onflow/flow-go-sdk v1.0.0-M1/go.mod h1:TDW0MNuCs4SvqYRUzkbRnRmHQL1h4X8wURsCw9P9beo= -github.com/onflow/flow-go-sdk v1.0.0-preview.30 h1:62IwC7l8Uw1mxoZe7ewJII0HFHLUMsg04z1BW3JSEfM= -github.com/onflow/flow-go-sdk v1.0.0-preview.30/go.mod h1:PBIk3vLqU1aLdbWPw7ljRDmwSGLcsuk/ipL9eLMgWwc= +github.com/onflow/flow-go-sdk v1.0.0-preview.30.0.20240523120036-f9d51677b347 h1:qXJa8wp2aJLzDO5TVmMxaSHlrZ0/O/HMHzrpFUDP0eo= +github.com/onflow/flow-go-sdk v1.0.0-preview.30.0.20240523120036-f9d51677b347/go.mod h1:J4iKISX976mxV3ReTWiURG/ai50h61s2XJZ3YcK2lCg= github.com/onflow/flow-nft/lib/go/contracts v1.2.1 h1:woAAS5z651sDpi7ihAHll8NvRS9uFXIXkL6xR+bKFZY= github.com/onflow/flow-nft/lib/go/contracts v1.2.1/go.mod h1:2gpbza+uzs1k7x31hkpBPlggIRkI53Suo0n2AyA2HcE= github.com/onflow/flow-nft/lib/go/templates v1.2.0 h1:JSQyh9rg0RC+D1930BiRXN8lrtMs+ubVMK6aQPon6Yc= diff --git a/services/ingestion/engine.go b/services/ingestion/engine.go index 57f285b59..a193462cf 100644 --- a/services/ingestion/engine.go +++ b/services/ingestion/engine.go @@ -4,12 +4,13 @@ import ( "context" "fmt" - "github.com/onflow/flow-evm-gateway/models" - "github.com/onflow/flow-evm-gateway/storage" "github.com/onflow/flow-go/engine" "github.com/onflow/flow-go/fvm/evm/types" 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" ) var _ models.Engine = &Engine{} @@ -124,7 +125,7 @@ func (e *Engine) Run(ctx context.Context) error { // // Any error is unexpected and fatal. func (e *Engine) processEvents(events *models.CadenceEvents) error { - e.log.Debug(). + e.log.Info(). Uint64("cadence-height", events.CadenceHeight()). Int("cadence-event-length", events.Length()). Msg("received new cadence evm events") @@ -175,12 +176,17 @@ func (e *Engine) indexBlock(cadenceHeight uint64, block *types.Block) error { return fmt.Errorf("invalid block height, expected %d, got %d: %w", e.evmLastHeight.Load(), block.Height, err) } - h, _ := block.Hash() + blockHash, _ := block.Hash() + txHashes := make([]string, len(block.TransactionHashes)) + for i, t := range block.TransactionHashes { + txHashes[i] = t.Hex() + } e.log.Info(). - Str("hash", h.Hex()). + Str("hash", blockHash.Hex()). Uint64("evm-height", block.Height). + Uint64("cadence-height", cadenceHeight). Str("parent-hash", block.ParentBlockHash.String()). - Str("tx-hash", block.TransactionHashes[0].Hex()). // now we only have 1 tx per block + Strs("tx-hashes", txHashes). Msg("new evm block executed event") // todo should probably be batch in the same as bellow tx @@ -207,7 +213,6 @@ func (e *Engine) indexTransaction(tx models.Transaction, receipt *gethTypes.Rece Int("log-count", len(receipt.Logs)). Uint64("evm-height", receipt.BlockNumber.Uint64()). Uint("tx-index", receipt.TransactionIndex). - Str("receipt-tx-hash", receipt.TxHash.String()). Str("tx-hash", txHash.String()). Msg("ingesting new transaction executed event") diff --git a/services/ingestion/subscriber_test.go b/services/ingestion/subscriber_test.go index 7da43b516..4be9d0dd5 100644 --- a/services/ingestion/subscriber_test.go +++ b/services/ingestion/subscriber_test.go @@ -8,79 +8,13 @@ import ( "github.com/onflow/flow-evm-gateway/models" "github.com/onflow/flow-evm-gateway/services/requester" + "github.com/onflow/flow-evm-gateway/services/testutils" - "github.com/onflow/flow-go-sdk" - "github.com/onflow/flow-go-sdk/access/mocks" flowGo "github.com/onflow/flow-go/model/flow" - "github.com/onflow/flow-go/storage" "github.com/rs/zerolog" "github.com/stretchr/testify/require" ) -type mockClient struct { - *mocks.Client - getLatestBlockHeaderFunc func(context.Context, bool) (*flow.BlockHeader, error) - getBlockHeaderByHeightFunc func(context.Context, uint64) (*flow.BlockHeader, error) - subscribeEventsByBlockHeightFunc func(context.Context, uint64, flow.EventFilter, ...access.SubscribeOption) (<-chan flow.BlockEvents, <-chan error, error) -} - -func (c *mockClient) GetBlockHeaderByHeight(ctx context.Context, height uint64) (*flow.BlockHeader, error) { - return c.getBlockHeaderByHeightFunc(ctx, height) -} - -func (c *mockClient) GetLatestBlockHeader(ctx context.Context, sealed bool) (*flow.BlockHeader, error) { - return c.getLatestBlockHeaderFunc(ctx, sealed) -} - -func (c *mockClient) SubscribeEventsByBlockHeight( - ctx context.Context, - startHeight uint64, - filter flow.EventFilter, - opts ...access.SubscribeOption, -) (<-chan flow.BlockEvents, <-chan error, error) { - return c.subscribeEventsByBlockHeightFunc(ctx, startHeight, filter, opts...) -} - -func setupClient(startHeight uint64, endHeight uint64) access.Client { - return &mockClient{ - Client: &mocks.Client{}, - getLatestBlockHeaderFunc: func(ctx context.Context, sealed bool) (*flow.BlockHeader, error) { - return &flow.BlockHeader{ - Height: endHeight, - }, nil - }, - getBlockHeaderByHeightFunc: func(ctx context.Context, height uint64) (*flow.BlockHeader, error) { - if height < startHeight || height > endHeight { - return nil, storage.ErrNotFound - } - - return &flow.BlockHeader{ - Height: height, - }, nil - }, - subscribeEventsByBlockHeightFunc: func( - ctx context.Context, - startHeight uint64, - filter flow.EventFilter, - opts ...access.SubscribeOption, - ) (<-chan flow.BlockEvents, <-chan error, error) { - events := make(chan flow.BlockEvents) - - go func() { - defer close(events) - - for i := startHeight; i <= endHeight; i++ { - events <- flow.BlockEvents{ - Height: i, - } - } - }() - - return events, make(chan error), nil - }, - } -} - // this test simulates two previous sporks and current spork // the subscriber should start with spork1Client then proceed to // spork2Client and end with currentClient. @@ -89,10 +23,10 @@ func Test_Subscribing(t *testing.T) { const endHeight = 50 sporkClients := []access.Client{ - setupClient(1, 10), - setupClient(11, 20), + testutils.SetupClientForRange(1, 10), + testutils.SetupClientForRange(11, 20), } - currentClient := setupClient(21, endHeight) + currentClient := testutils.SetupClientForRange(21, endHeight) client, err := requester.NewCrossSporkClient(currentClient, sporkClients, zerolog.Nop()) require.NoError(t, err) diff --git a/services/requester/cross-spork_client.go b/services/requester/cross-spork_client.go index f33093247..92f42f6c0 100644 --- a/services/requester/cross-spork_client.go +++ b/services/requester/cross-spork_client.go @@ -2,16 +2,80 @@ package requester import ( "context" + "errors" "fmt" "github.com/onflow/cadence" "github.com/onflow/flow-go-sdk" "github.com/onflow/flow-go-sdk/access" "github.com/rs/zerolog" - "golang.org/x/exp/maps" "golang.org/x/exp/slices" ) +var ErrOutOfRange = errors.New("height is out of range for provided spork clients") + +type sporkClient struct { + firstHeight uint64 + lastHeight uint64 + client access.Client +} + +// contains checks if the provided height is withing the range of available heights +func (s *sporkClient) contains(height uint64) bool { + return height >= s.firstHeight && height <= s.lastHeight +} + +type sporkClients []*sporkClient + +// addSpork will add a new spork host defined by the first and last height boundary in that spork. +func (s *sporkClients) add(client access.Client) error { + header, err := client.GetLatestBlockHeader(context.Background(), true) + if err != nil { + return fmt.Errorf("could not get latest height using the spork client: %w", err) + } + + info, err := client.GetNodeVersionInfo(context.Background()) + if err != nil { + return fmt.Errorf("could not get node info using the spork client: %w", err) + } + + *s = append(*s, &sporkClient{ + firstHeight: info.NodeRootBlockHeight, + lastHeight: header.Height, + client: client, + }) + + // make sure clients are always sorted + slices.SortFunc(*s, func(a, b *sporkClient) int { + return int(a.firstHeight) - int(b.firstHeight) + }) + + return nil +} + +// get spork client that contains the height or nil if not found. +func (s *sporkClients) get(height uint64) access.Client { + for _, spork := range *s { + if spork.contains(height) { + return spork.client + } + } + + return nil +} + +// continuous checks if all the past spork clients create a continuous +// range of heights. +func (s *sporkClients) continuous() bool { + for i := 0; i < len(*s)-1; i++ { + if (*s)[i].lastHeight+1 != (*s)[i+1].firstHeight { + return false + } + } + + return true +} + // CrossSporkClient is a wrapper around the Flow AN client that can // access different AN APIs based on the height boundaries of the sporks. // @@ -21,10 +85,9 @@ import ( // Any API that supports cross-spork access must have a defined function // that shadows the original access Client function. type CrossSporkClient struct { - logger zerolog.Logger - // this map holds the last heights and clients for each spork - sporkClients map[uint64]access.Client - sporkBoundaries []uint64 + logger zerolog.Logger + sporkClients *sporkClients + currentSporkFirstHeight uint64 access.Client } @@ -35,91 +98,70 @@ func NewCrossSporkClient( pastSporks []access.Client, logger zerolog.Logger, ) (*CrossSporkClient, error) { - client := &CrossSporkClient{ - logger: logger, - sporkClients: make(map[uint64]access.Client), - Client: currentSpork, + info, err := currentSpork.GetNodeVersionInfo(context.Background()) + if err != nil { + return nil, fmt.Errorf("failed to get node version info: %w", err) } - for _, sporkClient := range pastSporks { - if err := client.addSpork(sporkClient); err != nil { + clients := &sporkClients{} + for _, c := range pastSporks { + if err := clients.add(c); err != nil { return nil, err } } - // create a descending list of block heights that represent boundaries - // of each spork, after crossing each height, we use a different client - heights := maps.Keys(client.sporkClients) - slices.Sort(heights) - slices.Reverse(heights) // make it descending - client.sporkBoundaries = heights - - return client, nil -} - -// addSpork will add a new spork host defined by the last height boundary in that spork. -func (c *CrossSporkClient) addSpork(client access.Client) error { - header, err := client.GetLatestBlockHeader(context.Background(), true) - if err != nil { - return fmt.Errorf("could not get latest height using the spork client: %w", err) + if !clients.continuous() { + return nil, fmt.Errorf("provided past-spork clients don't create a continuous range of heights") } - lastHeight := header.Height - - if _, ok := c.sporkClients[lastHeight]; ok { - return fmt.Errorf("provided last height already exists") - } - - c.sporkClients[lastHeight] = client - - c.logger.Info(). - Uint64("spork-boundary", lastHeight). - Msg("added spork specific client") - - return nil + return &CrossSporkClient{ + logger: logger, + currentSporkFirstHeight: info.NodeRootBlockHeight, + sporkClients: clients, + Client: currentSpork, + }, nil } // IsPastSpork will check if the provided height is contained in the previous sporks. func (c *CrossSporkClient) IsPastSpork(height uint64) bool { - return len(c.sporkBoundaries) > 0 && height <= c.sporkBoundaries[0] + return height < c.currentSporkFirstHeight } -// getClientForHeight returns the client for the given height. It starts by using the current spork client, -// then iteratively checks the upper height boundaries in descending order and returns the last client -// that still contains the given height within its upper height limit. If no client is found, it returns -// the current spork client. -// Please note that even if a client for provided height is found we don't guarantee the data being available -// because it still might not have access to the height provided, because there might be other sporks with -// lower height boundaries that we didn't configure for. -// This would result in the error when using the client to access such data. -func (c *CrossSporkClient) getClientForHeight(height uint64) access.Client { - - // start by using the current spork client, then iterate all the upper height boundaries - // and find the last client that still contains the height in its upper height limit - client := c.Client - for _, upperBound := range c.sporkBoundaries { - if upperBound >= height { - client = c.sporkClients[upperBound] - - c.logger.Debug(). - Uint64("spork-boundary", upperBound). - Msg("using previous spork client") - } +// getClientForHeight returns the client for the given height that contains the height range. +// +// If the height is not contained in any of the past spork clients we return an error. +// If the height is contained in the current spork client we return the current spork client, +// but that doesn't guarantee the height will be found, since the height might be bigger than the +// latest height in the current spork, which is not checked due to performance reasons. +func (c *CrossSporkClient) getClientForHeight(height uint64) (access.Client, error) { + if !c.IsPastSpork(height) { + return c.Client, nil + } + + client := c.sporkClients.get(height) + if client == nil { + return nil, ErrOutOfRange } - return client + c.logger.Debug(). + Uint64("requested-cadence-height", height). + Msg("using previous spork client") + + return client, nil } // GetLatestHeightForSpork will determine the spork client in which the provided height is contained // and then find the latest height in that spork. func (c *CrossSporkClient) GetLatestHeightForSpork(ctx context.Context, height uint64) (uint64, error) { - block, err := c. - getClientForHeight(height). - GetLatestBlockHeader(ctx, true) + client, err := c.getClientForHeight(height) if err != nil { return 0, err } + block, err := client.GetLatestBlockHeader(ctx, true) + if err != nil { + return 0, err + } return block.Height, nil } @@ -127,9 +169,11 @@ func (c *CrossSporkClient) GetBlockHeaderByHeight( ctx context.Context, height uint64, ) (*flow.BlockHeader, error) { - return c. - getClientForHeight(height). - GetBlockHeaderByHeight(ctx, height) + client, err := c.getClientForHeight(height) + if err != nil { + return nil, err + } + return client.GetBlockHeaderByHeight(ctx, height) } func (c *CrossSporkClient) ExecuteScriptAtBlockHeight( @@ -138,9 +182,11 @@ func (c *CrossSporkClient) ExecuteScriptAtBlockHeight( script []byte, arguments []cadence.Value, ) (cadence.Value, error) { - return c. - getClientForHeight(height). - ExecuteScriptAtBlockHeight(ctx, height, script, arguments) + client, err := c.getClientForHeight(height) + if err != nil { + return nil, err + } + return client.ExecuteScriptAtBlockHeight(ctx, height, script, arguments) } func (c *CrossSporkClient) SubscribeEventsByBlockHeight( @@ -149,7 +195,9 @@ func (c *CrossSporkClient) SubscribeEventsByBlockHeight( filter flow.EventFilter, opts ...access.SubscribeOption, ) (<-chan flow.BlockEvents, <-chan error, error) { - return c. - getClientForHeight(startHeight). - SubscribeEventsByBlockHeight(ctx, startHeight, filter, opts...) + client, err := c.getClientForHeight(startHeight) + if err != nil { + return nil, nil, err + } + return client.SubscribeEventsByBlockHeight(ctx, startHeight, filter, opts...) } diff --git a/services/requester/cross-spork_client_test.go b/services/requester/cross-spork_client_test.go new file mode 100644 index 000000000..51e8c70c9 --- /dev/null +++ b/services/requester/cross-spork_client_test.go @@ -0,0 +1,129 @@ +package requester + +import ( + "context" + "testing" + + "github.com/onflow/flow-go-sdk" + "github.com/onflow/flow-go-sdk/access" + "github.com/rs/zerolog" + "github.com/stretchr/testify/require" + + "github.com/onflow/flow-evm-gateway/services/testutils" +) + +func Test_CrossSporkClient(t *testing.T) { + t.Run("contains", func(t *testing.T) { + first := uint64(10) + last := uint64(100) + client := sporkClient{ + firstHeight: first, + lastHeight: last, + client: nil, + } + + require.True(t, client.contains(first+1)) + require.True(t, client.contains(last-1)) + require.True(t, client.contains(first)) + require.True(t, client.contains(last)) + require.False(t, client.contains(2)) + require.False(t, client.contains(200)) + }) +} + +func Test_CrossSporkClients(t *testing.T) { + t.Run("add and validate", func(t *testing.T) { + clients := &sporkClients{} + + client1 := testutils.SetupClientForRange(10, 100) + client2 := testutils.SetupClientForRange(101, 200) + client3 := testutils.SetupClientForRange(201, 300) + + require.NoError(t, clients.add(client2)) + require.NoError(t, clients.add(client3)) + require.NoError(t, clients.add(client1)) + + require.True(t, clients.continuous()) + + require.Equal(t, client1, clients.get(10)) + require.Equal(t, client1, clients.get(100)) + require.Equal(t, client1, clients.get(20)) + require.Equal(t, client2, clients.get(120)) + require.Equal(t, client2, clients.get(101)) + require.Equal(t, client2, clients.get(200)) + + require.Equal(t, nil, clients.get(5)) + require.Equal(t, nil, clients.get(310)) + }) + + t.Run("add and validate not-continues", func(t *testing.T) { + clients := &sporkClients{} + + client1 := testutils.SetupClientForRange(10, 30) + client2 := testutils.SetupClientForRange(50, 80) + + require.NoError(t, clients.add(client1)) + require.NoError(t, clients.add(client2)) + + require.False(t, clients.continuous()) + }) +} + +func Test_CrossSpork(t *testing.T) { + t.Run("client", func(t *testing.T) { + past1Last := uint64(300) + past2Last := uint64(500) + currentLast := uint64(1000) + current := testutils.SetupClientForRange(501, currentLast) + past1 := testutils.SetupClientForRange(100, past1Last) + past2 := testutils.SetupClientForRange(301, past2Last) + + client, err := NewCrossSporkClient(current, []access.Client{past2, past1}, zerolog.Nop()) + require.NoError(t, err) + + c, err := client.getClientForHeight(150) + require.NoError(t, err) + require.Equal(t, past1, c) + + c, err = client.getClientForHeight(past2Last - 1) + require.NoError(t, err) + require.Equal(t, past2, c) + + c, err = client.getClientForHeight(600) + require.NoError(t, err) + require.Equal(t, current, c) + + c, err = client.getClientForHeight(10) + require.Nil(t, c) + require.ErrorIs(t, err, ErrOutOfRange) + + require.True(t, client.IsPastSpork(200)) + require.True(t, client.IsPastSpork(past1Last)) + require.False(t, client.IsPastSpork(past2Last+1)) + require.False(t, client.IsPastSpork(600)) + + _, err = client.ExecuteScriptAtBlockHeight(context.Background(), 20, []byte{}, nil) + require.ErrorIs(t, err, ErrOutOfRange) + + _, err = client.GetBlockHeaderByHeight(context.Background(), 20) + require.ErrorIs(t, err, ErrOutOfRange) + + _, _, err = client.SubscribeEventsByBlockHeight(context.Background(), 20, flow.EventFilter{}, nil) + require.ErrorIs(t, err, ErrOutOfRange) + + height, err := client.GetLatestHeightForSpork(context.Background(), past2Last-10) + require.NoError(t, err) + require.Equal(t, past2Last, height) + + height, err = client.GetLatestHeightForSpork(context.Background(), past1Last-10) + require.NoError(t, err) + require.Equal(t, past1Last, height) + + height, err = client.GetLatestHeightForSpork(context.Background(), currentLast-10) + require.NoError(t, err) + require.Equal(t, currentLast, height) + + _, err = client.GetLatestHeightForSpork(context.Background(), 10) + require.ErrorIs(t, err, ErrOutOfRange) + }) +} diff --git a/services/requester/requester.go b/services/requester/requester.go index d45ebda88..e88bb45a4 100644 --- a/services/requester/requester.go +++ b/services/requester/requester.go @@ -11,7 +11,6 @@ import ( "github.com/onflow/cadence" "github.com/onflow/flow-go-sdk" - "github.com/onflow/flow-go-sdk/access" "github.com/onflow/flow-go-sdk/crypto" "github.com/onflow/flow-go/fvm/evm/stdlib" evmTypes "github.com/onflow/flow-go/fvm/evm/types" @@ -89,14 +88,14 @@ type Requester interface { var _ Requester = &EVM{} type EVM struct { - client access.Client + client *CrossSporkClient config *config.Config signer crypto.Signer logger zerolog.Logger } func NewEVM( - client access.Client, + client *CrossSporkClient, config *config.Config, signer crypto.Signer, logger zerolog.Logger, @@ -145,10 +144,6 @@ func NewEVM( } func (e *EVM) SendRawTransaction(ctx context.Context, data []byte) (common.Hash, error) { - e.logger.Debug(). - Str("data", fmt.Sprintf("%x", data)). - Msg("send raw transaction") - tx := &types.Transaction{} if err := tx.UnmarshalBinary(data); err != nil { return common.Hash{}, err @@ -167,6 +162,7 @@ func (e *EVM) SendRawTransaction(ctx context.Context, data []byte) (common.Hash, script := e.replaceAddresses(runTxScript) flowID, err := e.signAndSend(ctx, script, hexEncodedTx) if err != nil { + e.logger.Error().Err(err).Str("data", string(data)).Msg("failed to send transaction") return common.Hash{}, err } @@ -236,25 +232,6 @@ func (e *EVM) signAndSend(ctx context.Context, script []byte, args ...cadence.Va return flow.EmptyID, fmt.Errorf("failed to send transaction: %w", err) } - // this is only used for debugging purposes - if d := e.logger.Debug(); d.Enabled() { - go func(id flow.Identifier) { - res, _ := e.client.GetTransactionResult(context.Background(), id) - if res != nil && res.Error != nil { - e.logger.Error(). - Str("flow-id", id.String()). - Err(res.Error). - Msg("flow transaction failed to execute") - return - } - - e.logger.Debug(). - Str("flow-id", id.String()). - Str("events", fmt.Sprintf("%v", res.Events)). - Msg("flow transaction executed successfully") - }(flowTx.ID()) - } - return flowTx.ID(), nil } @@ -275,11 +252,14 @@ func (e *EVM) GetBalance( []cadence.Value{hexEncodedAddress}, ) if err != nil { - return nil, err + e.logger.Error(). + Err(err). + Str("address", address.String()). + Uint64("cadence-height", height). + Msg("failed to get get balance") + return nil, fmt.Errorf("failed to get balance: %w", err) } - e.logger.Info().Str("address", address.String()).Msg("get balance") - // sanity check, should never occur if _, ok := val.(cadence.UInt); !ok { e.logger.Panic().Msg(fmt.Sprintf("failed to convert balance %v to UInt", val)) @@ -305,11 +285,13 @@ func (e *EVM) GetNonce( []cadence.Value{hexEncodedAddress}, ) if err != nil { - return 0, err + e.logger.Error().Err(err). + Str("address", address.String()). + Uint64("cadence-height", height). + Msg("failed to get nonce") + return 0, fmt.Errorf("failed to get nonce: %w", err) } - e.logger.Info().Str("address", address.String()).Msg("get nonce") - // sanity check, should never occur if _, ok := val.(cadence.UInt64); !ok { e.logger.Panic().Msg(fmt.Sprintf("failed to convert balance %v to UInt64", val)) @@ -324,10 +306,6 @@ func (e *EVM) Call( from common.Address, height uint64, ) ([]byte, error) { - e.logger.Debug(). - Str("data", fmt.Sprintf("%x", data)). - Msg("call") - hexEncodedTx, err := cadence.NewString(hex.EncodeToString(data)) if err != nil { return nil, err @@ -345,6 +323,12 @@ func (e *EVM) Call( []cadence.Value{hexEncodedTx, hexEncodedAddress}, ) if err != nil { + e.logger.Error(). + Err(err). + Uint64("cadence-height", height). + Str("from", from.String()). + Str("data", string(data)). + Msg("failed to execute call") return nil, fmt.Errorf("failed to execute script: %w", err) } @@ -358,8 +342,7 @@ func (e *EVM) Call( result := evmResult.ReturnedValue - e.logger.Info(). - Str("data", fmt.Sprintf("%x", data)). + e.logger.Debug(). Str("result", hex.EncodeToString(result)). Msg("call executed") diff --git a/services/testutils/mock_client.go b/services/testutils/mock_client.go new file mode 100644 index 000000000..eece1c9df --- /dev/null +++ b/services/testutils/mock_client.go @@ -0,0 +1,84 @@ +package testutils + +import ( + "context" + + "github.com/onflow/flow-go-sdk" + "github.com/onflow/flow-go-sdk/access" + "github.com/onflow/flow-go-sdk/access/mocks" + "github.com/onflow/flow-go/storage" +) + +type MockClient struct { + *mocks.Client + getLatestBlockHeaderFunc func(context.Context, bool) (*flow.BlockHeader, error) + getBlockHeaderByHeightFunc func(context.Context, uint64) (*flow.BlockHeader, error) + subscribeEventsByBlockHeightFunc func(context.Context, uint64, flow.EventFilter, ...access.SubscribeOption) (<-chan flow.BlockEvents, <-chan error, error) + getNodeVersionInfoFunc func(ctx context.Context) (*flow.NodeVersionInfo, error) +} + +func (c *MockClient) GetBlockHeaderByHeight(ctx context.Context, height uint64) (*flow.BlockHeader, error) { + return c.getBlockHeaderByHeightFunc(ctx, height) +} + +func (c *MockClient) GetLatestBlockHeader(ctx context.Context, sealed bool) (*flow.BlockHeader, error) { + return c.getLatestBlockHeaderFunc(ctx, sealed) +} + +func (c *MockClient) GetNodeVersionInfo(ctx context.Context) (*flow.NodeVersionInfo, error) { + return c.getNodeVersionInfoFunc(ctx) +} + +func (c *MockClient) SubscribeEventsByBlockHeight( + ctx context.Context, + startHeight uint64, + filter flow.EventFilter, + opts ...access.SubscribeOption, +) (<-chan flow.BlockEvents, <-chan error, error) { + return c.subscribeEventsByBlockHeightFunc(ctx, startHeight, filter, opts...) +} + +func SetupClientForRange(startHeight uint64, endHeight uint64) access.Client { + return &MockClient{ + Client: &mocks.Client{}, + getLatestBlockHeaderFunc: func(ctx context.Context, sealed bool) (*flow.BlockHeader, error) { + return &flow.BlockHeader{ + Height: endHeight, + }, nil + }, + getBlockHeaderByHeightFunc: func(ctx context.Context, height uint64) (*flow.BlockHeader, error) { + if height < startHeight || height > endHeight { + return nil, storage.ErrNotFound + } + + return &flow.BlockHeader{ + Height: height, + }, nil + }, + getNodeVersionInfoFunc: func(ctx context.Context) (*flow.NodeVersionInfo, error) { + return &flow.NodeVersionInfo{ + NodeRootBlockHeight: startHeight, + }, nil + }, + subscribeEventsByBlockHeightFunc: func( + ctx context.Context, + startHeight uint64, + filter flow.EventFilter, + opts ...access.SubscribeOption, + ) (<-chan flow.BlockEvents, <-chan error, error) { + events := make(chan flow.BlockEvents) + + go func() { + defer close(events) + + for i := startHeight; i <= endHeight; i++ { + events <- flow.BlockEvents{ + Height: i, + } + } + }() + + return events, make(chan error), nil + }, + } +} diff --git a/tests/go.mod b/tests/go.mod index e760a587d..0cf8079fc 100644 --- a/tests/go.mod +++ b/tests/go.mod @@ -5,10 +5,10 @@ go 1.20 require ( github.com/goccy/go-json v0.10.2 github.com/onflow/cadence v1.0.0-preview.29 - github.com/onflow/flow-emulator v1.0.0-preview.23 + github.com/onflow/flow-emulator v1.0.0-preview.23.0.20240524112921-431194fd9bc0 github.com/onflow/flow-evm-gateway v0.0.0-20240201154855-4d4d3d3f19c7 github.com/onflow/flow-go v0.35.5-0.20240517202625-55f862b45dfd - github.com/onflow/flow-go-sdk v1.0.0-preview.30 + github.com/onflow/flow-go-sdk v1.0.0-preview.30.0.20240523120036-f9d51677b347 github.com/onflow/go-ethereum v1.13.4 github.com/rs/zerolog v1.31.0 github.com/stretchr/testify v1.9.0 diff --git a/tests/go.sum b/tests/go.sum index 276d73ac8..fec59d495 100644 --- a/tests/go.sum +++ b/tests/go.sum @@ -1018,7 +1018,7 @@ github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZw github.com/aws/aws-sdk-go-v2 v1.2.0/go.mod h1:zEQs02YRBw1DjK0PoJv3ygDYOFTre1ejlJWl8FwAuQo= github.com/aws/aws-sdk-go-v2 v1.21.2/go.mod h1:ErQhvNuEMhJjweavOYhxVkn2RUx7kQXVATHrjKtxIpM= github.com/aws/aws-sdk-go-v2 v1.23.1/go.mod h1:i1XDttT4rnf6vxc9AuskLc6s7XBee8rlLilKlc03uAA= -github.com/aws/aws-sdk-go-v2 v1.26.1 h1:5554eUqIYVWpU0YmeeYZ0wU64H2VLBs8TlhRB2L+EkA= +github.com/aws/aws-sdk-go-v2 v1.27.0 h1:7bZWKoXhzI+mMR/HjdMx8ZCC5+6fY0lS5tr0bbgiLlo= github.com/aws/aws-sdk-go-v2/config v1.1.1/go.mod h1:0XsVy9lBI/BCXm+2Tuvt39YmdHwS5unDQmxZOYe8F5Y= github.com/aws/aws-sdk-go-v2/config v1.18.45/go.mod h1:ZwDUgFnQgsazQTnWfeLWk5GjeqTQTL8lMkoE1UXzxdE= github.com/aws/aws-sdk-go-v2/config v1.25.5/go.mod h1:Bf4gDvy4ZcFIK0rqDu1wp9wrubNba2DojiPB2rt6nvI= @@ -1040,7 +1040,7 @@ github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2 h1:Ji0DY1x github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.0.2/go.mod h1:45MfaXZ0cNbeuT0KQ1XJylq8A6+OpVV2E5kvY/Kq+u8= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.37/go.mod h1:vBmDnwWXWxNPFRMmG2m/3MKOe+xEcMDo1tanpaWCcck= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.4/go.mod h1:aYCGNjyUCUelhofxlZyj63srdxWUSsBSGg5l6MCuXuE= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.7 h1:ogRAwT1/gxJBcSWDMZlgyFUM962F51A5CRhDLbxLdmo= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.9 h1:Wx0rlZoEJR7JwlSZcHnEa7CNjrSIyVxMFWGAaXy4fJY= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.7.0 h1:HWsM0YQWX76V6MOp07YuTYacm8k7h69ObJuw7Nck+og= github.com/aws/aws-sdk-go-v2/service/kms v1.26.3/go.mod h1:N3++/sLV97B8Zliz7KRqNcojOX7iMBZWKiuit5FKtH0= github.com/aws/aws-sdk-go-v2/service/route53 v1.1.1/go.mod h1:rLiOUrPLW/Er5kRcQ7NkwbjlijluLsrIbu/iyl35RO4= @@ -2023,8 +2023,8 @@ github.com/onflow/flow-core-contracts/lib/go/contracts v1.1.0 h1:AegPBm079X0qjne github.com/onflow/flow-core-contracts/lib/go/contracts v1.1.0/go.mod h1:u/mkP/B+PbV33tEG3qfkhhBlydSvAKxfLZSfB4lsJHg= github.com/onflow/flow-core-contracts/lib/go/templates v1.0.0 h1:za6bxPPW4JIsthhasUDTa1ruKjIO8DIhun9INQfj61Y= github.com/onflow/flow-core-contracts/lib/go/templates v1.0.0/go.mod h1:NgbMOYnMh0GN48VsNKZuiwK7uyk38Wyo8jN9+C9QE30= -github.com/onflow/flow-emulator v1.0.0-preview.23 h1:wgbwhRkzaSZbr348og8csr2AvLrcyGSP8HR8P2mxa3g= -github.com/onflow/flow-emulator v1.0.0-preview.23/go.mod h1:QprPouTWO3iv9VF/y4Ksltv2XIbzNMzjjr5zzq51i7Q= +github.com/onflow/flow-emulator v1.0.0-preview.23.0.20240524112921-431194fd9bc0 h1:kpg8rCXLbxXaxHpw6vbHS88jMDlDpVayQVOUgQmR3Mc= +github.com/onflow/flow-emulator v1.0.0-preview.23.0.20240524112921-431194fd9bc0/go.mod h1:QprPouTWO3iv9VF/y4Ksltv2XIbzNMzjjr5zzq51i7Q= 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= @@ -2032,8 +2032,8 @@ github.com/onflow/flow-ft/lib/go/templates v1.0.0/go.mod h1:uQ8XFqmMK2jxyBSVrmyu github.com/onflow/flow-go v0.35.5-0.20240517202625-55f862b45dfd h1:bSoQMARSC4dk6sQPv6SRkV7QvovKoNksTnydK9e5hL4= github.com/onflow/flow-go v0.35.5-0.20240517202625-55f862b45dfd/go.mod h1:5ysH9wggXlvJqbALEBZc2uNx6DIE+QsBoocuM8bSGC0= github.com/onflow/flow-go-sdk v1.0.0-M1/go.mod h1:TDW0MNuCs4SvqYRUzkbRnRmHQL1h4X8wURsCw9P9beo= -github.com/onflow/flow-go-sdk v1.0.0-preview.30 h1:62IwC7l8Uw1mxoZe7ewJII0HFHLUMsg04z1BW3JSEfM= -github.com/onflow/flow-go-sdk v1.0.0-preview.30/go.mod h1:PBIk3vLqU1aLdbWPw7ljRDmwSGLcsuk/ipL9eLMgWwc= +github.com/onflow/flow-go-sdk v1.0.0-preview.30.0.20240523120036-f9d51677b347 h1:qXJa8wp2aJLzDO5TVmMxaSHlrZ0/O/HMHzrpFUDP0eo= +github.com/onflow/flow-go-sdk v1.0.0-preview.30.0.20240523120036-f9d51677b347/go.mod h1:J4iKISX976mxV3ReTWiURG/ai50h61s2XJZ3YcK2lCg= github.com/onflow/flow-nft/lib/go/contracts v1.2.1 h1:woAAS5z651sDpi7ihAHll8NvRS9uFXIXkL6xR+bKFZY= github.com/onflow/flow-nft/lib/go/contracts v1.2.1/go.mod h1:2gpbza+uzs1k7x31hkpBPlggIRkI53Suo0n2AyA2HcE= github.com/onflow/flow-nft/lib/go/templates v1.2.0 h1:JSQyh9rg0RC+D1930BiRXN8lrtMs+ubVMK6aQPon6Yc= diff --git a/tests/web3js/eth_logs_filtering_test.js b/tests/web3js/eth_logs_filtering_test.js index 216ee0e29..f48602ae2 100644 --- a/tests/web3js/eth_logs_filtering_test.js +++ b/tests/web3js/eth_logs_filtering_test.js @@ -4,6 +4,8 @@ const helpers = require('./helpers') const web3 = conf.web3 it('emit logs and retrieve them using different filters', async() => { + setTimeout(() => process.exit(1), 19*1000) // hack if the ws connection is not closed + let deployed = await helpers.deployContract("storage") let contractAddress = deployed.receipt.contractAddress @@ -88,4 +90,4 @@ it('emit logs and retrieve them using different filters', async() => { // todo compose more complex topic filters using OR and AND logic */ -}).timeout(10*1000) \ No newline at end of file +}).timeout(20*1000) \ No newline at end of file diff --git a/tests/web3js/eth_streaming_filters_test.js b/tests/web3js/eth_streaming_filters_test.js index 38cdbc4af..b9cdcfe54 100644 --- a/tests/web3js/eth_streaming_filters_test.js +++ b/tests/web3js/eth_streaming_filters_test.js @@ -65,6 +65,8 @@ async function assertFilterLogs(subscription, expectedLogs) { } it('streaming of logs using filters', async() => { + setTimeout(() => process.exit(1), (timeout-1)*1000) // hack if the ws connection is not closed + let contractDeployment = await helpers.deployContract("storage") let contractAddress = contractDeployment.receipt.contractAddress diff --git a/tests/web3js/eth_streaming_test.js b/tests/web3js/eth_streaming_test.js index 6c3d94ece..f0595ea03 100644 --- a/tests/web3js/eth_streaming_test.js +++ b/tests/web3js/eth_streaming_test.js @@ -6,6 +6,8 @@ const {Web3} = require("web3"); const timeout = 30 // test timeout seconds it('streaming of logs using filters', async() => { + setTimeout(() => process.exit(1), (timeout-1)*1000) // hack if the ws connection is not closed + let deployed = await helpers.deployContract("storage") let contractAddress = deployed.receipt.contractAddress