diff --git a/pkg/chain/ethereum/blockcounter/blockcounter.go b/pkg/chain/ethereum/blockcounter/blockcounter.go index 2b822bf..bdf22c5 100644 --- a/pkg/chain/ethereum/blockcounter/blockcounter.go +++ b/pkg/chain/ethereum/blockcounter/blockcounter.go @@ -3,6 +3,7 @@ package blockcounter import ( "context" "fmt" + "github.com/ethereum/go-ethereum" "strconv" "sync" "time" @@ -10,7 +11,6 @@ import ( "github.com/ipfs/go-log" "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/ethclient" ) var logger = log.Logger("keep-block-counter") @@ -155,7 +155,7 @@ func (ebc *EthereumBlockCounter) receiveBlocks() { } // subscribeBlocks creates a subscription to Geth to get each block. -func (ebc *EthereumBlockCounter) subscribeBlocks(ctx context.Context, client *ethclient.Client) error { +func (ebc *EthereumBlockCounter) subscribeBlocks(ctx context.Context, client ethereum.ChainReader) error { errorChan := make(chan error) newBlockChan := make(chan *types.Header) @@ -213,7 +213,7 @@ func (ebc *EthereumBlockCounter) subscribeBlocks(ctx context.Context, client *et return nil } -func CreateBlockCounter(client *ethclient.Client) (*EthereumBlockCounter, error) { +func CreateBlockCounter(client ethereum.ChainReader) (*EthereumBlockCounter, error) { ctx := context.Background() startupBlock, err := client.BlockByNumber( diff --git a/pkg/chain/ethereum/ethutil/ethutil.go b/pkg/chain/ethereum/ethutil/ethutil.go index 6813577..9fb70f5 100644 --- a/pkg/chain/ethereum/ethutil/ethutil.go +++ b/pkg/chain/ethereum/ethutil/ethutil.go @@ -21,6 +21,15 @@ import ( var logger = log.Logger("keep-ethutil") +// EthereumClient wraps the core `bind.ContractBackend` interface with +// some other interfaces allowing to expose additional methods provided +// by client implementations. +type EthereumClient interface { + bind.ContractBackend + ethereum.ChainReader + ethereum.TransactionReader +} + // AddressFromHex converts the passed string to a common.Address and returns it, // unless it is not a valid address, in which case it returns an error. Compare // to common.HexToAddress, which assumes the address is valid and does not @@ -170,13 +179,13 @@ func EstimateGas( } type loggingWrapper struct { - bind.ContractBackend + EthereumClient logger log.EventLogger } func (lw *loggingWrapper) SuggestGasPrice(ctx context.Context) (*big.Int, error) { - price, err := lw.ContractBackend.SuggestGasPrice(ctx) + price, err := lw.EthereumClient.SuggestGasPrice(ctx) if err != nil { lw.logger.Debugf("error requesting gas price suggestion: [%v]", err) @@ -188,7 +197,7 @@ func (lw *loggingWrapper) SuggestGasPrice(ctx context.Context) (*big.Int, error) } func (lw *loggingWrapper) EstimateGas(ctx context.Context, msg ethereum.CallMsg) (uint64, error) { - gas, err := lw.ContractBackend.EstimateGas(ctx, msg) + gas, err := lw.EthereumClient.EstimateGas(ctx, msg) if err != nil { return 0, err @@ -198,9 +207,9 @@ func (lw *loggingWrapper) EstimateGas(ctx context.Context, msg ethereum.CallMsg) return gas, nil } -// WrapCallLogging wraps certain call-related methods on the given `backend` +// WrapCallLogging wraps certain call-related methods on the given `client` // with debug logging sent to the given `logger`. Actual functionality is -// delegated to the passed backend. -func WrapCallLogging(logger log.EventLogger, backend bind.ContractBackend) bind.ContractBackend { - return &loggingWrapper{backend, logger} +// delegated to the passed client. +func WrapCallLogging(logger log.EventLogger, client EthereumClient) EthereumClient { + return &loggingWrapper{client, logger} } diff --git a/pkg/chain/ethereum/ethutil/rate_limiter.go b/pkg/chain/ethereum/ethutil/rate_limiter.go index 2acd0b1..585d304 100644 --- a/pkg/chain/ethereum/ethutil/rate_limiter.go +++ b/pkg/chain/ethereum/ethutil/rate_limiter.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "github.com/ethereum/go-ethereum" - "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "golang.org/x/sync/semaphore" @@ -14,7 +13,7 @@ import ( ) type rateLimiter struct { - bind.ContractBackend + EthereumClient limiter *rate.Limiter semaphore *semaphore.Weighted @@ -43,10 +42,10 @@ type RateLimiterConfig struct { // All types of requests to the contract are rate-limited, // including view function calls. func WrapRateLimiting( - backend bind.ContractBackend, + client EthereumClient, config *RateLimiterConfig, -) bind.ContractBackend { - rateLimiter := &rateLimiter{ContractBackend: backend} +) EthereumClient { + rateLimiter := &rateLimiter{EthereumClient: client} if config.RequestsPerSecondLimit > 0 { rateLimiter.limiter = rate.NewLimiter( @@ -111,7 +110,7 @@ func (rl *rateLimiter) CodeAt( } defer rl.releasePermit() - return rl.ContractBackend.CodeAt(ctx, contract, blockNumber) + return rl.EthereumClient.CodeAt(ctx, contract, blockNumber) } func (rl *rateLimiter) CallContract( @@ -125,7 +124,7 @@ func (rl *rateLimiter) CallContract( } defer rl.releasePermit() - return rl.ContractBackend.CallContract(ctx, call, blockNumber) + return rl.EthereumClient.CallContract(ctx, call, blockNumber) } func (rl *rateLimiter) PendingCodeAt( @@ -138,7 +137,7 @@ func (rl *rateLimiter) PendingCodeAt( } defer rl.releasePermit() - return rl.ContractBackend.PendingCodeAt(ctx, account) + return rl.EthereumClient.PendingCodeAt(ctx, account) } func (rl *rateLimiter) PendingNonceAt( @@ -151,7 +150,7 @@ func (rl *rateLimiter) PendingNonceAt( } defer rl.releasePermit() - return rl.ContractBackend.PendingNonceAt(ctx, account) + return rl.EthereumClient.PendingNonceAt(ctx, account) } func (rl *rateLimiter) SuggestGasPrice( @@ -163,7 +162,7 @@ func (rl *rateLimiter) SuggestGasPrice( } defer rl.releasePermit() - return rl.ContractBackend.SuggestGasPrice(ctx) + return rl.EthereumClient.SuggestGasPrice(ctx) } func (rl *rateLimiter) EstimateGas( @@ -176,7 +175,7 @@ func (rl *rateLimiter) EstimateGas( } defer rl.releasePermit() - return rl.ContractBackend.EstimateGas(ctx, call) + return rl.EthereumClient.EstimateGas(ctx, call) } func (rl *rateLimiter) SendTransaction( @@ -189,7 +188,7 @@ func (rl *rateLimiter) SendTransaction( } defer rl.releasePermit() - return rl.ContractBackend.SendTransaction(ctx, tx) + return rl.EthereumClient.SendTransaction(ctx, tx) } func (rl *rateLimiter) FilterLogs( @@ -202,7 +201,7 @@ func (rl *rateLimiter) FilterLogs( } defer rl.releasePermit() - return rl.ContractBackend.FilterLogs(ctx, query) + return rl.EthereumClient.FilterLogs(ctx, query) } func (rl *rateLimiter) SubscribeFilterLogs( @@ -216,5 +215,123 @@ func (rl *rateLimiter) SubscribeFilterLogs( } defer rl.releasePermit() - return rl.ContractBackend.SubscribeFilterLogs(ctx, query, ch) + return rl.EthereumClient.SubscribeFilterLogs(ctx, query, ch) +} + +func (rl *rateLimiter) BlockByHash( + ctx context.Context, + hash common.Hash, +) (*types.Block, error) { + err := rl.acquirePermit() + if err != nil { + return nil, fmt.Errorf("cannot acquire rate limiter permit: [%v]", err) + } + defer rl.releasePermit() + + return rl.EthereumClient.BlockByHash(ctx, hash) +} + +func (rl *rateLimiter) BlockByNumber( + ctx context.Context, + number *big.Int, +) (*types.Block, error) { + err := rl.acquirePermit() + if err != nil { + return nil, fmt.Errorf("cannot acquire rate limiter permit: [%v]", err) + } + defer rl.releasePermit() + + return rl.EthereumClient.BlockByNumber(ctx, number) +} + +func (rl *rateLimiter) HeaderByHash( + ctx context.Context, + hash common.Hash, +) (*types.Header, error) { + err := rl.acquirePermit() + if err != nil { + return nil, fmt.Errorf("cannot acquire rate limiter permit: [%v]", err) + } + defer rl.releasePermit() + + return rl.EthereumClient.HeaderByHash(ctx, hash) +} + +func (rl *rateLimiter) HeaderByNumber( + ctx context.Context, + number *big.Int, +) (*types.Header, error) { + err := rl.acquirePermit() + if err != nil { + return nil, fmt.Errorf("cannot acquire rate limiter permit: [%v]", err) + } + defer rl.releasePermit() + + return rl.EthereumClient.HeaderByNumber(ctx, number) +} + +func (rl *rateLimiter) TransactionCount( + ctx context.Context, + blockHash common.Hash, +) (uint, error) { + err := rl.acquirePermit() + if err != nil { + return 0, fmt.Errorf("cannot acquire rate limiter permit: [%v]", err) + } + defer rl.releasePermit() + + return rl.EthereumClient.TransactionCount(ctx, blockHash) +} + +func (rl *rateLimiter) TransactionInBlock( + ctx context.Context, + blockHash common.Hash, + index uint, +) (*types.Transaction, error) { + err := rl.acquirePermit() + if err != nil { + return nil, fmt.Errorf("cannot acquire rate limiter permit: [%v]", err) + } + defer rl.releasePermit() + + return rl.EthereumClient.TransactionInBlock(ctx, blockHash, index) +} + +func (rl *rateLimiter) SubscribeNewHead( + ctx context.Context, + ch chan<- *types.Header, +) (ethereum.Subscription, error) { + err := rl.acquirePermit() + if err != nil { + return nil, fmt.Errorf("cannot acquire rate limiter permit: [%v]", err) + } + defer rl.releasePermit() + + return rl.EthereumClient.SubscribeNewHead(ctx, ch) +} + +func (rl *rateLimiter) TransactionByHash( + ctx context.Context, + txHash common.Hash, +) (*types.Transaction, bool, error) { + err := rl.acquirePermit() + if err != nil { + return nil, false, fmt.Errorf("cannot acquire rate limiter permit: [%v]", err) + } + defer rl.releasePermit() + + return rl.EthereumClient.TransactionByHash(ctx, txHash) +} + +func (rl *rateLimiter) TransactionReceipt( + ctx context.Context, + txHash common.Hash, +) (*types.Receipt, error) { + err := rl.acquirePermit() + if err != nil { + return nil, fmt.Errorf("cannot acquire rate limiter permit: [%v]", err) + } + defer rl.releasePermit() + + return rl.EthereumClient.TransactionReceipt(ctx, txHash) } diff --git a/pkg/chain/ethereum/ethutil/rate_limiter_test.go b/pkg/chain/ethereum/ethutil/rate_limiter_test.go index 00d0d8e..b16788d 100644 --- a/pkg/chain/ethereum/ethutil/rate_limiter_test.go +++ b/pkg/chain/ethereum/ethutil/rate_limiter_test.go @@ -3,7 +3,6 @@ package ethutil import ( "context" "github.com/ethereum/go-ethereum" - "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "math/big" @@ -20,14 +19,14 @@ func TestRateLimiter(t *testing.T) { requests := 500 requestDuration := 10 * time.Millisecond - backend := &mockBackend{ + client := &mockEthereumClient{ requestDuration, make([]string, 0), sync.Mutex{}, } - rateLimitingBackend := WrapRateLimiting( - backend, + rateLimitingClient := WrapRateLimiting( + client, &RateLimiterConfig{ RequestsPerSecondLimit: requestsPerSecondLimit, ConcurrencyLimit: concurrencyLimit, @@ -35,7 +34,7 @@ func TestRateLimiter(t *testing.T) { }, ) - for testName, test := range getTests(rateLimitingBackend) { + for testName, test := range getTests(rateLimitingClient) { t.Run(testName, func(t *testing.T) { wg := sync.WaitGroup{} wg.Add(requests) @@ -75,7 +74,7 @@ func TestRateLimiter(t *testing.T) { maxConcurrency := 0 temporaryConcurrency := 0 - for _, event := range backend.events { + for _, event := range client.events { if event == "start" { temporaryConcurrency++ } @@ -109,14 +108,14 @@ func TestRateLimiter_RequestsPerSecondLimitOnly(t *testing.T) { requests := 500 requestDuration := 10 * time.Millisecond - backend := &mockBackend{ + client := &mockEthereumClient{ requestDuration, make([]string, 0), sync.Mutex{}, } - rateLimitingBackend := WrapRateLimiting( - backend, + rateLimitingClient := WrapRateLimiting( + client, &RateLimiterConfig{ RequestsPerSecondLimit: requestsPerSecondLimit, ConcurrencyLimit: concurrencyLimit, @@ -124,7 +123,7 @@ func TestRateLimiter_RequestsPerSecondLimitOnly(t *testing.T) { }, ) - for testName, test := range getTests(rateLimitingBackend) { + for testName, test := range getTests(rateLimitingClient) { t.Run(testName, func(t *testing.T) { wg := sync.WaitGroup{} wg.Add(requests) @@ -176,14 +175,14 @@ func TestRateLimiter_ConcurrencyLimitOnly(t *testing.T) { requests := 500 requestDuration := 10 * time.Millisecond - backend := &mockBackend{ + client := &mockEthereumClient{ requestDuration, make([]string, 0), sync.Mutex{}, } - rateLimitingBackend := WrapRateLimiting( - backend, + rateLimitingClient := WrapRateLimiting( + client, &RateLimiterConfig{ RequestsPerSecondLimit: requestsPerSecondLimit, ConcurrencyLimit: concurrencyLimit, @@ -191,7 +190,7 @@ func TestRateLimiter_ConcurrencyLimitOnly(t *testing.T) { }, ) - for testName, test := range getTests(rateLimitingBackend) { + for testName, test := range getTests(rateLimitingClient) { t.Run(testName, func(t *testing.T) { wg := sync.WaitGroup{} wg.Add(requests) @@ -217,7 +216,7 @@ func TestRateLimiter_ConcurrencyLimitOnly(t *testing.T) { maxConcurrency := 0 temporaryConcurrency := 0 - for _, event := range backend.events { + for _, event := range client.events { if event == "start" { temporaryConcurrency++ } @@ -251,14 +250,14 @@ func TestRateLimiter_AcquirePermitTimout(t *testing.T) { requests := 3 requestDuration := 250 * time.Millisecond - backend := &mockBackend{ + client := &mockEthereumClient{ requestDuration, make([]string, 0), sync.Mutex{}, } - rateLimitingBackend := WrapRateLimiting( - backend, + rateLimitingClient := WrapRateLimiting( + client, &RateLimiterConfig{ RequestsPerSecondLimit: requestsPerSecondLimit, ConcurrencyLimit: concurrencyLimit, @@ -276,7 +275,7 @@ func TestRateLimiter_AcquirePermitTimout(t *testing.T) { go func() { <-startSignal - err := rateLimitingBackend.SendTransaction(context.Background(), nil) + err := rateLimitingClient.SendTransaction(context.Background(), nil) if err != nil { errors <- err } @@ -305,106 +304,179 @@ func TestRateLimiter_AcquirePermitTimout(t *testing.T) { } } -type mockBackend struct { +type mockEthereumClient struct { requestDuration time.Duration events []string mutex sync.Mutex } -func (mb *mockBackend) mockRequest() { - mb.mutex.Lock() - mb.events = append(mb.events, "start") - mb.mutex.Unlock() +func (mec *mockEthereumClient) mockRequest() { + mec.mutex.Lock() + mec.events = append(mec.events, "start") + mec.mutex.Unlock() - time.Sleep(mb.requestDuration) + time.Sleep(mec.requestDuration) - mb.mutex.Lock() - mb.events = append(mb.events, "end") - mb.mutex.Unlock() + mec.mutex.Lock() + mec.events = append(mec.events, "end") + mec.mutex.Unlock() } -func (mb *mockBackend) CodeAt( +func (mec *mockEthereumClient) CodeAt( ctx context.Context, contract common.Address, blockNumber *big.Int, ) ([]byte, error) { - mb.mockRequest() + mec.mockRequest() return nil, nil } -func (mb *mockBackend) CallContract( +func (mec *mockEthereumClient) CallContract( ctx context.Context, call ethereum.CallMsg, blockNumber *big.Int, ) ([]byte, error) { - mb.mockRequest() + mec.mockRequest() return nil, nil } -func (mb *mockBackend) PendingCodeAt( +func (mec *mockEthereumClient) PendingCodeAt( ctx context.Context, account common.Address, ) ([]byte, error) { - mb.mockRequest() + mec.mockRequest() return nil, nil } -func (mb *mockBackend) PendingNonceAt( +func (mec *mockEthereumClient) PendingNonceAt( ctx context.Context, account common.Address, ) (uint64, error) { - mb.mockRequest() + mec.mockRequest() return 0, nil } -func (mb *mockBackend) SuggestGasPrice( +func (mec *mockEthereumClient) SuggestGasPrice( ctx context.Context, ) (*big.Int, error) { - mb.mockRequest() + mec.mockRequest() return nil, nil } -func (mb *mockBackend) EstimateGas( +func (mec *mockEthereumClient) EstimateGas( ctx context.Context, call ethereum.CallMsg, ) (uint64, error) { - mb.mockRequest() + mec.mockRequest() return 0, nil } -func (mb *mockBackend) SendTransaction( +func (mec *mockEthereumClient) SendTransaction( ctx context.Context, tx *types.Transaction, ) error { - mb.mockRequest() + mec.mockRequest() return nil } -func (mb *mockBackend) FilterLogs( +func (mec *mockEthereumClient) FilterLogs( ctx context.Context, query ethereum.FilterQuery, ) ([]types.Log, error) { - mb.mockRequest() + mec.mockRequest() return nil, nil } -func (mb *mockBackend) SubscribeFilterLogs( +func (mec *mockEthereumClient) SubscribeFilterLogs( ctx context.Context, query ethereum.FilterQuery, ch chan<- types.Log, ) (ethereum.Subscription, error) { - mb.mockRequest() + mec.mockRequest() + return nil, nil +} + +func (mec *mockEthereumClient) BlockByHash( + ctx context.Context, + hash common.Hash, +) (*types.Block, error) { + mec.mockRequest() + return nil, nil +} + +func (mec *mockEthereumClient) BlockByNumber( + ctx context.Context, + number *big.Int, +) (*types.Block, error) { + mec.mockRequest() + return nil, nil +} + +func (mec *mockEthereumClient) HeaderByHash( + ctx context.Context, + hash common.Hash, +) (*types.Header, error) { + mec.mockRequest() + return nil, nil +} + +func (mec *mockEthereumClient) HeaderByNumber( + ctx context.Context, + number *big.Int, +) (*types.Header, error) { + mec.mockRequest() + return nil, nil +} + +func (mec *mockEthereumClient) TransactionCount( + ctx context.Context, + blockHash common.Hash, +) (uint, error) { + mec.mockRequest() + return 0, nil +} + +func (mec *mockEthereumClient) TransactionInBlock( + ctx context.Context, + blockHash common.Hash, + index uint, +) (*types.Transaction, error) { + mec.mockRequest() + return nil, nil +} + +func (mec *mockEthereumClient) SubscribeNewHead( + ctx context.Context, + ch chan<- *types.Header, +) (ethereum.Subscription, error) { + mec.mockRequest() + return nil, nil +} + +func (mec *mockEthereumClient) TransactionByHash( + ctx context.Context, + txHash common.Hash, +) (*types.Transaction, bool, error) { + mec.mockRequest() + return nil, false, nil +} + +func (mec *mockEthereumClient) TransactionReceipt( + ctx context.Context, + txHash common.Hash, +) (*types.Receipt, error) { + mec.mockRequest() return nil, nil } func getTests( - backend bind.ContractBackend, + client EthereumClient, ) map[string]struct{ function func() error } { return map[string]struct{ function func() error }{ "test CodeAt": { function: func() error { - _, err := backend.CodeAt( + _, err := client.CodeAt( context.Background(), [20]byte{}, nil, @@ -414,7 +486,7 @@ func getTests( }, "test CallContract": { function: func() error { - _, err := backend.CallContract( + _, err := client.CallContract( context.Background(), ethereum.CallMsg{}, nil, @@ -424,7 +496,7 @@ func getTests( }, "test PendingCodeAt": { function: func() error { - _, err := backend.PendingCodeAt( + _, err := client.PendingCodeAt( context.Background(), [20]byte{}, ) @@ -433,7 +505,7 @@ func getTests( }, "test PendingNonceAt": { function: func() error { - _, err := backend.PendingNonceAt( + _, err := client.PendingNonceAt( context.Background(), [20]byte{}, ) @@ -442,7 +514,7 @@ func getTests( }, "test SuggestGasPrice": { function: func() error { - _, err := backend.SuggestGasPrice( + _, err := client.SuggestGasPrice( context.Background(), ) return err @@ -450,7 +522,7 @@ func getTests( }, "test EstimateGas": { function: func() error { - _, err := backend.EstimateGas( + _, err := client.EstimateGas( context.Background(), ethereum.CallMsg{}, ) @@ -459,7 +531,7 @@ func getTests( }, "test SendTransaction": { function: func() error { - err := backend.SendTransaction( + err := client.SendTransaction( context.Background(), nil, ) @@ -468,7 +540,7 @@ func getTests( }, "test FilterLogs": { function: func() error { - _, err := backend.FilterLogs( + _, err := client.FilterLogs( context.Background(), ethereum.FilterQuery{}, ) @@ -477,7 +549,7 @@ func getTests( }, "test SubscribeFilterLogs": { function: func() error { - _, err := backend.SubscribeFilterLogs( + _, err := client.SubscribeFilterLogs( context.Background(), ethereum.FilterQuery{}, nil, @@ -485,5 +557,87 @@ func getTests( return err }, }, + "test BlockByHash": { + function: func() error { + _, err := client.BlockByHash( + context.Background(), + common.Hash{}, + ) + return err + }, + }, + "test BlockByNumber": { + function: func() error { + _, err := client.BlockByNumber( + context.Background(), + nil, + ) + return err + }, + }, + "test HeaderByHash": { + function: func() error { + _, err := client.HeaderByHash( + context.Background(), + common.Hash{}, + ) + return err + }, + }, + "test HeaderByNumber": { + function: func() error { + _, err := client.HeaderByNumber( + context.Background(), + nil, + ) + return err + }, + }, + "test TransactionCount": { + function: func() error { + _, err := client.TransactionCount( + context.Background(), + common.Hash{}, + ) + return err + }, + }, + "test TransactionInBlock": { + function: func() error { + _, err := client.TransactionInBlock( + context.Background(), + common.Hash{}, + 0, + ) + return err + }, + }, + "test SubscribeNewHead": { + function: func() error { + _, err := client.SubscribeNewHead( + context.Background(), + nil, + ) + return err + }, + }, + "test TransactionByHash": { + function: func() error { + _, _, err := client.TransactionByHash( + context.Background(), + common.Hash{}, + ) + return err + }, + }, + "test TransactionReceipt": { + function: func() error { + _, err := client.TransactionReceipt( + context.Background(), + common.Hash{}, + ) + return err + }, + }, } }