diff --git a/Makefile b/Makefile index 36a62ebb..9a2a599f 100644 --- a/Makefile +++ b/Makefile @@ -7,6 +7,7 @@ SWAGGER = $(GOBIN)/swagger PROTOC_GEN_GO = $(GOBIN)/protoc-gen-go PROTOC_GEN_GO_GRPC = $(GOBIN)/protoc-gen-go-grpc GOMERGETYPES = $(GOBIN)/gomergetypes +MOCKGEN = $(GOBIN)/mockgen PROTOC = ./toolbin/protoc --plugin=protoc-gen-go=$(PROTOC_GEN_GO) --plugin=protoc-gen-go-grpc=$(PROTOC_GEN_GO_GRPC) @@ -32,6 +33,8 @@ tools: @go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.47.0 @go install golang.org/x/tools/cmd/goimports@v0.1.11 + @go install github.com/golang/mock/mockgen@5b455625bd2c8ffbcc0de6a0873f864ba3820904 + @go install github.com/go-swagger/go-swagger/cmd/swagger@v0.29.0 @go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.28 @@ -67,18 +70,19 @@ protogen: require-tools .PHONY: mocks mocks: - mockgen -source ethereum/client.go -destination ethereum/mocks/mock_client.go - mockgen -source feeds/interfaces.go -destination feeds/mocks/mock_feeds.go - mockgen -source ethereum/contract_backend.go -destination ethereum/mocks/mock_ethclient.go - mockgen -source registry/client.go -destination registry/mocks/mock_client.go - mockgen -source registry/version.go -destination registry/mocks/mock_version.go - mockgen -source domain/registry/regmsg/regmsg.go -destination domain/registry/regmsg/mocks/mock_regmsg.go - mockgen -source ipfs/client.go -destination ipfs/mocks/mock_client.go - mockgen -source release/client.go -destination release/mocks/mock_client.go - mockgen -source domain/ethereum.go -destination domain/mocks/mock_ethereum.go - mockgen -source manifest/client.go -destination manifest/mocks/mock_client.go - mockgen -source clients/graphql/client.go -destination clients/mocks/mock_graphql_client.go - mockgen -source utils/ethutils/iterator.go -destination utils/ethutils/mocks/mock_iterator.go + $(MOCKGEN) -source ethereum/client.go -destination ethereum/mocks/mock_client.go + $(MOCKGEN) -source feeds/interfaces.go -destination feeds/mocks/mock_feeds.go + $(MOCKGEN) -source ethereum/contract_backend.go -destination ethereum/mocks/mock_ethclient.go + $(MOCKGEN) -source registry/client.go -destination registry/mocks/mock_client.go + $(MOCKGEN) -source registry/version.go -destination registry/mocks/mock_version.go + $(MOCKGEN) -source domain/registry/regmsg/regmsg.go -destination domain/registry/regmsg/mocks/mock_regmsg.go + $(MOCKGEN) -source ipfs/client.go -destination ipfs/mocks/mock_client.go + $(MOCKGEN) -source release/client.go -destination release/mocks/mock_client.go + $(MOCKGEN) -source domain/ethereum.go -destination domain/mocks/mock_ethereum.go + $(MOCKGEN) -source manifest/client.go -destination manifest/mocks/mock_client.go + $(MOCKGEN) -source clients/graphql/client.go -destination clients/mocks/mock_graphql_client.go + $(MOCKGEN) -source utils/ethutils/iterator.go -destination utils/ethutils/mocks/mock_iterator.go + $(MOCKGEN) -source inspect/proxy_api.go -destination inspect/mocks/mock_proxy_api.go .PHONY: test test: diff --git a/go.mod b/go.mod index f9cc680d..a6fa2cbd 100644 --- a/go.mod +++ b/go.mod @@ -29,6 +29,7 @@ require ( github.com/kelseyhightower/envconfig v1.4.0 github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe github.com/patrickmn/go-cache v2.1.0+incompatible + github.com/pkg/errors v0.9.1 github.com/shirou/gopsutil v3.21.11+incompatible github.com/showwin/speedtest-go v1.1.5 github.com/sirupsen/logrus v1.8.1 @@ -195,7 +196,6 @@ require ( github.com/opentracing/opentracing-go v1.2.0 // indirect github.com/openzipkin/zipkin-go v0.4.0 // indirect github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect - github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/polydawn/refmt v0.0.0-20201211092308-30ac6d18308e // indirect github.com/prometheus/client_golang v1.14.0 // indirect diff --git a/inspect/mocks/mock_proxy_api.go b/inspect/mocks/mock_proxy_api.go new file mode 100644 index 00000000..5636c3a4 --- /dev/null +++ b/inspect/mocks/mock_proxy_api.go @@ -0,0 +1,67 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: inspect/proxy_api.go + +// Package mock_inspect is a generated GoMock package. +package mock_inspect + +import ( + context "context" + big "math/big" + reflect "reflect" + + types "github.com/ethereum/go-ethereum/core/types" + gomock "github.com/golang/mock/gomock" +) + +// MockProxyAPIClient is a mock of ProxyAPIClient interface. +type MockProxyAPIClient struct { + ctrl *gomock.Controller + recorder *MockProxyAPIClientMockRecorder +} + +// MockProxyAPIClientMockRecorder is the mock recorder for MockProxyAPIClient. +type MockProxyAPIClientMockRecorder struct { + mock *MockProxyAPIClient +} + +// NewMockProxyAPIClient creates a new mock instance. +func NewMockProxyAPIClient(ctrl *gomock.Controller) *MockProxyAPIClient { + mock := &MockProxyAPIClient{ctrl: ctrl} + mock.recorder = &MockProxyAPIClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockProxyAPIClient) EXPECT() *MockProxyAPIClientMockRecorder { + return m.recorder +} + +// BlockByNumber mocks base method. +func (m *MockProxyAPIClient) BlockByNumber(ctx context.Context, int2 *big.Int) (*types.Block, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "BlockByNumber", ctx, int2) + ret0, _ := ret[0].(*types.Block) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// BlockByNumber indicates an expected call of BlockByNumber. +func (mr *MockProxyAPIClientMockRecorder) BlockByNumber(ctx, int2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BlockByNumber", reflect.TypeOf((*MockProxyAPIClient)(nil).BlockByNumber), ctx, int2) +} + +// BlockNumber mocks base method. +func (m *MockProxyAPIClient) BlockNumber(ctx context.Context) (uint64, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "BlockNumber", ctx) + ret0, _ := ret[0].(uint64) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// BlockNumber indicates an expected call of BlockNumber. +func (mr *MockProxyAPIClientMockRecorder) BlockNumber(ctx interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BlockNumber", reflect.TypeOf((*MockProxyAPIClient)(nil).BlockNumber), ctx) +} diff --git a/inspect/offset.go b/inspect/offset.go index 6cc1bb92..16918218 100644 --- a/inspect/offset.go +++ b/inspect/offset.go @@ -6,7 +6,6 @@ import ( "math/big" "time" - "github.com/ethereum/go-ethereum/ethclient" "github.com/montanaflynn/stats" "golang.org/x/sync/errgroup" ) @@ -19,8 +18,8 @@ type offsetStats struct { } func calculateOffsetStats( - ctx context.Context, primaryClient *ethclient.Client, - secondaryClient *ethclient.Client, + ctx context.Context, primaryClient, + secondaryClient ProxyAPIClient, ) (offsetStats, error) { ds, err := collectOffsetData(ctx, primaryClient, secondaryClient) if err != nil { @@ -32,7 +31,7 @@ func calculateOffsetStats( // collectOffsetData measures how long does it take to receive a recently created block and compares given eth clients. // The idea is to mimic the behavior of Scanner feed and Bot proxy query. -func collectOffsetData(ctx context.Context, primaryClient *ethclient.Client, secondaryClient *ethclient.Client) ( +func collectOffsetData(ctx context.Context, primaryClient, secondaryClient ProxyAPIClient) ( []float64, error, ) { maxDuration := time.Second * 20 @@ -56,6 +55,11 @@ func collectOffsetData(ctx context.Context, primaryClient *ethclient.Client, sec case <-ctx.Done(): return dataPoints, nil case <-t.C: + // circuit breaker for easier testing + if len(dataPoints) == 10 { + return dataPoints, nil + } + g, ctx := errgroup.WithContext(ctx) var ( @@ -98,7 +102,7 @@ func collectOffsetData(ctx context.Context, primaryClient *ethclient.Client, sec } } } -func measureBlockDelay(ctx context.Context, client *ethclient.Client, blockNum uint64) (int64, error) { +func measureBlockDelay(ctx context.Context, client ProxyAPIClient, blockNum uint64) (int64, error) { t := time.Millisecond * 200 start := time.Now() diff --git a/inspect/offset_test.go b/inspect/offset_test.go new file mode 100644 index 00000000..17ce72f6 --- /dev/null +++ b/inspect/offset_test.go @@ -0,0 +1,37 @@ +package inspect + +import ( + "context" + "fmt" + "testing" + "time" + + mock_inspect "github.com/forta-network/forta-core-go/inspect/mocks" + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/assert" +) + +func TestCalculateOffsetStats(t *testing.T) { + // Create a test context with a timeout + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + ctrl := gomock.NewController(t) + primaryClient := mock_inspect.NewMockProxyAPIClient(ctrl) + secondaryClient := mock_inspect.NewMockProxyAPIClient(ctrl) + + // Test when everything is successful + primaryClient.EXPECT().BlockNumber(gomock.Any()).Return(uint64(5), nil) + primaryClient.EXPECT().BlockByNumber(gomock.Any(), gomock.Any()).Return(nil, nil).AnyTimes() + secondaryClient.EXPECT().BlockByNumber(gomock.Any(), gomock.Any()).Return(nil, nil).AnyTimes() + stats, err := calculateOffsetStats(ctx, primaryClient, secondaryClient) + assert.NoError(t, err) + assert.Equal(t, stats, offsetStats{0, 0, 0, 10}) + + // Test when collectOffsetData returns an error + primaryClient.EXPECT().BlockNumber(gomock.Any()).Return(uint64(0), fmt.Errorf("error")) + + stats, err = calculateOffsetStats(ctx, primaryClient, secondaryClient) + assert.Error(t, err) + assert.Equal(t, stats, offsetStats{}) +} diff --git a/inspect/proxy_api.go b/inspect/proxy_api.go index c9634166..4a62edc9 100644 --- a/inspect/proxy_api.go +++ b/inspect/proxy_api.go @@ -4,8 +4,8 @@ import ( "context" "fmt" "math/big" - "time" + "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/rpc" "github.com/hashicorp/go-multierror" @@ -56,6 +56,11 @@ type ProxyAPIInspector struct{} // compile time check: it should implement the interface var _ Inspector = &ProxyAPIInspector{} +type ProxyAPIClient interface { + BlockNumber(ctx context.Context) (uint64, error) + BlockByNumber(ctx context.Context, int2 *big.Int) (*types.Block, error) +} + // Name returns the name of the inspector. func (pai *ProxyAPIInspector) Name() string { return "proxy-api" @@ -125,7 +130,21 @@ func (pai *ProxyAPIInspector) Inspect(ctx context.Context, inspectionCfg Inspect results.Indicators[IndicatorProxyAPIIsETH2] = ResultFailure } - stats, err := pai.detectOffset(ctx, inspectionCfg) + scanRPCClient, err := rpc.DialContext(ctx, inspectionCfg.ScanAPIURL) + if err != nil { + resultErr = multierror.Append(resultErr, fmt.Errorf("can't dial json-rpc api %w", err)) + + results.Indicators[IndicatorProxyAPIAccessible] = ResultFailure + results.Indicators[IndicatorProxyAPIModuleWeb3] = ResultFailure + results.Indicators[IndicatorProxyAPIModuleEth] = ResultFailure + results.Indicators[IndicatorProxyAPIModuleNet] = ResultFailure + results.Indicators[IndicatorProxyAPIHistorySupport] = ResultFailure + results.Indicators[IndicatorProxyAPIChainID] = ResultFailure + } + + scanClient := ethclient.NewClient(scanRPCClient) + + stats, err := calculateOffsetStats(ctx, proxyClient, scanClient) if err != nil { resultErr = multierror.Append(resultErr, fmt.Errorf("can't calculate scan-proxy offset: %w", err)) results.Indicators[IndicatorProxyAPIOffsetScanMean] = ResultUnknown @@ -142,29 +161,6 @@ func (pai *ProxyAPIInspector) Inspect(ctx context.Context, inspectionCfg Inspect return } -func (pai *ProxyAPIInspector) detectOffset( - ctx context.Context, - inspectionCfg InspectionConfig, -) (os offsetStats, resultErr error) { - proxyCtx, cancel := context.WithTimeout(ctx, time.Second*30) - defer cancel() - - proxyRPCClient, err := rpc.DialContext(ctx, inspectionCfg.ProxyAPIURL) - if err != nil { - return offsetStats{}, err - } - - proxyClient := ethclient.NewClient(proxyRPCClient) - scanRPCClient, err := rpc.DialContext(ctx, inspectionCfg.ScanAPIURL) - if err != nil { - return offsetStats{}, err - } - - scanClient := ethclient.NewClient(scanRPCClient) - - return calculateOffsetStats(proxyCtx, scanClient, proxyClient) -} - // checkSupportedModules double-checks the functionality of modules that were declared as supported by // the node. func checkSupportedModules( @@ -213,7 +209,7 @@ func checkHistorySupport(ctx context.Context, latestBlock uint64, client *ethcli } // findOldestSupportedBlock returns the earliest block provided by client -func findOldestSupportedBlock(ctx context.Context, client *ethclient.Client, low, high uint64) uint64 { +func findOldestSupportedBlock(ctx context.Context, client ProxyAPIClient, low, high uint64) uint64 { memo := make(map[uint64]bool) // terminating condition, results merged diff --git a/inspect/proxy_api_test.go b/inspect/proxy_api_test.go index cb31c76b..81390efd 100644 --- a/inspect/proxy_api_test.go +++ b/inspect/proxy_api_test.go @@ -2,108 +2,41 @@ package inspect import ( "context" + "errors" "testing" - "github.com/ethereum/go-ethereum/ethclient" - "github.com/kelseyhightower/envconfig" - "github.com/stretchr/testify/require" + types "github.com/ethereum/go-ethereum/core/types" + mock_inspect "github.com/forta-network/forta-core-go/inspect/mocks" + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/assert" ) -const ( - testProxyAPIOldestSupportedBlock = uint64(0) -) - -var testProxyEnv struct { - ProxyAPI string `envconfig:"proxy_api" default:"https://rpc.ankr.com/eth_goerli"` -} - -func init() { - envconfig.MustProcess("test", &testProxyEnv) -} - -func TestProxyAPIInspection(t *testing.T) { - r := require.New(t) - - inspector := &ProxyAPIInspector{} - results, err := inspector.Inspect( - context.Background(), InspectionConfig{ - ProxyAPIURL: testProxyEnv.ProxyAPI, - ScanAPIURL: testProxyEnv.ProxyAPI, - BlockNumber: testProxyAPIOldestSupportedBlock, - }, - ) - r.NoError(err) +func TestFindOldestSupportedBlock(t *testing.T) { + // Create a new Gomock controller + ctrl := gomock.NewController(t) + defer ctrl.Finish() - r.Equal( - map[string]float64{ - IndicatorProxyAPIAccessible: ResultSuccess, - IndicatorProxyAPIChainID: float64(5), - IndicatorProxyAPIModuleWeb3: ResultSuccess, - IndicatorProxyAPIModuleEth: ResultSuccess, - IndicatorProxyAPIModuleNet: ResultSuccess, - IndicatorProxyAPIHistorySupport: VeryOldBlockNumber, - IndicatorProxyAPIIsETH2: ResultSuccess, - // trick to make test less flaky and ignore offset issues - IndicatorProxyAPIOffsetScanMax: results.Indicators[IndicatorProxyAPIOffsetScanMax], - IndicatorProxyAPIOffsetScanMean: results.Indicators[IndicatorProxyAPIOffsetScanMean], - IndicatorProxyAPIOffsetScanMedian: results.Indicators[IndicatorProxyAPIOffsetScanMedian], - IndicatorProxyAPIOffsetScanSamples: results.Indicators[IndicatorProxyAPIOffsetScanSamples], - }, results.Indicators, - ) - - r.Equal( - map[string]string{ - MetadataProxyAPIBlockByNumberHash: "7232705dbb71b9d5ef65891c2c6e7020137ffb652ed938a88621b322f09ab4a4", - }, results.Metadata, - ) -} - -func Test_findOldestSupportedBlock(t *testing.T) { - r := require.New(t) - - cli, err := ethclient.Dial(testProxyEnv.ProxyAPI) - r.NoError(err) + // Create a mock ethclient.Client + mockClient := mock_inspect.NewMockProxyAPIClient(ctrl) + // Create a test context ctx := context.Background() - latestBlockNum, err := cli.BlockNumber(ctx) - r.NoError(err) - result := findOldestSupportedBlock(context.Background(), cli, 0, latestBlockNum) - r.Equal(testProxyAPIOldestSupportedBlock, result) -} + // Test when the earliest block is found + mockClient.EXPECT().BlockByNumber(ctx, gomock.Any()).Return(&types.Block{}, nil).Times(7) + + oldestBlock := findOldestSupportedBlock(ctx, mockClient, 0, 200) + assert.Equal(t, uint64(0), oldestBlock) -func TestProxyAPIInspector_detectOffset(t *testing.T) { - type args struct { - ctx context.Context - inspectionCfg InspectionConfig - } + // Test when the earliest block is not found + mockClient.EXPECT().BlockByNumber(ctx, gomock.Any()).Return(nil, errors.New("block not found")).Times(2) + mockClient.EXPECT().BlockByNumber(ctx, gomock.Any()).Return(&types.Block{}, nil).Times(5) + oldestBlock = findOldestSupportedBlock(ctx, mockClient, 0, 200) + assert.Equal(t, uint64(151), oldestBlock) - tests := []struct { - name string - args args - }{ - { - name: "free and free combo", - args: args{ - ctx: context.Background(), - inspectionCfg: InspectionConfig{ - ScanAPIURL: "https://rpc.ankr.com/eth", - ProxyAPIURL: "https://rpc.ankr.com/eth", - }, - }, - }, - } - for _, tt := range tests { - t.Run( - tt.name, func(t *testing.T) { - pai := &ProxyAPIInspector{} - stats, err := pai.detectOffset(tt.args.ctx, tt.args.inspectionCfg) - if err != nil { - t.Log(err) - } + // Test when the client returns an error for all blocks + mockClient.EXPECT().BlockByNumber(ctx, gomock.Any()).Return(nil, errors.New("block not found")).AnyTimes() - t.Log(stats) - }, - ) - } + oldestBlock = findOldestSupportedBlock(ctx, mockClient, 0, 200) + assert.Equal(t, uint64(200), oldestBlock) }