diff --git a/Makefile b/Makefile index 9a2a599f..1a448b4e 100644 --- a/Makefile +++ b/Makefile @@ -82,11 +82,10 @@ mocks: $(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: - go test -v -count=1 -coverprofile=coverage.out ./... + go test -v -count=1 -short -coverprofile=coverage.out ./... .PHONY: coverage coverage: diff --git a/ethereum/client.go b/ethereum/client.go index d58bc72f..8614d135 100644 --- a/ethereum/client.go +++ b/ethereum/client.go @@ -26,11 +26,28 @@ import ( log "github.com/sirupsen/logrus" ) +// EthClient is the original interface from go-ethereum. +type EthClient interface { + ethereum.ChainReader + ethereum.ChainStateReader + ethereum.TransactionReader + ethereum.LogFilterer + ethereum.ContractCaller + BlockNumber(ctx context.Context) (uint64, error) + BlockByNumber(ctx context.Context, blockNum *big.Int) (*types.Block, error) +} + // RPCClient is a wrapper implementation of the RPC client. type RPCClient interface { Close() + Call(result interface{}, method string, args ...interface{}) error CallContext(ctx context.Context, result interface{}, method string, args ...interface{}) error - Subscribe(ctx context.Context, channel interface{}, args ...interface{}) (domain.ClientSubscription, error) +} + +// Subscriber subscribes to Ethereum namespaces. +type Subscriber interface { + RPCClient + Subscribe(ctx context.Context, namespace string, channel interface{}, args ...interface{}) (domain.ClientSubscription, error) } // Client is an interface encompassing all ethereum actions @@ -82,6 +99,7 @@ var maxBackoff = 1 * time.Minute type streamEthClient struct { apiName string rpcClient RPCClient + subscriber Subscriber retryInterval time.Duration isWebsocket bool @@ -331,7 +349,7 @@ func (e *streamEthClient) SubscribeToHead(ctx context.Context) (domain.HeaderCh, log.Debug("subscribing to blockchain head") recvCh := make(chan *types.Header) sendCh := make(chan *types.Header) - sub, err := e.rpcClient.Subscribe(ctx, recvCh, "newHeads") + sub, err := e.subscriber.Subscribe(ctx, "eth", recvCh, "newHeads") if err != nil { return nil, fmt.Errorf("failed to subscribe: %v", err) } @@ -384,8 +402,8 @@ type rpcClient struct { *rpc.Client } -func (rc *rpcClient) Subscribe(ctx context.Context, channel interface{}, args ...interface{}) (domain.ClientSubscription, error) { - sub, err := rc.EthSubscribe(ctx, channel, args...) +func (rc *rpcClient) Subscribe(ctx context.Context, namespace string, channel interface{}, args ...interface{}) (domain.ClientSubscription, error) { + sub, err := rc.Subscribe(ctx, namespace, channel, args...) return sub, err } diff --git a/ethereum/client_test.go b/ethereum/client_test.go index 7ecf3700..f252e04b 100644 --- a/ethereum/client_test.go +++ b/ethereum/client_test.go @@ -20,20 +20,21 @@ const testBlockHash = "0x4fc0862e76691f5312964883954d5c2db35e2b8f7a4f191775a4f50 var testErr = errors.New("test err") -func initClient(t *testing.T) (*streamEthClient, *mocks.MockRPCClient, context.Context) { +func initClient(t *testing.T) (*streamEthClient, *mocks.MockRPCClient, *mocks.MockSubscriber, context.Context) { minBackoff = 1 * time.Millisecond maxBackoff = 1 * time.Millisecond ctx := context.Background() ctrl := gomock.NewController(t) client := mocks.NewMockRPCClient(ctrl) + subscriber := mocks.NewMockSubscriber(ctrl) - return &streamEthClient{rpcClient: client}, client, ctx + return &streamEthClient{rpcClient: client, subscriber: subscriber}, client, subscriber, ctx } func TestEthClient_BlockByHash(t *testing.T) { r := require.New(t) - ethClient, client, ctx := initClient(t) + ethClient, client, _, ctx := initClient(t) hash := testBlockHash // verify retry client.EXPECT().CallContext(gomock.Any(), gomock.Any(), blocksByHash, testBlockHash).Return(testErr).Times(1) @@ -50,10 +51,10 @@ func TestEthClient_BlockByHash(t *testing.T) { func TestEthClient_SubscribeToHeader_Err(t *testing.T) { r := require.New(t) - ethClient, client, ctx := initClient(t) + ethClient, _, subscriber, ctx := initClient(t) sub := mock_domain.NewMockClientSubscription(gomock.NewController(t)) - client.EXPECT().Subscribe(gomock.Any(), gomock.Any(), "newHeads").Return(sub, nil).Times(2) + subscriber.EXPECT().Subscribe(gomock.Any(), "eth", gomock.Any(), "newHeads").Return(sub, nil).Times(2) errCh := make(chan error, 1) errCh <- errors.New("subscription encountered some error") sub.EXPECT().Err().Return(errCh).Times(2) @@ -64,9 +65,12 @@ func TestEthClient_SubscribeToHeader_Err(t *testing.T) { headerCh, err = ethClient.SubscribeToHead(ctx) r.NoError(err) + var blocked bool select { case <-time.After(time.Second): // should continue from here + blocked = true case <-headerCh: // should block } + r.True(blocked) } diff --git a/ethereum/mocks/mock_client.go b/ethereum/mocks/mock_client.go index f18ae4d9..9396d2ab 100644 --- a/ethereum/mocks/mock_client.go +++ b/ethereum/mocks/mock_client.go @@ -11,12 +11,292 @@ import ( time "time" ethereum "github.com/ethereum/go-ethereum" + common "github.com/ethereum/go-ethereum/common" types "github.com/ethereum/go-ethereum/core/types" health "github.com/forta-network/forta-core-go/clients/health" domain "github.com/forta-network/forta-core-go/domain" gomock "github.com/golang/mock/gomock" ) +// MockEthClient is a mock of EthClient interface. +type MockEthClient struct { + ctrl *gomock.Controller + recorder *MockEthClientMockRecorder +} + +// MockEthClientMockRecorder is the mock recorder for MockEthClient. +type MockEthClientMockRecorder struct { + mock *MockEthClient +} + +// NewMockEthClient creates a new mock instance. +func NewMockEthClient(ctrl *gomock.Controller) *MockEthClient { + mock := &MockEthClient{ctrl: ctrl} + mock.recorder = &MockEthClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockEthClient) EXPECT() *MockEthClientMockRecorder { + return m.recorder +} + +// BalanceAt mocks base method. +func (m *MockEthClient) BalanceAt(ctx context.Context, account common.Address, blockNumber *big.Int) (*big.Int, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "BalanceAt", ctx, account, blockNumber) + ret0, _ := ret[0].(*big.Int) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// BalanceAt indicates an expected call of BalanceAt. +func (mr *MockEthClientMockRecorder) BalanceAt(ctx, account, blockNumber interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BalanceAt", reflect.TypeOf((*MockEthClient)(nil).BalanceAt), ctx, account, blockNumber) +} + +// BlockByHash mocks base method. +func (m *MockEthClient) BlockByHash(ctx context.Context, hash common.Hash) (*types.Block, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "BlockByHash", ctx, hash) + ret0, _ := ret[0].(*types.Block) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// BlockByHash indicates an expected call of BlockByHash. +func (mr *MockEthClientMockRecorder) BlockByHash(ctx, hash interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BlockByHash", reflect.TypeOf((*MockEthClient)(nil).BlockByHash), ctx, hash) +} + +// BlockByNumber mocks base method. +func (m *MockEthClient) BlockByNumber(ctx context.Context, number *big.Int) (*types.Block, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "BlockByNumber", ctx, number) + ret0, _ := ret[0].(*types.Block) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// BlockByNumber indicates an expected call of BlockByNumber. +func (mr *MockEthClientMockRecorder) BlockByNumber(ctx, number interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BlockByNumber", reflect.TypeOf((*MockEthClient)(nil).BlockByNumber), ctx, number) +} + +// BlockNumber mocks base method. +func (m *MockEthClient) 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 *MockEthClientMockRecorder) BlockNumber(ctx interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BlockNumber", reflect.TypeOf((*MockEthClient)(nil).BlockNumber), ctx) +} + +// CallContract mocks base method. +func (m *MockEthClient) CallContract(ctx context.Context, call ethereum.CallMsg, blockNumber *big.Int) ([]byte, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CallContract", ctx, call, blockNumber) + ret0, _ := ret[0].([]byte) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CallContract indicates an expected call of CallContract. +func (mr *MockEthClientMockRecorder) CallContract(ctx, call, blockNumber interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CallContract", reflect.TypeOf((*MockEthClient)(nil).CallContract), ctx, call, blockNumber) +} + +// CodeAt mocks base method. +func (m *MockEthClient) CodeAt(ctx context.Context, account common.Address, blockNumber *big.Int) ([]byte, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CodeAt", ctx, account, blockNumber) + ret0, _ := ret[0].([]byte) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CodeAt indicates an expected call of CodeAt. +func (mr *MockEthClientMockRecorder) CodeAt(ctx, account, blockNumber interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CodeAt", reflect.TypeOf((*MockEthClient)(nil).CodeAt), ctx, account, blockNumber) +} + +// FilterLogs mocks base method. +func (m *MockEthClient) FilterLogs(ctx context.Context, q ethereum.FilterQuery) ([]types.Log, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FilterLogs", ctx, q) + ret0, _ := ret[0].([]types.Log) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// FilterLogs indicates an expected call of FilterLogs. +func (mr *MockEthClientMockRecorder) FilterLogs(ctx, q interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FilterLogs", reflect.TypeOf((*MockEthClient)(nil).FilterLogs), ctx, q) +} + +// HeaderByHash mocks base method. +func (m *MockEthClient) HeaderByHash(ctx context.Context, hash common.Hash) (*types.Header, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "HeaderByHash", ctx, hash) + ret0, _ := ret[0].(*types.Header) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// HeaderByHash indicates an expected call of HeaderByHash. +func (mr *MockEthClientMockRecorder) HeaderByHash(ctx, hash interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HeaderByHash", reflect.TypeOf((*MockEthClient)(nil).HeaderByHash), ctx, hash) +} + +// HeaderByNumber mocks base method. +func (m *MockEthClient) HeaderByNumber(ctx context.Context, number *big.Int) (*types.Header, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "HeaderByNumber", ctx, number) + ret0, _ := ret[0].(*types.Header) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// HeaderByNumber indicates an expected call of HeaderByNumber. +func (mr *MockEthClientMockRecorder) HeaderByNumber(ctx, number interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HeaderByNumber", reflect.TypeOf((*MockEthClient)(nil).HeaderByNumber), ctx, number) +} + +// NonceAt mocks base method. +func (m *MockEthClient) NonceAt(ctx context.Context, account common.Address, blockNumber *big.Int) (uint64, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "NonceAt", ctx, account, blockNumber) + ret0, _ := ret[0].(uint64) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// NonceAt indicates an expected call of NonceAt. +func (mr *MockEthClientMockRecorder) NonceAt(ctx, account, blockNumber interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NonceAt", reflect.TypeOf((*MockEthClient)(nil).NonceAt), ctx, account, blockNumber) +} + +// StorageAt mocks base method. +func (m *MockEthClient) StorageAt(ctx context.Context, account common.Address, key common.Hash, blockNumber *big.Int) ([]byte, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "StorageAt", ctx, account, key, blockNumber) + ret0, _ := ret[0].([]byte) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// StorageAt indicates an expected call of StorageAt. +func (mr *MockEthClientMockRecorder) StorageAt(ctx, account, key, blockNumber interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StorageAt", reflect.TypeOf((*MockEthClient)(nil).StorageAt), ctx, account, key, blockNumber) +} + +// SubscribeFilterLogs mocks base method. +func (m *MockEthClient) SubscribeFilterLogs(ctx context.Context, q ethereum.FilterQuery, ch chan<- types.Log) (ethereum.Subscription, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SubscribeFilterLogs", ctx, q, ch) + ret0, _ := ret[0].(ethereum.Subscription) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// SubscribeFilterLogs indicates an expected call of SubscribeFilterLogs. +func (mr *MockEthClientMockRecorder) SubscribeFilterLogs(ctx, q, ch interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SubscribeFilterLogs", reflect.TypeOf((*MockEthClient)(nil).SubscribeFilterLogs), ctx, q, ch) +} + +// SubscribeNewHead mocks base method. +func (m *MockEthClient) SubscribeNewHead(ctx context.Context, ch chan<- *types.Header) (ethereum.Subscription, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SubscribeNewHead", ctx, ch) + ret0, _ := ret[0].(ethereum.Subscription) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// SubscribeNewHead indicates an expected call of SubscribeNewHead. +func (mr *MockEthClientMockRecorder) SubscribeNewHead(ctx, ch interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SubscribeNewHead", reflect.TypeOf((*MockEthClient)(nil).SubscribeNewHead), ctx, ch) +} + +// TransactionByHash mocks base method. +func (m *MockEthClient) TransactionByHash(ctx context.Context, txHash common.Hash) (*types.Transaction, bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "TransactionByHash", ctx, txHash) + ret0, _ := ret[0].(*types.Transaction) + ret1, _ := ret[1].(bool) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// TransactionByHash indicates an expected call of TransactionByHash. +func (mr *MockEthClientMockRecorder) TransactionByHash(ctx, txHash interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TransactionByHash", reflect.TypeOf((*MockEthClient)(nil).TransactionByHash), ctx, txHash) +} + +// TransactionCount mocks base method. +func (m *MockEthClient) TransactionCount(ctx context.Context, blockHash common.Hash) (uint, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "TransactionCount", ctx, blockHash) + ret0, _ := ret[0].(uint) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// TransactionCount indicates an expected call of TransactionCount. +func (mr *MockEthClientMockRecorder) TransactionCount(ctx, blockHash interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TransactionCount", reflect.TypeOf((*MockEthClient)(nil).TransactionCount), ctx, blockHash) +} + +// TransactionInBlock mocks base method. +func (m *MockEthClient) TransactionInBlock(ctx context.Context, blockHash common.Hash, index uint) (*types.Transaction, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "TransactionInBlock", ctx, blockHash, index) + ret0, _ := ret[0].(*types.Transaction) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// TransactionInBlock indicates an expected call of TransactionInBlock. +func (mr *MockEthClientMockRecorder) TransactionInBlock(ctx, blockHash, index interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TransactionInBlock", reflect.TypeOf((*MockEthClient)(nil).TransactionInBlock), ctx, blockHash, index) +} + +// TransactionReceipt mocks base method. +func (m *MockEthClient) TransactionReceipt(ctx context.Context, txHash common.Hash) (*types.Receipt, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "TransactionReceipt", ctx, txHash) + ret0, _ := ret[0].(*types.Receipt) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// TransactionReceipt indicates an expected call of TransactionReceipt. +func (mr *MockEthClientMockRecorder) TransactionReceipt(ctx, txHash interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TransactionReceipt", reflect.TypeOf((*MockEthClient)(nil).TransactionReceipt), ctx, txHash) +} + // MockRPCClient is a mock of RPCClient interface. type MockRPCClient struct { ctrl *gomock.Controller @@ -40,6 +320,25 @@ func (m *MockRPCClient) EXPECT() *MockRPCClientMockRecorder { return m.recorder } +// Call mocks base method. +func (m *MockRPCClient) Call(result interface{}, method string, args ...interface{}) error { + m.ctrl.T.Helper() + varargs := []interface{}{result, method} + for _, a := range args { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "Call", varargs...) + ret0, _ := ret[0].(error) + return ret0 +} + +// Call indicates an expected call of Call. +func (mr *MockRPCClientMockRecorder) Call(result, method interface{}, args ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{result, method}, args...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Call", reflect.TypeOf((*MockRPCClient)(nil).Call), varargs...) +} + // CallContext mocks base method. func (m *MockRPCClient) CallContext(ctx context.Context, result interface{}, method string, args ...interface{}) error { m.ctrl.T.Helper() @@ -71,10 +370,83 @@ func (mr *MockRPCClientMockRecorder) Close() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockRPCClient)(nil).Close)) } +// MockSubscriber is a mock of Subscriber interface. +type MockSubscriber struct { + ctrl *gomock.Controller + recorder *MockSubscriberMockRecorder +} + +// MockSubscriberMockRecorder is the mock recorder for MockSubscriber. +type MockSubscriberMockRecorder struct { + mock *MockSubscriber +} + +// NewMockSubscriber creates a new mock instance. +func NewMockSubscriber(ctrl *gomock.Controller) *MockSubscriber { + mock := &MockSubscriber{ctrl: ctrl} + mock.recorder = &MockSubscriberMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockSubscriber) EXPECT() *MockSubscriberMockRecorder { + return m.recorder +} + +// Call mocks base method. +func (m *MockSubscriber) Call(result interface{}, method string, args ...interface{}) error { + m.ctrl.T.Helper() + varargs := []interface{}{result, method} + for _, a := range args { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "Call", varargs...) + ret0, _ := ret[0].(error) + return ret0 +} + +// Call indicates an expected call of Call. +func (mr *MockSubscriberMockRecorder) Call(result, method interface{}, args ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{result, method}, args...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Call", reflect.TypeOf((*MockSubscriber)(nil).Call), varargs...) +} + +// CallContext mocks base method. +func (m *MockSubscriber) CallContext(ctx context.Context, result interface{}, method string, args ...interface{}) error { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, result, method} + for _, a := range args { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "CallContext", varargs...) + ret0, _ := ret[0].(error) + return ret0 +} + +// CallContext indicates an expected call of CallContext. +func (mr *MockSubscriberMockRecorder) CallContext(ctx, result, method interface{}, args ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, result, method}, args...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CallContext", reflect.TypeOf((*MockSubscriber)(nil).CallContext), varargs...) +} + +// Close mocks base method. +func (m *MockSubscriber) Close() { + m.ctrl.T.Helper() + m.ctrl.Call(m, "Close") +} + +// Close indicates an expected call of Close. +func (mr *MockSubscriberMockRecorder) Close() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockSubscriber)(nil).Close)) +} + // Subscribe mocks base method. -func (m *MockRPCClient) Subscribe(ctx context.Context, channel interface{}, args ...interface{}) (domain.ClientSubscription, error) { +func (m *MockSubscriber) Subscribe(ctx context.Context, namespace string, channel interface{}, args ...interface{}) (domain.ClientSubscription, error) { m.ctrl.T.Helper() - varargs := []interface{}{ctx, channel} + varargs := []interface{}{ctx, namespace, channel} for _, a := range args { varargs = append(varargs, a) } @@ -85,10 +457,10 @@ func (m *MockRPCClient) Subscribe(ctx context.Context, channel interface{}, args } // Subscribe indicates an expected call of Subscribe. -func (mr *MockRPCClientMockRecorder) Subscribe(ctx, channel interface{}, args ...interface{}) *gomock.Call { +func (mr *MockSubscriberMockRecorder) Subscribe(ctx, namespace, channel interface{}, args ...interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - varargs := append([]interface{}{ctx, channel}, args...) - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Subscribe", reflect.TypeOf((*MockRPCClient)(nil).Subscribe), varargs...) + varargs := append([]interface{}{ctx, namespace, channel}, args...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Subscribe", reflect.TypeOf((*MockSubscriber)(nil).Subscribe), varargs...) } // MockClient is a mock of Client interface. diff --git a/feeds/blocks.go b/feeds/blocks.go index 48db9334..54970a70 100644 --- a/feeds/blocks.go +++ b/feeds/blocks.go @@ -35,6 +35,7 @@ type blockFeed struct { cache utils.Cache chainID *big.Int tracing bool + logs bool started bool rateLimit *time.Ticker maxBlockAge *time.Duration @@ -56,6 +57,7 @@ type BlockFeedConfig struct { ChainID *big.Int RateLimit *time.Ticker Tracing bool + DisableLogs bool SkipBlocksOlderThan *time.Duration } @@ -313,10 +315,13 @@ func (bf *blockFeed) forEachBlock() error { traces = nil } - logs, err := bf.logsForBlock(blockNumToAnalyze) - if err != nil { - logger.WithError(err).Errorf("error getting logs for block") - continue + var logs []domain.LogEntry + if bf.logs { + logs, err = bf.logsForBlock(blockNumToAnalyze) + if err != nil { + logger.WithError(err).Errorf("error getting logs for block") + continue + } } blockTs, err := block.GetTimestamp() @@ -390,6 +395,7 @@ func NewBlockFeed(ctx context.Context, client ethereum.Client, traceClient ether cache: utils.NewCache(10000), chainID: cfg.ChainID, tracing: cfg.Tracing, + logs: !cfg.DisableLogs, rateLimit: cfg.RateLimit, maxBlockAge: cfg.SkipBlocksOlderThan, subscriptionMode: client.IsWebsocket(), diff --git a/feeds/blocks_test.go b/feeds/blocks_test.go index be7efe1d..d3b598e0 100644 --- a/feeds/blocks_test.go +++ b/feeds/blocks_test.go @@ -81,6 +81,7 @@ func getTestBlockFeed(t *testing.T) (*blockFeed, *mocks.MockClient, *mocks.MockC traceClient: traceClient, cache: cache, tracing: true, + logs: true, maxBlockAge: &maxBlockAge, }, client, traceClient, ctx, cancel } diff --git a/feeds/timeline/timeline.go b/feeds/timeline/timeline.go new file mode 100644 index 00000000..d5f76a5b --- /dev/null +++ b/feeds/timeline/timeline.go @@ -0,0 +1,200 @@ +package timeline + +import ( + "sort" + "sync" + "time" + + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/forta-network/forta-core-go/domain" + "github.com/forta-network/forta-core-go/protocol/settings" + log "github.com/sirupsen/logrus" +) + +// BlockTimeline implements a block feed subscriber and keeps track of the +// latest block in every minute. +type BlockTimeline struct { + threshold int + maxMinutes int + chainMinutes []*Minute // when the block was produced + localMinutes []*Minute // when we handled the block + delay *time.Duration + mu sync.RWMutex +} + +// Minute represents a minute in a chain. +type Minute struct { + HighestBlockNumber uint64 + Timestamp time.Time +} + +// NewBlockTimeline creates a new block timeline. +func NewBlockTimeline(chainID, maxMinutes int) *BlockTimeline { + bt := &BlockTimeline{ + threshold: settings.GetChainSettings(chainID).BlockThreshold, + maxMinutes: maxMinutes, + } + + go bt.cleanup() + + return bt +} + +func (bt *BlockTimeline) cleanup() { + ticker := time.NewTicker(time.Minute) + for { + <-ticker.C + bt.doCleanup() + } +} + +func (bt *BlockTimeline) doCleanup() { + bt.mu.Lock() + defer bt.mu.Unlock() + + currSize := len(bt.chainMinutes) + if currSize > bt.maxMinutes { + extra := currSize - bt.maxMinutes + bt.chainMinutes = bt.chainMinutes[extra:] // release oldest + } + + currSize = len(bt.localMinutes) + if currSize > bt.maxMinutes { + extra := currSize - bt.maxMinutes + bt.localMinutes = bt.localMinutes[extra:] // release oldest + } +} + +// HandleBlock handles a block incoming from block feed. +func (bt *BlockTimeline) HandleBlock(evt *domain.BlockEvent) error { + bt.mu.Lock() + defer bt.mu.Unlock() + + blockTs, err := evt.Block.GetTimestamp() + if err != nil { + log.WithError(err).Error("failed to get block timestamp") + return nil + } + delay := time.Since(*blockTs) + bt.delay = &delay + + localMinuteTs := time.Now().Truncate(time.Minute) + + blockMinuteTs := blockTs.Truncate(time.Minute) + blockNum, err := hexutil.DecodeUint64(evt.Block.Number) + if err != nil { + log.WithError(err).Error("failed to decode block number") + } + + var foundBlockMinute bool + for _, minute := range bt.chainMinutes { + if minute.Timestamp.Equal(blockMinuteTs) { + if blockNum > minute.HighestBlockNumber { + minute.HighestBlockNumber = blockNum + } + foundBlockMinute = true + break + } + } + if !foundBlockMinute { + bt.chainMinutes = append(bt.chainMinutes, &Minute{ + HighestBlockNumber: blockNum, + Timestamp: blockMinuteTs, + }) + } + + var foundLocalMinute bool + for _, minute := range bt.localMinutes { + if minute.Timestamp.Equal(localMinuteTs) { + if blockNum > minute.HighestBlockNumber { + minute.HighestBlockNumber = blockNum + } + foundLocalMinute = true + break + } + } + if !foundLocalMinute { + bt.localMinutes = append(bt.localMinutes, &Minute{ + HighestBlockNumber: blockNum, + Timestamp: localMinuteTs, + }) + } + + sort.Slice(bt.chainMinutes, func(i, j int) bool { + return bt.chainMinutes[i].Timestamp.Before(bt.chainMinutes[j].Timestamp) + }) + sort.Slice(bt.localMinutes, func(i, j int) bool { + return bt.localMinutes[i].Timestamp.Before(bt.localMinutes[j].Timestamp) + }) + + return nil +} + +func (bt *BlockTimeline) GetDelay() (time.Duration, bool) { + bt.mu.RLock() + defer bt.mu.RUnlock() + + if bt.delay == nil { + return 0, false + } + return *bt.delay, true +} + +func (bt *BlockTimeline) getLatestUpTo(minutes []*Minute, ts time.Time) (uint64, bool) { + ts = ts.Truncate(time.Minute) + var foundMinute *Minute + for _, minute := range minutes { + if minute.Timestamp.After(ts) { + break + } + foundMinute = minute + } + if foundMinute != nil { + return foundMinute.HighestBlockNumber, true + } + return 0, false +} + +// CalculateLag calculates the block number lag by taking the average of each minute. +func (bt *BlockTimeline) CalculateLag() (float64, bool) { + bt.mu.RLock() + defer bt.mu.RUnlock() + + var ( + total float64 + count float64 + ) + for i, chainMinute := range bt.chainMinutes { + // exclude the last minute + if i == len(bt.chainMinutes)-1 { + break + } + // avoid calculation if we can't find a highest + localMinuteHighest, ok := bt.getLatestUpTo(bt.localMinutes, chainMinute.Timestamp) + if !ok { + continue + } + total += float64(chainMinute.HighestBlockNumber - localMinuteHighest) + count++ + } + if count == 0 { + return 0, false + } + return total / count, true +} + +// EstimateBlockScore estimates the block score based on the lag and the block threshold. +func (bt *BlockTimeline) EstimateBlockScore() (float64, bool) { + lag, ok := bt.CalculateLag() + if !ok { + return 0, false + } + estimate := (float64(bt.threshold) - float64(lag)) / float64(bt.threshold) + if estimate < 0 { + estimate = 0 + } + if estimate > 1 { + estimate = 1 + } + return estimate, true +} diff --git a/feeds/timeline/timeline_test.go b/feeds/timeline/timeline_test.go new file mode 100644 index 00000000..a764080c --- /dev/null +++ b/feeds/timeline/timeline_test.go @@ -0,0 +1,237 @@ +package timeline + +import ( + "context" + "fmt" + "math/big" + "os" + "testing" + "time" + + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/forta-network/forta-core-go/domain" + "github.com/forta-network/forta-core-go/ethereum" + "github.com/forta-network/forta-core-go/feeds" + "github.com/stretchr/testify/require" +) + +func TestTimeline_GlobalHighest(t *testing.T) { + r := require.New(t) + + blockTimeline := &BlockTimeline{ + maxMinutes: 2, + } + + min1 := time.Now().UTC().Truncate(time.Minute) + min2 := min1.Add(time.Minute) + min1Ts := hexutil.EncodeUint64(uint64(min1.Unix())) + min2Ts := hexutil.EncodeUint64(uint64(min2.Unix())) + min3Ts := hexutil.EncodeUint64(uint64(min1.Add(time.Minute * 2).Unix())) + + // add first minute block number + r.NoError(blockTimeline.HandleBlock(blockForTimestamp(min1Ts, "0x1"))) + // replace the first minute number + r.NoError(blockTimeline.HandleBlock(blockForTimestamp(min1Ts, "0x2"))) + // add new one for the next minute + r.NoError(blockTimeline.HandleBlock(blockForTimestamp(min2Ts, "0x3"))) + // replace the second minute number + r.NoError(blockTimeline.HandleBlock(blockForTimestamp(min2Ts, "0x4"))) + // replace the first minute number + r.NoError(blockTimeline.HandleBlock(blockForTimestamp(min1Ts, "0x5"))) + // replace the second minute number + r.NoError(blockTimeline.HandleBlock(blockForTimestamp(min2Ts, "0x6"))) + // add a third minute + r.NoError(blockTimeline.HandleBlock(blockForTimestamp(min3Ts, "0x7"))) + + // verify state + r.EqualValues(5, blockTimeline.chainMinutes[0].HighestBlockNumber) + r.EqualValues(6, blockTimeline.chainMinutes[1].HighestBlockNumber) + r.EqualValues(7, blockTimeline.chainMinutes[2].HighestBlockNumber) + + // cleanup should remove the first minute because of the max minutes num + blockTimeline.doCleanup() + + // verify state + r.EqualValues(6, blockTimeline.chainMinutes[0].HighestBlockNumber) + r.EqualValues(7, blockTimeline.chainMinutes[1].HighestBlockNumber) + + highestGlobal, ok := blockTimeline.getLatestUpTo(blockTimeline.chainMinutes, min2) + r.True(ok) + r.Equal(uint64(6), highestGlobal) +} + +func TestTimeline_CalculateLag(t *testing.T) { + r := require.New(t) + + blockTimeline := NewBlockTimeline(1, 1000000) + + start := time.Now().UTC().Truncate(time.Minute) + + // minute: 1 + // local: 1 + // chain: 2 + // lag: 1 + min1 := start + blockTimeline.localMinutes = append(blockTimeline.localMinutes, &Minute{ + Timestamp: min1, + HighestBlockNumber: 1, + }) + blockTimeline.chainMinutes = append(blockTimeline.chainMinutes, &Minute{ + Timestamp: min1, + HighestBlockNumber: 2, + }) + _, ok := blockTimeline.CalculateLag() + r.False(ok) + + // minute: 2 + // local: 3 + // chain: 8 + // lag: 5 + min2 := min1.Add(time.Minute) + blockTimeline.localMinutes = append(blockTimeline.localMinutes, &Minute{ + Timestamp: min2, + HighestBlockNumber: 3, + }) + blockTimeline.chainMinutes = append(blockTimeline.chainMinutes, &Minute{ + Timestamp: min2, + HighestBlockNumber: 8, + }) + lag, ok := blockTimeline.CalculateLag() + r.True(ok) + r.Equal(float64(1), lag) // because excludes the last minute: (2-1)/1 + + // lags for a while: these minutes have block minutes but no local processing minutes + // minute: 3 + // chain: 12 + // lag: 9 <-- using previous local (12 - 3) + blockTimeline.chainMinutes = append(blockTimeline.chainMinutes, &Minute{ + Timestamp: min1.Add(time.Minute * 2), + HighestBlockNumber: 12, + }) + // minute: 3 + // chain: 16 + // lag: 13 <-- using previous local (16 - 3) + blockTimeline.chainMinutes = append(blockTimeline.chainMinutes, &Minute{ + Timestamp: min1.Add(time.Minute * 3), + HighestBlockNumber: 16, + }) + // minute: 4 + // chain: 18 + // lag: 15 <-- using previous local (18 - 3) + blockTimeline.chainMinutes = append(blockTimeline.chainMinutes, &Minute{ + Timestamp: min1.Add(time.Minute * 4), + HighestBlockNumber: 18, + }) + + // catches up in this minute - processes up to 20 + // minute: 6 + // local: 20 + // chain: 22 + // lag: 2 + min6 := min1.Add(time.Minute * 5) + blockTimeline.localMinutes = append(blockTimeline.localMinutes, &Minute{ + Timestamp: min6, + HighestBlockNumber: 20, + }) + blockTimeline.chainMinutes = append(blockTimeline.chainMinutes, &Minute{ + Timestamp: min6, + HighestBlockNumber: 22, + }) + + // this last minute doesn't matter as last minutes are excluded from calculation + // minute: 7 + // local: 22 + // chain: 26 + // lag: 4 <-- shouldn't matter + min7 := min1.Add(time.Minute * 6) + blockTimeline.localMinutes = append(blockTimeline.localMinutes, &Minute{ + Timestamp: min7, + HighestBlockNumber: 22, + }) + blockTimeline.chainMinutes = append(blockTimeline.chainMinutes, &Minute{ + Timestamp: min7, + HighestBlockNumber: 26, + }) + + // we are iterating by the block minutes during the calculation so + // this local minute doesn't matter + // minute: 8 + // local: 24 + // lag: 2 <-- shouldn't matter, using previous local (24-22) + blockTimeline.localMinutes = append(blockTimeline.localMinutes, &Minute{ + Timestamp: min1.Add(time.Minute * 7), + HighestBlockNumber: 24, + }) + + // the final calculation + lag, ok = blockTimeline.CalculateLag() + r.True(ok) + r.Equal(float64(1+5+9+13+15+2)/float64(6), lag) + estimate, ok := blockTimeline.EstimateBlockScore() + r.True(ok) + r.Equal(0.625, estimate) + + testDelay := time.Second + blockTimeline.delay = &testDelay + delay, ok := blockTimeline.GetDelay() + r.True(ok) + r.Equal(testDelay, delay) +} + +func blockForTimestamp(ts, blockNumber string) *domain.BlockEvent { + return &domain.BlockEvent{ + Block: &domain.Block{ + Timestamp: ts, + Number: blockNumber, + }, + } +} + +func TestRealTimeLag(t *testing.T) { + if os.Getenv("TIMELINE_EXPERIMENT") != "1" { + t.Skip("skipping timeline experiment") + } + + //logrus.SetLevel(logrus.ErrorLevel) + + r := require.New(t) + ctx, cancel := context.WithCancel(context.Background()) + ethClient, err := ethereum.NewStreamEthClient(ctx, "", os.Getenv("JSON_RPC_API")) + ethClient.SetRetryInterval(time.Second * 2) + r.NoError(err) + blockFeed, err := feeds.NewBlockFeed(ctx, ethClient, ethClient, feeds.BlockFeedConfig{ + ChainID: big.NewInt(137), + //DisableLogs: true, + }) + r.NoError(err) + + blockTimeline := &BlockTimeline{} + errCh := blockFeed.Subscribe(func(evt *domain.BlockEvent) error { + // blockTs, _ := evt.Block.GetTimestamp() + // delay := time.Since(*blockTs) + // fmt.Println("delay:", delay) + return blockTimeline.HandleBlock(evt) + }) + + go blockFeed.Start() + + go func() { + longTicker := time.After(time.Minute * 10) + shortTicker := time.NewTicker(time.Minute).C + for { + select { + case <-longTicker: + cancel() + return + case <-shortTicker: + lag, ok := blockTimeline.CalculateLag() + if !ok { + continue + } + fmt.Println("lag at", time.Now().Truncate(time.Minute).Format(time.RFC3339), ":", lag, "blocks") + } + } + }() + + <-errCh +} diff --git a/go.mod b/go.mod index a6fa2cbd..f9cc680d 100644 --- a/go.mod +++ b/go.mod @@ -29,7 +29,6 @@ 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 @@ -196,6 +195,7 @@ 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/api.go b/inspect/api.go index d63100fb..b0d2aa7f 100644 --- a/inspect/api.go +++ b/inspect/api.go @@ -9,8 +9,11 @@ import ( "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/rpc" "github.com/forta-network/forta-core-go/domain" + "github.com/forta-network/forta-core-go/ethereum" + "github.com/forta-network/forta-core-go/registry" ) const ( @@ -18,8 +21,29 @@ const ( latestBlock = "latest" ) +// RPCDialContextFunc dials the RPC endpoint and creates an RPC client. +type RPCDialContextFunc func(ctx context.Context, rawurl string) (ethereum.RPCClient, error) + +// EthClientDialContextFunc dials the Ethereum client. +type EthClientDialContextFunc func(ctx context.Context, rawurl string) (ethereum.EthClient, error) + +// RegistryNewClientFunc creates a new registry client. +type RegistryNewClientFunc func(ctx context.Context, cfg registry.ClientConfig) (registry.Client, error) + +var ( + RPCDialContext RPCDialContextFunc = func(ctx context.Context, rawurl string) (ethereum.RPCClient, error) { + return rpc.DialContext(ctx, rawurl) + } + EthClientDialContext EthClientDialContextFunc = func(ctx context.Context, rawurl string) (ethereum.EthClient, error) { + return ethclient.DialContext(ctx, rawurl) + } + RegistryNewClient RegistryNewClientFunc = func(ctx context.Context, cfg registry.ClientConfig) (registry.Client, error) { + return registry.NewClient(ctx, cfg) + } +) + // GetBlockResponseHash computes a hash by using some data from the API response. -func GetBlockResponseHash(ctx context.Context, rpcClient *rpc.Client, blockNumber uint64) (string, error) { +func GetBlockResponseHash(ctx context.Context, rpcClient ethereum.RPCClient, blockNumber uint64) (string, error) { var block domain.Block if err := getRpcResponse(ctx, rpcClient, &block, "eth_getBlockByNumber", hexutil.EncodeUint64(blockNumber), true); err != nil { return "", err @@ -32,7 +56,7 @@ func GetBlockResponseHash(ctx context.Context, rpcClient *rpc.Client, blockNumbe } // GetTraceResponseHash computes a hash by using some data from the API response. -func GetTraceResponseHash(ctx context.Context, rpcClient *rpc.Client, blockNumber uint64) (string, error) { +func GetTraceResponseHash(ctx context.Context, rpcClient ethereum.RPCClient, blockNumber uint64) (string, error) { var traces []*domain.Trace if err := getRpcResponse(ctx, rpcClient, &traces, "trace_block", hexutil.EncodeUint64(blockNumber)); err != nil { return "", err @@ -51,12 +75,12 @@ func hashOf(str string) string { return hex.EncodeToString(hash[:]) } -func getRpcResponse(ctx context.Context, rpcClient *rpc.Client, respData interface{}, method string, args ...interface{}) error { +func getRpcResponse(ctx context.Context, rpcClient ethereum.RPCClient, respData interface{}, method string, args ...interface{}) error { return rpcClient.CallContext(ctx, &respData, method, args...) } // GetNetworkID gets the network ID from net_version. -func GetNetworkID(ctx context.Context, rpcClient *rpc.Client) (*big.Int, error) { +func GetNetworkID(ctx context.Context, rpcClient ethereum.RPCClient) (*big.Int, error) { var resultStr string err := rpcClient.CallContext(ctx, &resultStr, "net_version") if err == nil { @@ -73,7 +97,7 @@ func GetNetworkID(ctx context.Context, rpcClient *rpc.Client) (*big.Int, error) } // GetChainID gets the chain ID from eth_chainId. -func GetChainID(ctx context.Context, rpcClient *rpc.Client) (*big.Int, error) { +func GetChainID(ctx context.Context, rpcClient ethereum.RPCClient) (*big.Int, error) { var result string err := rpcClient.CallContext(ctx, &result, "eth_chainId") if err != nil { @@ -83,7 +107,7 @@ func GetChainID(ctx context.Context, rpcClient *rpc.Client) (*big.Int, error) { } // GetChainOrNetworkID gets the chain ID from either of eth_chainId or net_version. -func GetChainOrNetworkID(ctx context.Context, rpcClient *rpc.Client) (*big.Int, error) { +func GetChainOrNetworkID(ctx context.Context, rpcClient ethereum.RPCClient) (*big.Int, error) { num, err1 := GetChainID(ctx, rpcClient) if err1 == nil { return num, nil @@ -111,7 +135,7 @@ func decodeChainID(numStr string) (*big.Int, error) { func IsETH2Chain(chainId uint64) bool { return chainId == 1 || chainId == 5 } -func SupportsETH2(ctx context.Context, rpcClient *rpc.Client) bool { +func SupportsETH2(ctx context.Context, rpcClient ethereum.RPCClient) bool { chainID, err := GetChainOrNetworkID(ctx, rpcClient) if err != nil { return false @@ -139,9 +163,6 @@ func SupportsETH2(ctx context.Context, rpcClient *rpc.Client) bool { if err := (&nonce).UnmarshalText([]byte(*block.Nonce)); err != nil { return false } - if err != nil { - return false - } if difficulty.Sign() == 0 && nonce.Uint64() == 0 { return true diff --git a/inspect/mocks/mock_proxy_api.go b/inspect/mocks/mock_proxy_api.go index 5636c3a4..01acddc4 100644 --- a/inspect/mocks/mock_proxy_api.go +++ b/inspect/mocks/mock_proxy_api.go @@ -3,65 +3,3 @@ // 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 16918218..2b10e46d 100644 --- a/inspect/offset.go +++ b/inspect/offset.go @@ -6,6 +6,7 @@ import ( "math/big" "time" + "github.com/forta-network/forta-core-go/ethereum" "github.com/montanaflynn/stats" "golang.org/x/sync/errgroup" ) @@ -19,7 +20,7 @@ type offsetStats struct { func calculateOffsetStats( ctx context.Context, primaryClient, - secondaryClient ProxyAPIClient, + secondaryClient ethereum.EthClient, ) (offsetStats, error) { ds, err := collectOffsetData(ctx, primaryClient, secondaryClient) if err != nil { @@ -31,7 +32,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, secondaryClient ProxyAPIClient) ( +func collectOffsetData(ctx context.Context, primaryClient, secondaryClient ethereum.EthClient) ( []float64, error, ) { maxDuration := time.Second * 20 @@ -102,7 +103,7 @@ func collectOffsetData(ctx context.Context, primaryClient, secondaryClient Proxy } } } -func measureBlockDelay(ctx context.Context, client ProxyAPIClient, blockNum uint64) (int64, error) { +func measureBlockDelay(ctx context.Context, client ethereum.EthClient, blockNum uint64) (int64, error) { t := time.Millisecond * 200 start := time.Now() diff --git a/inspect/offset_test.go b/inspect/offset_test.go index 17ce72f6..2aaa3f09 100644 --- a/inspect/offset_test.go +++ b/inspect/offset_test.go @@ -6,7 +6,7 @@ import ( "testing" "time" - mock_inspect "github.com/forta-network/forta-core-go/inspect/mocks" + mock_ethereum "github.com/forta-network/forta-core-go/ethereum/mocks" "github.com/golang/mock/gomock" "github.com/stretchr/testify/assert" ) @@ -17,8 +17,8 @@ func TestCalculateOffsetStats(t *testing.T) { defer cancel() ctrl := gomock.NewController(t) - primaryClient := mock_inspect.NewMockProxyAPIClient(ctrl) - secondaryClient := mock_inspect.NewMockProxyAPIClient(ctrl) + primaryClient := mock_ethereum.NewMockEthClient(ctrl) + secondaryClient := mock_ethereum.NewMockEthClient(ctrl) // Test when everything is successful primaryClient.EXPECT().BlockNumber(gomock.Any()).Return(uint64(5), nil) diff --git a/inspect/proxy_api.go b/inspect/proxy_api.go index 4a62edc9..34937c64 100644 --- a/inspect/proxy_api.go +++ b/inspect/proxy_api.go @@ -5,9 +5,7 @@ import ( "fmt" "math/big" - "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/ethclient" - "github.com/ethereum/go-ethereum/rpc" + "github.com/forta-network/forta-core-go/ethereum" "github.com/hashicorp/go-multierror" ) @@ -56,11 +54,6 @@ 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" @@ -75,7 +68,7 @@ func (pai *ProxyAPIInspector) Inspect(ctx context.Context, inspectionCfg Inspect results = NewInspectionResults() results.Indicators = defaultIndicators(proxyAPIIndicators) - proxyRPCClient, err := rpc.DialContext(ctx, inspectionCfg.ProxyAPIURL) + proxyRPCClient, err := RPCDialContext(ctx, inspectionCfg.ProxyAPIURL) if err != nil { resultErr = multierror.Append(resultErr, fmt.Errorf("can't dial json-rpc api %w", err)) @@ -91,8 +84,7 @@ func (pai *ProxyAPIInspector) Inspect(ctx context.Context, inspectionCfg Inspect results.Indicators[IndicatorProxyAPIAccessible] = ResultSuccess } - proxyClient := ethclient.NewClient(proxyRPCClient) - + proxyClient, err := EthClientDialContext(ctx, inspectionCfg.ProxyAPIURL) if id, err := GetChainOrNetworkID(ctx, proxyRPCClient); err != nil { resultErr = multierror.Append(resultErr, fmt.Errorf("can't query chain id: %v", err)) results.Indicators[IndicatorProxyAPIChainID] = ResultFailure @@ -130,20 +122,7 @@ func (pai *ProxyAPIInspector) Inspect(ctx context.Context, inspectionCfg Inspect results.Indicators[IndicatorProxyAPIIsETH2] = ResultFailure } - 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) - + scanClient, err := EthClientDialContext(ctx, inspectionCfg.ScanAPIURL) stats, err := calculateOffsetStats(ctx, proxyClient, scanClient) if err != nil { resultErr = multierror.Append(resultErr, fmt.Errorf("can't calculate scan-proxy offset: %w", err)) @@ -164,7 +143,7 @@ func (pai *ProxyAPIInspector) Inspect(ctx context.Context, inspectionCfg Inspect // checkSupportedModules double-checks the functionality of modules that were declared as supported by // the node. func checkSupportedModules( - ctx context.Context, rpcClient *rpc.Client, results *InspectionResults, + ctx context.Context, rpcClient ethereum.RPCClient, results *InspectionResults, ) (resultError error) { // sends net_version under the hood. should prove the node supports net module _, err := GetNetworkID(ctx, rpcClient) @@ -197,7 +176,7 @@ func checkSupportedModules( } // checkHistorySupport inspects block history supports. results earliest provided block -func checkHistorySupport(ctx context.Context, latestBlock uint64, client *ethclient.Client) float64 { +func checkHistorySupport(ctx context.Context, latestBlock uint64, client ethereum.EthClient) float64 { // check for a very old block _, err := client.BlockByNumber(ctx, big.NewInt(VeryOldBlockNumber)) if err == nil { @@ -209,7 +188,7 @@ func checkHistorySupport(ctx context.Context, latestBlock uint64, client *ethcli } // findOldestSupportedBlock returns the earliest block provided by client -func findOldestSupportedBlock(ctx context.Context, client ProxyAPIClient, low, high uint64) uint64 { +func findOldestSupportedBlock(ctx context.Context, client ethereum.EthClient, 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 81390efd..b8bdfbcd 100644 --- a/inspect/proxy_api_test.go +++ b/inspect/proxy_api_test.go @@ -2,22 +2,106 @@ package inspect import ( "context" + "encoding/json" "errors" + "math/big" "testing" types "github.com/ethereum/go-ethereum/core/types" - mock_inspect "github.com/forta-network/forta-core-go/inspect/mocks" + "github.com/forta-network/forta-core-go/ethereum" + mock_ethereum "github.com/forta-network/forta-core-go/ethereum/mocks" + "github.com/forta-network/forta-core-go/registry" + mock_registry "github.com/forta-network/forta-core-go/registry/mocks" "github.com/golang/mock/gomock" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) +func TestProxyAPIInspection(t *testing.T) { + r := require.New(t) + ctx := context.Background() + + ctrl := gomock.NewController(t) + rpcClient := mock_ethereum.NewMockRPCClient(ctrl) + ethClient := mock_ethereum.NewMockEthClient(ctrl) + regClient := mock_registry.NewMockClient(ctrl) + + RPCDialContext = func(ctx context.Context, rawurl string) (ethereum.RPCClient, error) { + return rpcClient, nil + } + EthClientDialContext = func(ctx context.Context, rawurl string) (ethereum.EthClient, error) { + return ethClient, nil + } + RegistryNewClient = func(ctx context.Context, cfg registry.ClientConfig) (registry.Client, error) { + return regClient, nil + } + + rpcClient.EXPECT().CallContext(gomock.Any(), gomock.Any(), "net_version"). + DoAndReturn(func(ctx interface{}, result interface{}, method interface{}, args ...interface{}) error { + json.Unmarshal([]byte(`"0x5"`), result) + return nil + }).AnyTimes() + rpcClient.EXPECT().CallContext(gomock.Any(), gomock.Any(), "eth_chainId"). + DoAndReturn(func(ctx interface{}, result interface{}, method interface{}, args ...interface{}) error { + json.Unmarshal([]byte(`"0x5"`), result) + return nil + }).AnyTimes() + rpcClient.EXPECT().CallContext(gomock.Any(), gomock.Any(), "web3_clientVersion").Return(nil) + ethClient.EXPECT().BlockNumber(gomock.Any()).Return(uint64(123), nil) + rpcClient.EXPECT().CallContext(gomock.Any(), gomock.Any(), "eth_getBlockByNumber", gomock.Any()). + DoAndReturn(func(ctx interface{}, result interface{}, method interface{}, args ...interface{}) error { + json.Unmarshal([]byte(`"{}"`), result) + return nil + }) + + // oldest supported block inspection calls + ethClient.EXPECT().BlockByNumber(gomock.Any(), big.NewInt(VeryOldBlockNumber)).Return(&types.Block{}, nil) + + // eth2 support inspection calls + rpcClient.EXPECT().CallContext(gomock.Any(), gomock.Any(), "eth_getBlockByNumber", "latest", true). + DoAndReturn(func(ctx interface{}, result interface{}, method interface{}, args ...interface{}) error { + json.Unmarshal([]byte(`{"difficulty":"0x0","nonce":"0x0000000000000000"}`), result) + return nil + }) + + // offset inspection calls + ethClient.EXPECT().BlockNumber(gomock.Any()).Return(uint64(123), nil).AnyTimes() + ethClient.EXPECT().BlockByNumber(gomock.Any(), gomock.Any()).Return(&types.Block{}, nil).AnyTimes() + + inspector := &ProxyAPIInspector{} + results, err := inspector.Inspect( + ctx, InspectionConfig{}, + ) + r.NoError(err) + + 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: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + }, results.Metadata, + ) +} + func TestFindOldestSupportedBlock(t *testing.T) { // Create a new Gomock controller ctrl := gomock.NewController(t) - defer ctrl.Finish() - - // Create a mock ethclient.Client - mockClient := mock_inspect.NewMockProxyAPIClient(ctrl) + mockClient := mock_ethereum.NewMockEthClient(ctrl) // Create a test context ctx := context.Background() diff --git a/inspect/registry_api.go b/inspect/registry_api.go index 64851055..9ab30ce6 100644 --- a/inspect/registry_api.go +++ b/inspect/registry_api.go @@ -4,7 +4,6 @@ import ( "context" "fmt" - "github.com/ethereum/go-ethereum/rpc" "github.com/forta-network/forta-core-go/registry" "github.com/hashicorp/go-multierror" ) @@ -40,7 +39,7 @@ func (sai *RegistryAPIInspector) Inspect(ctx context.Context, inspectionCfg Insp results = NewInspectionResults() results.Indicators = defaultIndicators(registryAPIIndicators) - _, err := rpc.DialContext(ctx, inspectionCfg.RegistryAPIURL) + _, err := EthClientDialContext(ctx, inspectionCfg.RegistryAPIURL) if err != nil { resultErr = multierror.Append(resultErr, fmt.Errorf("can't dial json-rpc api: %w", err)) @@ -52,7 +51,7 @@ func (sai *RegistryAPIInspector) Inspect(ctx context.Context, inspectionCfg Insp results.Indicators[IndicatorRegistryAPIAccessible] = ResultSuccess } - regClient, err := registry.NewClient(ctx, registry.ClientConfig{ + regClient, err := RegistryNewClient(ctx, registry.ClientConfig{ JsonRpcUrl: inspectionCfg.RegistryAPIURL, ENSAddress: inspectionCfg.ENSContractAddress, Name: "inspection-registry-client", diff --git a/inspect/registry_api_test.go b/inspect/registry_api_test.go index 761b930b..c014972e 100644 --- a/inspect/registry_api_test.go +++ b/inspect/registry_api_test.go @@ -4,27 +4,40 @@ import ( "context" "testing" - "github.com/kelseyhightower/envconfig" + "github.com/forta-network/forta-core-go/ethereum" + mock_ethereum "github.com/forta-network/forta-core-go/ethereum/mocks" + "github.com/forta-network/forta-core-go/registry" + mock_registry "github.com/forta-network/forta-core-go/registry/mocks" + "github.com/golang/mock/gomock" "github.com/stretchr/testify/require" ) -var testRegistryEnv struct { - RegistryAPI string `envconfig:"registry_api" default:"https://rpc.ankr.com/polygon"` -} - -func init() { - envconfig.MustProcess("test", &testRegistryEnv) -} - func TestRegistryAPIInspection(t *testing.T) { r := require.New(t) + scannerAddr := "0x3DC45b47B7559Ca3b231E5384D825F9B461A0398" + + ctrl := gomock.NewController(t) + rpcClient := mock_ethereum.NewMockRPCClient(ctrl) + ethClient := mock_ethereum.NewMockEthClient(ctrl) + regClient := mock_registry.NewMockClient(ctrl) + + RPCDialContext = func(ctx context.Context, rawurl string) (ethereum.RPCClient, error) { + return rpcClient, nil + } + EthClientDialContext = func(ctx context.Context, rawurl string) (ethereum.EthClient, error) { + return ethClient, nil + } + RegistryNewClient = func(ctx context.Context, cfg registry.ClientConfig) (registry.Client, error) { + return regClient, nil + } + + regClient.EXPECT().GetAssignmentHash(scannerAddr).Return(®istry.AssignmentHash{}, nil) + inspector := &RegistryAPIInspector{} results, err := inspector.Inspect( context.Background(), InspectionConfig{ - RegistryAPIURL: testRegistryEnv.RegistryAPI, - ENSContractAddress: "0x08f42fcc52a9C2F391bF507C4E8688D0b53e1bd7", - ScannerAddress: "0x3DC45b47B7559Ca3b231E5384D825F9B461A0398", + ScannerAddress: scannerAddr, }, ) r.NoError(err) diff --git a/inspect/scan_api.go b/inspect/scan_api.go index dfa3095a..4a5643fb 100644 --- a/inspect/scan_api.go +++ b/inspect/scan_api.go @@ -4,7 +4,7 @@ import ( "context" "fmt" - "github.com/ethereum/go-ethereum/rpc" + "github.com/forta-network/forta-core-go/ethereum" "github.com/hashicorp/go-multierror" ) @@ -48,7 +48,7 @@ func (sai *ScanAPIInspector) Inspect(ctx context.Context, inspectionCfg Inspecti results = NewInspectionResults() results.Indicators = defaultIndicators(scanAPIIndicators) - rpcClient, err := rpc.DialContext(ctx, inspectionCfg.ScanAPIURL) + rpcClient, err := RPCDialContext(ctx, inspectionCfg.ScanAPIURL) if err != nil { resultErr = multierror.Append(resultErr, fmt.Errorf("can't dial json-rpc api %w", err)) @@ -94,7 +94,7 @@ func (sai *ScanAPIInspector) Inspect(ctx context.Context, inspectionCfg Inspecti // checkSupportedModules double-checks the functionality of modules that were declared as supported by // the node. func checkSupportedScanApiModules( - ctx context.Context, rpcClient *rpc.Client, results *InspectionResults, + ctx context.Context, rpcClient ethereum.RPCClient, results *InspectionResults, ) (resultError error) { // sends net_version under the hood. should prove the node supports net module _, err := GetNetworkID(ctx, rpcClient) diff --git a/inspect/scan_api_test.go b/inspect/scan_api_test.go index e612e9ce..b3634ce3 100644 --- a/inspect/scan_api_test.go +++ b/inspect/scan_api_test.go @@ -2,31 +2,63 @@ package inspect import ( "context" + "encoding/json" "testing" - "github.com/kelseyhightower/envconfig" + "github.com/forta-network/forta-core-go/ethereum" + mock_ethereum "github.com/forta-network/forta-core-go/ethereum/mocks" + "github.com/forta-network/forta-core-go/registry" + mock_registry "github.com/forta-network/forta-core-go/registry/mocks" + "github.com/golang/mock/gomock" "github.com/stretchr/testify/require" ) -var testScanEnv struct { - ScanAPI string `envconfig:"scan_api" default:"https://goerli.infura.io/v3/9aa3d95b3bc440fa88ea12eaa4456161"` -} - -func init() { - envconfig.MustProcess("test", &testScanEnv) -} - func TestScanAPIInspection(t *testing.T) { r := require.New(t) - recentBlockNumber := testGetRecentBlockNumber(r, testScanEnv.ScanAPI) + ctrl := gomock.NewController(t) + rpcClient := mock_ethereum.NewMockRPCClient(ctrl) + ethClient := mock_ethereum.NewMockEthClient(ctrl) + regClient := mock_registry.NewMockClient(ctrl) + + RPCDialContext = func(ctx context.Context, rawurl string) (ethereum.RPCClient, error) { + return rpcClient, nil + } + EthClientDialContext = func(ctx context.Context, rawurl string) (ethereum.EthClient, error) { + return ethClient, nil + } + RegistryNewClient = func(ctx context.Context, cfg registry.ClientConfig) (registry.Client, error) { + return regClient, nil + } + + rpcClient.EXPECT().CallContext(gomock.Any(), gomock.Any(), "net_version"). + DoAndReturn(func(ctx interface{}, result interface{}, method interface{}, args ...interface{}) error { + json.Unmarshal([]byte(`"0x5"`), result) + return nil + }).AnyTimes() + rpcClient.EXPECT().CallContext(gomock.Any(), gomock.Any(), "eth_chainId"). + DoAndReturn(func(ctx interface{}, result interface{}, method interface{}, args ...interface{}) error { + json.Unmarshal([]byte(`"0x5"`), result) + return nil + }).AnyTimes() + + // block response hash inspection + rpcClient.EXPECT().CallContext(gomock.Any(), gomock.Any(), "eth_getBlockByNumber", gomock.Any()). + DoAndReturn(func(ctx interface{}, result interface{}, method interface{}, args ...interface{}) error { + json.Unmarshal([]byte(`"{}"`), result) + return nil + }) + + // eth2 support inspection calls + rpcClient.EXPECT().CallContext(gomock.Any(), gomock.Any(), "eth_getBlockByNumber", "latest", true). + DoAndReturn(func(ctx interface{}, result interface{}, method interface{}, args ...interface{}) error { + json.Unmarshal([]byte(`{"difficulty":"0x0","nonce":"0x0000000000000000"}`), result) + return nil + }) inspector := &ScanAPIInspector{} results, err := inspector.Inspect( - context.Background(), InspectionConfig{ - ScanAPIURL: testScanEnv.ScanAPI, - BlockNumber: recentBlockNumber, - }, + context.Background(), InspectionConfig{}, ) r.NoError(err) diff --git a/inspect/scorecalc/pass_fail_calculator.go b/inspect/scorecalc/pass_fail_calculator.go index 21f8cc57..92a87c29 100644 --- a/inspect/scorecalc/pass_fail_calculator.go +++ b/inspect/scorecalc/pass_fail_calculator.go @@ -57,7 +57,7 @@ func (c *chainPassFailCalculator) CalculateScore(results *inspect.InspectionResu if results.Indicators[inspect.IndicatorResourcesMemoryTotal] < c.config.MinTotalMemory { return 0, nil } - + if results.Inputs.IsETH2 && results.Indicators[inspect.IndicatorScanAPIIsETH2] == inspect.ResultFailure { return 0, nil } diff --git a/inspect/trace_api.go b/inspect/trace_api.go index 8faeb2c1..56247865 100644 --- a/inspect/trace_api.go +++ b/inspect/trace_api.go @@ -5,7 +5,6 @@ import ( "fmt" "time" - "github.com/ethereum/go-ethereum/rpc" "github.com/hashicorp/go-multierror" ) @@ -54,7 +53,7 @@ func (tai *TraceAPIInspector) Inspect(ctx context.Context, inspectionCfg Inspect } // checking API access - rpcClient, err := rpc.DialContext(ctx, inspectionCfg.TraceAPIURL) + rpcClient, err := RPCDialContext(ctx, inspectionCfg.TraceAPIURL) if err != nil { resultErr = multierror.Append(resultErr, fmt.Errorf("failed to dial api: %w", err)) diff --git a/inspect/trace_api_test.go b/inspect/trace_api_test.go index 1de5a84f..3e42dc80 100644 --- a/inspect/trace_api_test.go +++ b/inspect/trace_api_test.go @@ -2,32 +2,64 @@ package inspect import ( "context" + "encoding/json" "testing" - "github.com/ethereum/go-ethereum/ethclient" - "github.com/kelseyhightower/envconfig" + "github.com/forta-network/forta-core-go/ethereum" + mock_ethereum "github.com/forta-network/forta-core-go/ethereum/mocks" + "github.com/forta-network/forta-core-go/registry" + mock_registry "github.com/forta-network/forta-core-go/registry/mocks" + "github.com/golang/mock/gomock" "github.com/stretchr/testify/require" ) -var testTraceEnv struct { - TraceAPI string `envconfig:"trace_api" default:"https://rpcapi-tracing.testnet.fantom.network/"` -} - -func init() { - envconfig.MustProcess("test", &testTraceEnv) -} - func TestTraceAPIInspection(t *testing.T) { r := require.New(t) - recentBlockNumber := testGetRecentBlockNumber(r, testTraceEnv.TraceAPI) + ctrl := gomock.NewController(t) + rpcClient := mock_ethereum.NewMockRPCClient(ctrl) + ethClient := mock_ethereum.NewMockEthClient(ctrl) + regClient := mock_registry.NewMockClient(ctrl) + + RPCDialContext = func(ctx context.Context, rawurl string) (ethereum.RPCClient, error) { + return rpcClient, nil + } + EthClientDialContext = func(ctx context.Context, rawurl string) (ethereum.EthClient, error) { + return ethClient, nil + } + RegistryNewClient = func(ctx context.Context, cfg registry.ClientConfig) (registry.Client, error) { + return regClient, nil + } + + rpcClient.EXPECT().CallContext(gomock.Any(), gomock.Any(), "net_version"). + DoAndReturn(func(ctx interface{}, result interface{}, method interface{}, args ...interface{}) error { + json.Unmarshal([]byte(`"4002"`), result) + return nil + }).AnyTimes() + rpcClient.EXPECT().CallContext(gomock.Any(), gomock.Any(), "eth_chainId"). + DoAndReturn(func(ctx interface{}, result interface{}, method interface{}, args ...interface{}) error { + json.Unmarshal([]byte(`"4002"`), result) + return nil + }).AnyTimes() + + // trace response hash inspection + rpcClient.EXPECT().CallContext(gomock.Any(), gomock.Any(), "trace_block", gomock.Any()). + DoAndReturn(func(ctx interface{}, result interface{}, method interface{}, args ...interface{}) error { + json.Unmarshal([]byte(`"{}"`), result) + return nil + }) + + // block response hash inspection + rpcClient.EXPECT().CallContext(gomock.Any(), gomock.Any(), "eth_getBlockByNumber", gomock.Any()). + DoAndReturn(func(ctx interface{}, result interface{}, method interface{}, args ...interface{}) error { + json.Unmarshal([]byte(`"{}"`), result) + return nil + }) inspector := &TraceAPIInspector{} results, err := inspector.Inspect( context.Background(), InspectionConfig{ - TraceAPIURL: testTraceEnv.TraceAPI, - BlockNumber: recentBlockNumber, - CheckTrace: true, + CheckTrace: true, }, ) r.NoError(err) @@ -44,11 +76,3 @@ func TestTraceAPIInspection(t *testing.T) { r.NotEmpty(results.Metadata[MetadataTraceAPIBlockByNumberHash]) r.NotEmpty(results.Metadata[MetadataTraceAPITraceBlockHash]) } - -func testGetRecentBlockNumber(r *require.Assertions, apiURL string) uint64 { - client, err := ethclient.Dial(apiURL) - r.NoError(err) - block, err := client.BlockByNumber(context.Background(), nil) - r.NoError(err) - return block.NumberU64() - 100 -} diff --git a/inspect/validation/validate.go b/inspect/validation/validate.go index 5574cdd0..21e2cc52 100644 --- a/inspect/validation/validate.go +++ b/inspect/validation/validate.go @@ -5,7 +5,7 @@ import ( "strconv" "time" - "github.com/ethereum/go-ethereum/rpc" + "github.com/forta-network/forta-core-go/ethereum" "github.com/forta-network/forta-core-go/inspect" "github.com/hashicorp/go-multierror" "github.com/patrickmn/go-cache" @@ -19,9 +19,9 @@ const ( // InspectionValidator validates inspection results. type InspectionValidator struct { inspectionCfg *inspect.InspectionConfig - scanRpcClient *rpc.Client - traceRpcClient *rpc.Client - proxyRpcClient *rpc.Client + scanRpcClient ethereum.RPCClient + traceRpcClient ethereum.RPCClient + proxyRpcClient ethereum.RPCClient cache *cache.Cache } @@ -32,19 +32,19 @@ func NewValidator(ctx context.Context, inspectionCfg inspect.InspectionConfig) ( validator InspectionValidator err error ) - validator.scanRpcClient, err = rpc.DialContext(ctx, inspectionCfg.ScanAPIURL) + validator.scanRpcClient, err = inspect.RPCDialContext(ctx, inspectionCfg.ScanAPIURL) if err != nil { log.WithError(err).Error("failed to dial scan api") return nil, inspect.ErrReferenceScanAPI } if inspectionCfg.CheckTrace { - validator.traceRpcClient, err = rpc.DialContext(ctx, inspectionCfg.TraceAPIURL) + validator.traceRpcClient, err = inspect.RPCDialContext(ctx, inspectionCfg.TraceAPIURL) if err != nil { log.WithError(err).Error("failed to dial trace api") return nil, inspect.ErrReferenceTraceAPI } } - validator.proxyRpcClient, err = rpc.DialContext(ctx, inspectionCfg.ProxyAPIURL) + validator.proxyRpcClient, err = inspect.RPCDialContext(ctx, inspectionCfg.ProxyAPIURL) if err != nil { log.WithError(err).Error("failed to dial proxy api") return nil, inspect.ErrReferenceProxyAPI diff --git a/inspect/validation/validate_test.go b/inspect/validation/validate_test.go index eb0bfff8..983fd22a 100644 --- a/inspect/validation/validate_test.go +++ b/inspect/validation/validate_test.go @@ -2,48 +2,62 @@ package validation import ( "context" + "encoding/json" "testing" - "github.com/ethereum/go-ethereum/ethclient" + "github.com/forta-network/forta-core-go/ethereum" + mock_ethereum "github.com/forta-network/forta-core-go/ethereum/mocks" "github.com/forta-network/forta-core-go/inspect" - "github.com/kelseyhightower/envconfig" + "github.com/golang/mock/gomock" "github.com/stretchr/testify/require" ) -var testValidateEnv struct { - ScanAPI string `envconfig:"scan_api" default:"https://rpcapi.fantom.network"` - TraceAPI string `envconfig:"trace_api" default:"https://rpcapi-tracing.fantom.network"` -} - -func init() { - envconfig.MustProcess("test", &testValidateEnv) -} - func TestValidateInspectionSuccess(t *testing.T) { ctx := context.Background() r := require.New(t) - recentBlockNumber := testGetRecentBlockNumber(r, testValidateEnv.ScanAPI) + ctrl := gomock.NewController(t) + rpcClient := mock_ethereum.NewMockRPCClient(ctrl) + + inspect.RPCDialContext = func(ctx context.Context, rawurl string) (ethereum.RPCClient, error) { + return rpcClient, nil + } + inspectionCfg := inspect.InspectionConfig{ - ScanAPIURL: testValidateEnv.ScanAPI, - ProxyAPIURL: testValidateEnv.ScanAPI, - TraceAPIURL: testValidateEnv.TraceAPI, - BlockNumber: recentBlockNumber, - CheckTrace: true, + CheckTrace: true, } - // make only scan and trace api inspections using the inspection config - results, err := inspect.InspectAll(ctx, []inspect.Inspector{ - &inspect.ScanAPIInspector{}, - &inspect.ProxyAPIInspector{}, - &inspect.TraceAPIInspector{}, - }, inspectionCfg) - r.NoError(err) + expectedHash := "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + + // trace response hash inspection + rpcClient.EXPECT().CallContext(gomock.Any(), gomock.Any(), "trace_block", gomock.Any()). + DoAndReturn(func(ctx interface{}, result interface{}, method interface{}, args ...interface{}) error { + json.Unmarshal([]byte(`"{}"`), result) + return nil + }) + + // block response hash inspection + rpcClient.EXPECT().CallContext(gomock.Any(), gomock.Any(), "eth_getBlockByNumber", gomock.Any()). + DoAndReturn(func(ctx interface{}, result interface{}, method interface{}, args ...interface{}) error { + json.Unmarshal([]byte(`"{}"`), result) + return nil + }).Times(3) // validate the inspection using the same config validator, err := NewValidator(ctx, inspectionCfg) r.NoError(err) - _, err = validator.Validate(ctx, results) + _, err = validator.Validate(ctx, &inspect.InspectionResults{ + Inputs: inspect.InspectionConfig{ + BlockNumber: 10, + CheckTrace: true, + }, + Metadata: map[string]string{ + inspect.MetadataScanAPIBlockByNumberHash: expectedHash, + inspect.MetadataProxyAPIBlockByNumberHash: expectedHash, + inspect.MetadataTraceAPIBlockByNumberHash: expectedHash, + inspect.MetadataTraceAPITraceBlockHash: expectedHash, + }, + }) r.NoError(err) } @@ -51,42 +65,49 @@ func TestValidateInspectionFail(t *testing.T) { ctx := context.Background() r := require.New(t) - recentBlockNumber := testGetRecentBlockNumber(r, testValidateEnv.ScanAPI) - inspectionCfg1 := inspect.InspectionConfig{ - ScanAPIURL: testValidateEnv.ScanAPI, - ProxyAPIURL: testValidateEnv.ScanAPI, - TraceAPIURL: testValidateEnv.TraceAPI, - BlockNumber: recentBlockNumber, - CheckTrace: true, - } + ctrl := gomock.NewController(t) + rpcClient := mock_ethereum.NewMockRPCClient(ctrl) - // make only scan api inspection - results, err := inspect.InspectAll(ctx, []inspect.Inspector{ - &inspect.ScanAPIInspector{}, - &inspect.ProxyAPIInspector{}, - }, inspectionCfg1) - r.NoError(err) + inspect.RPCDialContext = func(ctx context.Context, rawurl string) (ethereum.RPCClient, error) { + return rpcClient, nil + } - // now let's tamper with the initial conditions so trace inspection result is different - inspectionCfg2 := inspect.InspectionConfig{ - ScanAPIURL: testValidateEnv.ScanAPI, - ProxyAPIURL: testValidateEnv.ScanAPI, - TraceAPIURL: testValidateEnv.TraceAPI, - BlockNumber: recentBlockNumber - 10, - CheckTrace: true, + inspectionCfg := inspect.InspectionConfig{ + CheckTrace: true, } - // make only trace api inspection - traceResults, err := inspect.InspectAll(ctx, []inspect.Inspector{ - &inspect.TraceAPIInspector{}, - }, inspectionCfg2) - r.NoError(err) - results.CopyFrom(traceResults) + expectedHash := "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + unexpectedHash := "foobar" + + // trace response hash inspection + rpcClient.EXPECT().CallContext(gomock.Any(), gomock.Any(), "trace_block", gomock.Any()). + DoAndReturn(func(ctx interface{}, result interface{}, method interface{}, args ...interface{}) error { + json.Unmarshal([]byte(`"{}"`), result) + return nil + }) + + // block response hash inspection + rpcClient.EXPECT().CallContext(gomock.Any(), gomock.Any(), "eth_getBlockByNumber", gomock.Any()). + DoAndReturn(func(ctx interface{}, result interface{}, method interface{}, args ...interface{}) error { + json.Unmarshal([]byte(`"{}"`), result) + return nil + }).Times(3) // validate the inspection using the first config - validator, err := NewValidator(ctx, inspectionCfg1) + validator, err := NewValidator(ctx, inspectionCfg) r.NoError(err) - verrs, err := validator.Validate(ctx, results) + verrs, err := validator.Validate(ctx, &inspect.InspectionResults{ + Inputs: inspect.InspectionConfig{ + BlockNumber: 10, + CheckTrace: true, + }, + Metadata: map[string]string{ + inspect.MetadataScanAPIBlockByNumberHash: expectedHash, + inspect.MetadataProxyAPIBlockByNumberHash: expectedHash, + inspect.MetadataTraceAPIBlockByNumberHash: unexpectedHash, + inspect.MetadataTraceAPITraceBlockHash: unexpectedHash, + }, + }) // expect error(s) r.Error(err) @@ -103,11 +124,3 @@ func TestValidateInspectionFail(t *testing.T) { r.True(verrs.HasCode(inspect.ErrResultTraceAPIBlockMismatch.Code())) r.True(verrs.HasCode(inspect.ErrResultTraceAPITraceBlockMismatch.Code())) } - -func testGetRecentBlockNumber(r *require.Assertions, apiURL string) uint64 { - client, err := ethclient.Dial(apiURL) - r.NoError(err) - block, err := client.BlockByNumber(context.Background(), nil) - r.NoError(err) - return block.NumberU64() - 20 -} diff --git a/protocol/settings/chain.go b/protocol/settings/chain.go index 364adc2f..aa454824 100644 --- a/protocol/settings/chain.go +++ b/protocol/settings/chain.go @@ -45,11 +45,11 @@ var allChainSettings = []ChainSettings{ Name: "Optimism", EnableTrace: false, JsonRpcRateLimiting: defaultRateLimiting, - InspectionInterval: 5000, + InspectionInterval: 100, DefaultOffset: 0, - SafeOffset: 500, - BlockThreshold: 10000, + SafeOffset: 5, + BlockThreshold: 100, }, { ChainID: 56, diff --git a/registry/assignments_test.go b/registry/assignments_test.go index d701af59..6cf0cc67 100644 --- a/registry/assignments_test.go +++ b/registry/assignments_test.go @@ -132,6 +132,10 @@ var testExpectedAssignmentList = []*Assignment{ } func TestGetAssignmentList(t *testing.T) { + if testing.Short() { + t.Skip("skipping assignment list test in short mode") + } + r := require.New(t) cfg := defaultConfig