From 21c4cde066f3d6a49072ca338c966265b910583e Mon Sep 17 00:00:00 2001 From: Dimitris Grigoriou Date: Tue, 4 Jun 2024 18:13:40 +0300 Subject: [PATCH] Removed unused old evm client (#13380) * Removed unused old evm client * Add changeset --- .changeset/rude-bulldogs-draw.md | 5 + core/chains/evm/client/chain_client.go | 80 +- core/chains/evm/client/chain_client_test.go | 832 ++++++++++++ core/chains/evm/client/client.go | 380 ------ core/chains/evm/client/client_test.go | 915 ------------- core/chains/evm/client/erroring_node.go | 150 --- core/chains/evm/client/erroring_node_test.go | 101 -- core/chains/evm/client/helpers_test.go | 45 +- core/chains/evm/client/node.go | 1160 ----------------- core/chains/evm/client/node_fsm.go | 259 ---- core/chains/evm/client/node_fsm_test.go | 173 --- core/chains/evm/client/node_lifecycle.go | 451 ------- core/chains/evm/client/node_lifecycle_test.go | 857 ------------ .../evm/client/node_selector_highest_head.go | 32 - .../client/node_selector_highest_head_test.go | 175 --- .../client/node_selector_priority_level.go | 104 -- .../node_selector_priority_level_test.go | 86 -- .../evm/client/node_selector_round_robin.go | 39 - .../client/node_selector_round_robin_test.go | 55 - .../client/node_selector_total_difficulty.go | 43 - .../node_selector_total_difficulty_test.go | 176 --- core/chains/evm/client/node_test.go | 31 - core/chains/evm/client/pool.go | 503 ------- core/chains/evm/client/pool_test.go | 393 ------ core/chains/evm/client/rpc_client.go | 82 ++ core/chains/evm/client/send_only_node.go | 267 ---- .../evm/client/send_only_node_lifecycle.go | 68 - core/chains/evm/client/send_only_node_test.go | 170 --- core/chains/evm/mocks/node.go | 881 ------------- core/chains/evm/mocks/send_only_node.go | 181 --- core/services/chainlink/config_test.go | 4 +- 31 files changed, 1001 insertions(+), 7697 deletions(-) create mode 100644 .changeset/rude-bulldogs-draw.md delete mode 100644 core/chains/evm/client/client.go delete mode 100644 core/chains/evm/client/client_test.go delete mode 100644 core/chains/evm/client/erroring_node.go delete mode 100644 core/chains/evm/client/erroring_node_test.go delete mode 100644 core/chains/evm/client/node.go delete mode 100644 core/chains/evm/client/node_fsm.go delete mode 100644 core/chains/evm/client/node_fsm_test.go delete mode 100644 core/chains/evm/client/node_lifecycle.go delete mode 100644 core/chains/evm/client/node_lifecycle_test.go delete mode 100644 core/chains/evm/client/node_selector_highest_head.go delete mode 100644 core/chains/evm/client/node_selector_highest_head_test.go delete mode 100644 core/chains/evm/client/node_selector_priority_level.go delete mode 100644 core/chains/evm/client/node_selector_priority_level_test.go delete mode 100644 core/chains/evm/client/node_selector_round_robin.go delete mode 100644 core/chains/evm/client/node_selector_round_robin_test.go delete mode 100644 core/chains/evm/client/node_selector_total_difficulty.go delete mode 100644 core/chains/evm/client/node_selector_total_difficulty_test.go delete mode 100644 core/chains/evm/client/node_test.go delete mode 100644 core/chains/evm/client/pool.go delete mode 100644 core/chains/evm/client/pool_test.go delete mode 100644 core/chains/evm/client/send_only_node.go delete mode 100644 core/chains/evm/client/send_only_node_lifecycle.go delete mode 100644 core/chains/evm/client/send_only_node_test.go delete mode 100644 core/chains/evm/mocks/node.go delete mode 100644 core/chains/evm/mocks/send_only_node.go diff --git a/.changeset/rude-bulldogs-draw.md b/.changeset/rude-bulldogs-draw.md new file mode 100644 index 00000000000..709c3af867d --- /dev/null +++ b/.changeset/rude-bulldogs-draw.md @@ -0,0 +1,5 @@ +--- +"chainlink": patch +--- + +Removed deprecated evm client code #internal diff --git a/core/chains/evm/client/chain_client.go b/core/chains/evm/client/chain_client.go index 04b1ff29387..a28d8ab4a9f 100644 --- a/core/chains/evm/client/chain_client.go +++ b/core/chains/evm/client/chain_client.go @@ -19,9 +19,87 @@ import ( evmtypes "github.com/smartcontractkit/chainlink/v2/core/chains/evm/types" ) +const queryTimeout = 10 * time.Second +const BALANCE_OF_ADDRESS_FUNCTION_SELECTOR = "0x70a08231" + var _ Client = (*chainClient)(nil) -// TODO-1663: rename this to client, once the client.go file is deprecated. +//go:generate mockery --quiet --name Client --output ./mocks/ --case=underscore + +// Client is the interface used to interact with an ethereum node. +type Client interface { + Dial(ctx context.Context) error + Close() + // ChainID locally stored for quick access + ConfiguredChainID() *big.Int + // ChainID RPC call + ChainID() (*big.Int, error) + + // NodeStates returns a map of node Name->node state + // It might be nil or empty, e.g. for mock clients etc + NodeStates() map[string]string + + TokenBalance(ctx context.Context, address common.Address, contractAddress common.Address) (*big.Int, error) + BalanceAt(ctx context.Context, account common.Address, blockNumber *big.Int) (*big.Int, error) + LINKBalance(ctx context.Context, address common.Address, linkAddress common.Address) (*commonassets.Link, error) + + // Wrapped RPC methods + CallContext(ctx context.Context, result interface{}, method string, args ...interface{}) error + BatchCallContext(ctx context.Context, b []rpc.BatchElem) error + // BatchCallContextAll calls BatchCallContext for every single node including + // sendonlys. + // CAUTION: This should only be used for mass re-transmitting transactions, it + // might have unexpected effects to use it for anything else. + BatchCallContextAll(ctx context.Context, b []rpc.BatchElem) error + + // HeadByNumber and HeadByHash is a reimplemented version due to a + // difference in how block header hashes are calculated by Parity nodes + // running on Kovan, Avalanche and potentially others. We have to return our own wrapper type to capture the + // correct hash from the RPC response. + HeadByNumber(ctx context.Context, n *big.Int) (*evmtypes.Head, error) + HeadByHash(ctx context.Context, n common.Hash) (*evmtypes.Head, error) + SubscribeNewHead(ctx context.Context, ch chan<- *evmtypes.Head) (ethereum.Subscription, error) + LatestFinalizedBlock(ctx context.Context) (head *evmtypes.Head, err error) + + SendTransactionReturnCode(ctx context.Context, tx *types.Transaction, fromAddress common.Address) (commonclient.SendTxReturnCode, error) + + // Wrapped Geth client methods + // blockNumber can be specified as `nil` to imply latest block + // if blocks, transactions, or receipts are not found - a nil result and an error are returned + // these methods may not be compatible with non Ethereum chains as return types may follow different formats + // suggested options: use HeadByNumber/HeadByHash (above) or CallContext and parse with custom types + SendTransaction(ctx context.Context, tx *types.Transaction) error + CodeAt(ctx context.Context, account common.Address, blockNumber *big.Int) ([]byte, error) + PendingCodeAt(ctx context.Context, account common.Address) ([]byte, error) + PendingNonceAt(ctx context.Context, account common.Address) (uint64, error) + SequenceAt(ctx context.Context, account common.Address, blockNumber *big.Int) (evmtypes.Nonce, error) + TransactionByHash(ctx context.Context, txHash common.Hash) (*types.Transaction, error) + TransactionReceipt(ctx context.Context, txHash common.Hash) (*types.Receipt, error) + BlockByNumber(ctx context.Context, number *big.Int) (*types.Block, error) + BlockByHash(ctx context.Context, hash common.Hash) (*types.Block, error) + FilterLogs(ctx context.Context, q ethereum.FilterQuery) ([]types.Log, error) + SubscribeFilterLogs(ctx context.Context, q ethereum.FilterQuery, ch chan<- types.Log) (ethereum.Subscription, error) + EstimateGas(ctx context.Context, call ethereum.CallMsg) (uint64, error) + SuggestGasPrice(ctx context.Context) (*big.Int, error) + SuggestGasTipCap(ctx context.Context) (*big.Int, error) + LatestBlockHeight(ctx context.Context) (*big.Int, error) + + HeaderByNumber(ctx context.Context, n *big.Int) (*types.Header, error) + HeaderByHash(ctx context.Context, h common.Hash) (*types.Header, error) + + CallContract(ctx context.Context, msg ethereum.CallMsg, blockNumber *big.Int) ([]byte, error) + PendingCallContract(ctx context.Context, msg ethereum.CallMsg) ([]byte, error) + + IsL2() bool + + // Simulate the transaction prior to sending to catch zk out-of-counters errors ahead of time + CheckTxValidity(ctx context.Context, from common.Address, to common.Address, data []byte) *SendError +} + +func ContextWithDefaultTimeout() (ctx context.Context, cancel context.CancelFunc) { + return context.WithTimeout(context.Background(), queryTimeout) +} + type chainClient struct { multiNode commonclient.MultiNode[ *big.Int, diff --git a/core/chains/evm/client/chain_client_test.go b/core/chains/evm/client/chain_client_test.go index 268a552fda4..f18ec539677 100644 --- a/core/chains/evm/client/chain_client_test.go +++ b/core/chains/evm/client/chain_client_test.go @@ -1,25 +1,750 @@ package client_test import ( + "context" + "encoding/json" "errors" + "fmt" "math/big" + "net/http/httptest" + "net/url" + "os" + "strings" + "sync/atomic" "testing" "time" + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/rpc" + pkgerrors "github.com/pkg/errors" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" commonclient "github.com/smartcontractkit/chainlink/v2/common/client" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client/mocks" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/testutils" + evmtypes "github.com/smartcontractkit/chainlink/v2/core/chains/evm/types" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/utils" ) +func mustNewChainClient(t *testing.T, wsURL string, sendonlys ...url.URL) client.Client { + return mustNewChainClientWithChainID(t, wsURL, testutils.FixtureChainID, sendonlys...) +} + +func mustNewChainClientWithChainID(t *testing.T, wsURL string, chainID *big.Int, sendonlys ...url.URL) client.Client { + cfg := client.TestNodePoolConfig{ + NodeSelectionMode: commonclient.NodeSelectionModeRoundRobin, + } + c, err := client.NewChainClientWithTestNode(t, cfg, time.Second*0, cfg.NodeLeaseDuration, wsURL, nil, sendonlys, 42, chainID) + require.NoError(t, err) + return c +} + +func TestEthClient_TransactionReceipt(t *testing.T) { + t.Parallel() + + txHash := "0xb903239f8543d04b5dc1ba6579132b143087c68db1b2168786408fcbce568238" + + mustReadResult := func(t *testing.T, file string) []byte { + response, err := os.ReadFile(file) + require.NoError(t, err) + var resp struct { + Result json.RawMessage `json:"result"` + } + err = json.Unmarshal(response, &resp) + require.NoError(t, err) + return resp.Result + } + + t.Run("happy path", func(t *testing.T) { + result := mustReadResult(t, "../../../testdata/jsonrpc/getTransactionReceipt.json") + + wsURL := testutils.NewWSServer(t, testutils.FixtureChainID, func(method string, params gjson.Result) (resp testutils.JSONRPCResponse) { + switch method { + case "eth_subscribe": + resp.Result = `"0x00"` + resp.Notify = headResult + return + case "eth_unsubscribe": + resp.Result = "true" + return + } + if assert.Equal(t, "eth_getTransactionReceipt", method) && assert.True(t, params.IsArray()) && + assert.Equal(t, txHash, params.Array()[0].String()) { + resp.Result = string(result) + } + return + }).WSURL().String() + + ethClient := mustNewChainClient(t, wsURL) + err := ethClient.Dial(tests.Context(t)) + require.NoError(t, err) + + hash := common.HexToHash(txHash) + receipt, err := ethClient.TransactionReceipt(tests.Context(t), hash) + require.NoError(t, err) + assert.Equal(t, hash, receipt.TxHash) + assert.Equal(t, big.NewInt(11), receipt.BlockNumber) + }) + + t.Run("no tx hash, returns ethereum.NotFound", func(t *testing.T) { + result := mustReadResult(t, "../../../testdata/jsonrpc/getTransactionReceipt_notFound.json") + wsURL := testutils.NewWSServer(t, testutils.FixtureChainID, func(method string, params gjson.Result) (resp testutils.JSONRPCResponse) { + switch method { + case "eth_subscribe": + resp.Result = `"0x00"` + resp.Notify = headResult + return + case "eth_unsubscribe": + resp.Result = "true" + return + } + if assert.Equal(t, "eth_getTransactionReceipt", method) && assert.True(t, params.IsArray()) && + assert.Equal(t, txHash, params.Array()[0].String()) { + resp.Result = string(result) + } + return + }).WSURL().String() + + ethClient := mustNewChainClient(t, wsURL) + err := ethClient.Dial(tests.Context(t)) + require.NoError(t, err) + + hash := common.HexToHash(txHash) + _, err = ethClient.TransactionReceipt(tests.Context(t), hash) + require.Equal(t, ethereum.NotFound, pkgerrors.Cause(err)) + }) +} + +func TestEthClient_PendingNonceAt(t *testing.T) { + t.Parallel() + + address := testutils.NewAddress() + + wsURL := testutils.NewWSServer(t, testutils.FixtureChainID, func(method string, params gjson.Result) (resp testutils.JSONRPCResponse) { + switch method { + case "eth_subscribe": + resp.Result = `"0x00"` + resp.Notify = headResult + return + case "eth_unsubscribe": + resp.Result = "true" + return + } + if !assert.Equal(t, "eth_getTransactionCount", method) || !assert.True(t, params.IsArray()) { + return + } + arr := params.Array() + if assert.Equal(t, strings.ToLower(address.Hex()), strings.ToLower(arr[0].String())) && + assert.Equal(t, "pending", arr[1].String()) { + resp.Result = `"0x100"` + } + return + }).WSURL().String() + + ethClient := mustNewChainClient(t, wsURL) + err := ethClient.Dial(tests.Context(t)) + require.NoError(t, err) + + result, err := ethClient.PendingNonceAt(tests.Context(t), address) + require.NoError(t, err) + + var expected uint64 = 256 + require.Equal(t, result, expected) +} + +func TestEthClient_BalanceAt(t *testing.T) { + t.Parallel() + + largeBalance, _ := big.NewInt(0).SetString("100000000000000000000", 10) + address := testutils.NewAddress() + + cases := []struct { + name string + balance *big.Int + }{ + {"basic", big.NewInt(256)}, + {"larger than signed 64 bit integer", largeBalance}, + } + + for _, test := range cases { + test := test + t.Run(test.name, func(t *testing.T) { + wsURL := testutils.NewWSServer(t, testutils.FixtureChainID, func(method string, params gjson.Result) (resp testutils.JSONRPCResponse) { + switch method { + case "eth_subscribe": + resp.Result = `"0x00"` + resp.Notify = headResult + return + case "eth_unsubscribe": + resp.Result = "true" + return + } + if assert.Equal(t, "eth_getBalance", method) && assert.True(t, params.IsArray()) && + assert.Equal(t, strings.ToLower(address.Hex()), strings.ToLower(params.Array()[0].String())) { + resp.Result = `"` + hexutil.EncodeBig(test.balance) + `"` + } + return + }).WSURL().String() + + ethClient := mustNewChainClient(t, wsURL) + err := ethClient.Dial(tests.Context(t)) + require.NoError(t, err) + + result, err := ethClient.BalanceAt(tests.Context(t), address, nil) + require.NoError(t, err) + assert.Equal(t, test.balance, result) + }) + } +} + +func TestEthClient_LatestBlockHeight(t *testing.T) { + t.Parallel() + + wsURL := testutils.NewWSServer(t, testutils.FixtureChainID, func(method string, params gjson.Result) (resp testutils.JSONRPCResponse) { + switch method { + case "eth_subscribe": + resp.Result = `"0x00"` + resp.Notify = headResult + return + case "eth_unsubscribe": + resp.Result = "true" + return + } + if !assert.Equal(t, "eth_blockNumber", method) { + return + } + resp.Result = `"0x100"` + return + }).WSURL().String() + + ethClient := mustNewChainClient(t, wsURL) + err := ethClient.Dial(tests.Context(t)) + require.NoError(t, err) + + result, err := ethClient.LatestBlockHeight(tests.Context(t)) + require.NoError(t, err) + require.Equal(t, big.NewInt(256), result) +} + +func TestEthClient_GetERC20Balance(t *testing.T) { + t.Parallel() + ctx := tests.Context(t) + + expectedBig, _ := big.NewInt(0).SetString("100000000000000000000000000000000000000", 10) + + cases := []struct { + name string + balance *big.Int + }{ + {"small", big.NewInt(256)}, + {"big", expectedBig}, + } + + for _, test := range cases { + test := test + t.Run(test.name, func(t *testing.T) { + contractAddress := testutils.NewAddress() + userAddress := testutils.NewAddress() + functionSelector := evmtypes.HexToFunctionSelector(client.BALANCE_OF_ADDRESS_FUNCTION_SELECTOR) // balanceOf(address) + txData := utils.ConcatBytes(functionSelector.Bytes(), common.LeftPadBytes(userAddress.Bytes(), utils.EVMWordByteLen)) + + wsURL := testutils.NewWSServer(t, testutils.FixtureChainID, func(method string, params gjson.Result) (resp testutils.JSONRPCResponse) { + switch method { + case "eth_subscribe": + resp.Result = `"0x00"` + resp.Notify = headResult + return + case "eth_unsubscribe": + resp.Result = "true" + return + } + if !assert.Equal(t, "eth_call", method) || !assert.True(t, params.IsArray()) { + return + } + arr := params.Array() + callArgs := arr[0] + if assert.True(t, callArgs.IsObject()) && + assert.Equal(t, strings.ToLower(contractAddress.Hex()), callArgs.Get("to").String()) && + assert.Equal(t, hexutil.Encode(txData), callArgs.Get("data").String()) && + assert.Equal(t, "latest", arr[1].String()) { + resp.Result = `"` + hexutil.EncodeBig(test.balance) + `"` + } + return + }).WSURL().String() + + ethClient := mustNewChainClient(t, wsURL) + err := ethClient.Dial(tests.Context(t)) + require.NoError(t, err) + + result, err := ethClient.TokenBalance(ctx, userAddress, contractAddress) + require.NoError(t, err) + assert.Equal(t, test.balance, result) + }) + } +} + +func TestReceipt_UnmarshalEmptyBlockHash(t *testing.T) { + t.Parallel() + + input := `{ + "transactionHash": "0x444172bef57ad978655171a8af2cfd89baa02a97fcb773067aef7794d6913374", + "gasUsed": "0x1", + "cumulativeGasUsed": "0x1", + "logs": [], + "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "blockNumber": "0x8bf99b", + "blockHash": null + }` + + var receipt types.Receipt + err := json.Unmarshal([]byte(input), &receipt) + require.NoError(t, err) +} + +func TestEthClient_HeaderByNumber(t *testing.T) { + t.Parallel() + + expectedBlockNum := big.NewInt(1) + expectedBlockHash := "0x41800b5c3f1717687d85fc9018faac0a6e90b39deaa0b99e7fe4fe796ddeb26a" + + cases := []struct { + name string + expectedRequestBlock *big.Int + expectedResponseBlock int64 + error error + rpcResp string + }{ + {"happy geth", expectedBlockNum, expectedBlockNum.Int64(), nil, + `{"difficulty":"0xf3a00","extraData":"0xd883010503846765746887676f312e372e318664617277696e","gasLimit":"0xffc001","gasUsed":"0x0","hash":"0x41800b5c3f1717687d85fc9018faac0a6e90b39deaa0b99e7fe4fe796ddeb26a","logsBloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","miner":"0xd1aeb42885a43b72b518182ef893125814811048","mixHash":"0x0f98b15f1a4901a7e9204f3c500a7bd527b3fb2c3340e12176a44b83e414a69e","nonce":"0x0ece08ea8c49dfd9","number":"0x1","parentHash":"0x41941023680923e0fe4d74a34bdac8141f2540e3ae90623718e47d66d1ca4a2d","receiptsRoot":"0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421","sha3Uncles":"0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347","size":"0x218","stateRoot":"0xc7b01007a10da045eacb90385887dd0c38fcb5db7393006bdde24b93873c334b","timestamp":"0x58318da2","totalDifficulty":"0x1f3a00","transactions":[],"transactionsRoot":"0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421","uncles":[]}`}, + {"happy parity", expectedBlockNum, expectedBlockNum.Int64(), nil, + `{"author":"0xd1aeb42885a43b72b518182ef893125814811048","difficulty":"0xf3a00","extraData":"0xd883010503846765746887676f312e372e318664617277696e","gasLimit":"0xffc001","gasUsed":"0x0","hash":"0x41800b5c3f1717687d85fc9018faac0a6e90b39deaa0b99e7fe4fe796ddeb26a","logsBloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","miner":"0xd1aeb42885a43b72b518182ef893125814811048","mixHash":"0x0f98b15f1a4901a7e9204f3c500a7bd527b3fb2c3340e12176a44b83e414a69e","nonce":"0x0ece08ea8c49dfd9","number":"0x1","parentHash":"0x41941023680923e0fe4d74a34bdac8141f2540e3ae90623718e47d66d1ca4a2d","receiptsRoot":"0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421","sealFields":["0xa00f98b15f1a4901a7e9204f3c500a7bd527b3fb2c3340e12176a44b83e414a69e","0x880ece08ea8c49dfd9"],"sha3Uncles":"0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347","size":"0x218","stateRoot":"0xc7b01007a10da045eacb90385887dd0c38fcb5db7393006bdde24b93873c334b","timestamp":"0x58318da2","totalDifficulty":"0x1f3a00","transactions":[],"transactionsRoot":"0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421","uncles":[]}`}, + {"missing header", expectedBlockNum, 0, fmt.Errorf("no live nodes available for chain %s", testutils.FixtureChainID.String()), + `null`}, + } + + for _, test := range cases { + test := test + t.Run(test.name, func(t *testing.T) { + wsURL := testutils.NewWSServer(t, testutils.FixtureChainID, func(method string, params gjson.Result) (resp testutils.JSONRPCResponse) { + switch method { + case "eth_subscribe": + resp.Result = `"0x00"` + resp.Notify = headResult + return + case "eth_unsubscribe": + resp.Result = "true" + return + } + if !assert.Equal(t, "eth_getBlockByNumber", method) || !assert.True(t, params.IsArray()) { + return + } + arr := params.Array() + blockNumStr := arr[0].String() + var blockNum hexutil.Big + err := blockNum.UnmarshalText([]byte(blockNumStr)) + if assert.NoError(t, err) && assert.Equal(t, test.expectedRequestBlock, blockNum.ToInt()) && + assert.Equal(t, false, arr[1].Bool()) { + resp.Result = test.rpcResp + } + return + }).WSURL().String() + + ethClient := mustNewChainClient(t, wsURL) + err := ethClient.Dial(tests.Context(t)) + require.NoError(t, err) + + ctx, cancel := context.WithTimeout(tests.Context(t), 5*time.Second) + result, err := ethClient.HeadByNumber(ctx, expectedBlockNum) + if test.error != nil { + require.Error(t, err, test.error) + } else { + require.NoError(t, err) + require.Equal(t, expectedBlockHash, result.Hash.Hex()) + require.Equal(t, test.expectedResponseBlock, result.Number) + require.Zero(t, testutils.FixtureChainID.Cmp(result.EVMChainID.ToInt())) + } + cancel() + }) + } +} + +func TestEthClient_SendTransaction_NoSecondaryURL(t *testing.T) { + t.Parallel() + + tx := testutils.NewLegacyTransaction(uint64(42), testutils.NewAddress(), big.NewInt(142), 242, big.NewInt(342), []byte{1, 2, 3}) + + wsURL := testutils.NewWSServer(t, testutils.FixtureChainID, func(method string, params gjson.Result) (resp testutils.JSONRPCResponse) { + switch method { + case "eth_subscribe": + resp.Result = `"0x00"` + resp.Notify = headResult + return + case "eth_unsubscribe": + resp.Result = "true" + return + } + if !assert.Equal(t, "eth_sendRawTransaction", method) { + return + } + resp.Result = `"` + tx.Hash().Hex() + `"` + return + }).WSURL().String() + + ethClient := mustNewChainClient(t, wsURL) + err := ethClient.Dial(tests.Context(t)) + require.NoError(t, err) + + err = ethClient.SendTransaction(tests.Context(t), tx) + assert.NoError(t, err) +} + +func TestEthClient_SendTransaction_WithSecondaryURLs(t *testing.T) { + t.Parallel() + + tx := testutils.NewLegacyTransaction(uint64(42), testutils.NewAddress(), big.NewInt(142), 242, big.NewInt(342), []byte{1, 2, 3}) + + wsURL := testutils.NewWSServer(t, testutils.FixtureChainID, func(method string, params gjson.Result) (resp testutils.JSONRPCResponse) { + switch method { + case "eth_subscribe": + resp.Result = `"0x00"` + resp.Notify = headResult + return + case "eth_unsubscribe": + resp.Result = "true" + return + case "eth_sendRawTransaction": + resp.Result = `"` + tx.Hash().Hex() + `"` + } + return + }).WSURL().String() + + rpcSrv := rpc.NewServer() + t.Cleanup(rpcSrv.Stop) + service := sendTxService{chainID: testutils.FixtureChainID} + err := rpcSrv.RegisterName("eth", &service) + require.NoError(t, err) + ts := httptest.NewServer(rpcSrv) + t.Cleanup(ts.Close) + + sendonlyURL, err := url.Parse(ts.URL) + require.NoError(t, err) + + ethClient := mustNewChainClient(t, wsURL, *sendonlyURL, *sendonlyURL) + err = ethClient.Dial(tests.Context(t)) + require.NoError(t, err) + + err = ethClient.SendTransaction(tests.Context(t), tx) + require.NoError(t, err) + + // Unfortunately it's a bit tricky to test this, since there is no + // synchronization. We have to rely on timing instead. + require.Eventually(t, func() bool { return service.sentCount.Load() == int32(2) }, tests.WaitTimeout(t), 500*time.Millisecond) +} + +func TestEthClient_SendTransactionReturnCode(t *testing.T) { + t.Parallel() + + fromAddress := testutils.NewAddress() + tx := testutils.NewLegacyTransaction(uint64(42), testutils.NewAddress(), big.NewInt(142), 242, big.NewInt(342), []byte{1, 2, 3}) + + t.Run("returns Fatal error type when error message is fatal", func(t *testing.T) { + wsURL := testutils.NewWSServer(t, testutils.FixtureChainID, func(method string, params gjson.Result) (resp testutils.JSONRPCResponse) { + switch method { + case "eth_subscribe": + resp.Result = `"0x00"` + resp.Notify = headResult + return + case "eth_unsubscribe": + resp.Result = "true" + return + case "eth_sendRawTransaction": + resp.Result = `"` + tx.Hash().Hex() + `"` + resp.Error.Message = "invalid sender" + } + return + }).WSURL().String() + + ethClient := mustNewChainClient(t, wsURL) + err := ethClient.Dial(tests.Context(t)) + require.NoError(t, err) + + errType, err := ethClient.SendTransactionReturnCode(tests.Context(t), tx, fromAddress) + assert.Error(t, err) + assert.Equal(t, errType, commonclient.Fatal) + }) + + t.Run("returns TransactionAlreadyKnown error type when error message is nonce too low", func(t *testing.T) { + wsURL := testutils.NewWSServer(t, testutils.FixtureChainID, func(method string, params gjson.Result) (resp testutils.JSONRPCResponse) { + switch method { + case "eth_subscribe": + resp.Result = `"0x00"` + resp.Notify = headResult + return + case "eth_unsubscribe": + resp.Result = "true" + return + case "eth_sendRawTransaction": + resp.Result = `"` + tx.Hash().Hex() + `"` + resp.Error.Message = "nonce too low" + } + return + }).WSURL().String() + + ethClient := mustNewChainClient(t, wsURL) + err := ethClient.Dial(tests.Context(t)) + require.NoError(t, err) + + errType, err := ethClient.SendTransactionReturnCode(tests.Context(t), tx, fromAddress) + assert.Error(t, err) + assert.Equal(t, errType, commonclient.TransactionAlreadyKnown) + }) + + t.Run("returns Successful error type when there is no error message", func(t *testing.T) { + wsURL := testutils.NewWSServer(t, testutils.FixtureChainID, func(method string, params gjson.Result) (resp testutils.JSONRPCResponse) { + switch method { + case "eth_subscribe": + resp.Result = `"0x00"` + resp.Notify = headResult + return + case "eth_unsubscribe": + resp.Result = "true" + return + case "eth_sendRawTransaction": + resp.Result = `"` + tx.Hash().Hex() + `"` + } + return + }).WSURL().String() + + ethClient := mustNewChainClient(t, wsURL) + err := ethClient.Dial(tests.Context(t)) + require.NoError(t, err) + + errType, err := ethClient.SendTransactionReturnCode(tests.Context(t), tx, fromAddress) + assert.NoError(t, err) + assert.Equal(t, errType, commonclient.Successful) + }) + + t.Run("returns Underpriced error type when transaction is terminally underpriced", func(t *testing.T) { + wsURL := testutils.NewWSServer(t, testutils.FixtureChainID, func(method string, params gjson.Result) (resp testutils.JSONRPCResponse) { + switch method { + case "eth_subscribe": + resp.Result = `"0x00"` + resp.Notify = headResult + return + case "eth_unsubscribe": + resp.Result = "true" + return + case "eth_sendRawTransaction": + resp.Result = `"` + tx.Hash().Hex() + `"` + resp.Error.Message = "transaction underpriced" + } + return + }).WSURL().String() + + ethClient := mustNewChainClient(t, wsURL) + err := ethClient.Dial(tests.Context(t)) + require.NoError(t, err) + + errType, err := ethClient.SendTransactionReturnCode(tests.Context(t), tx, fromAddress) + assert.Error(t, err) + assert.Equal(t, errType, commonclient.Underpriced) + }) + + t.Run("returns Unsupported error type when error message is queue full", func(t *testing.T) { + wsURL := testutils.NewWSServer(t, testutils.FixtureChainID, func(method string, params gjson.Result) (resp testutils.JSONRPCResponse) { + switch method { + case "eth_subscribe": + resp.Result = `"0x00"` + resp.Notify = headResult + return + case "eth_unsubscribe": + resp.Result = "true" + return + case "eth_sendRawTransaction": + resp.Result = `"` + tx.Hash().Hex() + `"` + resp.Error.Message = "queue full" + } + return + }).WSURL().String() + + ethClient := mustNewChainClient(t, wsURL) + err := ethClient.Dial(tests.Context(t)) + require.NoError(t, err) + + errType, err := ethClient.SendTransactionReturnCode(tests.Context(t), tx, fromAddress) + assert.Error(t, err) + assert.Equal(t, errType, commonclient.Unsupported) + }) + + t.Run("returns Retryable error type when there is a transaction gap", func(t *testing.T) { + wsURL := testutils.NewWSServer(t, testutils.FixtureChainID, func(method string, params gjson.Result) (resp testutils.JSONRPCResponse) { + switch method { + case "eth_subscribe": + resp.Result = `"0x00"` + resp.Notify = headResult + return + case "eth_unsubscribe": + resp.Result = "true" + return + case "eth_sendRawTransaction": + resp.Result = `"` + tx.Hash().Hex() + `"` + resp.Error.Message = "NonceGap" + } + return + }).WSURL().String() + + ethClient := mustNewChainClient(t, wsURL) + err := ethClient.Dial(tests.Context(t)) + require.NoError(t, err) + + errType, err := ethClient.SendTransactionReturnCode(tests.Context(t), tx, fromAddress) + assert.Error(t, err) + assert.Equal(t, errType, commonclient.Retryable) + }) + + t.Run("returns InsufficientFunds error type when the sender address doesn't have enough funds", func(t *testing.T) { + wsURL := testutils.NewWSServer(t, testutils.FixtureChainID, func(method string, params gjson.Result) (resp testutils.JSONRPCResponse) { + switch method { + case "eth_subscribe": + resp.Result = `"0x00"` + resp.Notify = headResult + return + case "eth_unsubscribe": + resp.Result = "true" + return + case "eth_sendRawTransaction": + resp.Result = `"` + tx.Hash().Hex() + `"` + resp.Error.Message = "insufficient funds for transfer" + } + return + }).WSURL().String() + + ethClient := mustNewChainClient(t, wsURL) + err := ethClient.Dial(tests.Context(t)) + require.NoError(t, err) + + errType, err := ethClient.SendTransactionReturnCode(tests.Context(t), tx, fromAddress) + assert.Error(t, err) + assert.Equal(t, errType, commonclient.InsufficientFunds) + }) + + t.Run("returns ExceedsFeeCap error type when gas price is too high for the node", func(t *testing.T) { + wsURL := testutils.NewWSServer(t, testutils.FixtureChainID, func(method string, params gjson.Result) (resp testutils.JSONRPCResponse) { + switch method { + case "eth_subscribe": + resp.Result = `"0x00"` + resp.Notify = headResult + return + case "eth_unsubscribe": + resp.Result = "true" + return + case "eth_sendRawTransaction": + resp.Result = `"` + tx.Hash().Hex() + `"` + resp.Error.Message = "Transaction fee cap exceeded" + } + return + }).WSURL().String() + + ethClient := mustNewChainClient(t, wsURL) + err := ethClient.Dial(tests.Context(t)) + require.NoError(t, err) + + errType, err := ethClient.SendTransactionReturnCode(tests.Context(t), tx, fromAddress) + assert.Error(t, err) + assert.Equal(t, errType, commonclient.ExceedsMaxFee) + }) + + t.Run("returns Unknown error type when the error can't be categorized", func(t *testing.T) { + wsURL := testutils.NewWSServer(t, testutils.FixtureChainID, func(method string, params gjson.Result) (resp testutils.JSONRPCResponse) { + switch method { + case "eth_subscribe": + resp.Result = `"0x00"` + resp.Notify = headResult + return + case "eth_unsubscribe": + resp.Result = "true" + return + case "eth_sendRawTransaction": + resp.Result = `"` + tx.Hash().Hex() + `"` + resp.Error.Message = "some random error" + } + return + }).WSURL().String() + + ethClient := mustNewChainClient(t, wsURL) + err := ethClient.Dial(tests.Context(t)) + require.NoError(t, err) + + errType, err := ethClient.SendTransactionReturnCode(tests.Context(t), tx, fromAddress) + assert.Error(t, err) + assert.Equal(t, errType, commonclient.Unknown) + }) +} + +type sendTxService struct { + chainID *big.Int + sentCount atomic.Int32 +} + +func (x *sendTxService) ChainId(ctx context.Context) (*hexutil.Big, error) { + return (*hexutil.Big)(x.chainID), nil +} + +func (x *sendTxService) SendRawTransaction(ctx context.Context, signRawTx hexutil.Bytes) error { + x.sentCount.Add(1) + return nil +} + +func TestEthClient_SubscribeNewHead(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithTimeout(tests.Context(t), tests.WaitTimeout(t)) + defer cancel() + + chainId := big.NewInt(123456) + wsURL := testutils.NewWSServer(t, chainId, func(method string, params gjson.Result) (resp testutils.JSONRPCResponse) { + if method == "eth_unsubscribe" { + resp.Result = "true" + return + } + assert.Equal(t, "eth_subscribe", method) + if assert.True(t, params.IsArray()) && assert.Equal(t, "newHeads", params.Array()[0].String()) { + resp.Result = `"0x00"` + resp.Notify = headResult + } + return + }).WSURL().String() + + ethClient := mustNewChainClientWithChainID(t, wsURL, chainId) + err := ethClient.Dial(tests.Context(t)) + require.NoError(t, err) + + headCh := make(chan *evmtypes.Head) + sub, err := ethClient.SubscribeNewHead(ctx, headCh) + require.NoError(t, err) + + select { + case err := <-sub.Err(): + t.Fatal(err) + case <-ctx.Done(): + t.Fatal(ctx.Err()) + case h := <-headCh: + require.NotNil(t, h.EVMChainID) + require.Zero(t, chainId.Cmp(h.EVMChainID.ToInt())) + } + sub.Unsubscribe() +} + func newMockRpc(t *testing.T) *mocks.RPCClient { mockRpc := mocks.NewRPCClient(t) mockRpc.On("Dial", mock.Anything).Return(nil).Once() @@ -71,3 +796,110 @@ func TestChainClient_BatchCallContext(t *testing.T) { } }) } + +func TestEthClient_ErroringClient(t *testing.T) { + t.Parallel() + ctx := tests.Context(t) + + // Empty node means there are no active nodes to select from, causing client to always return error. + erroringClient := client.NewChainClientWithEmptyNode(t, commonclient.NodeSelectionModeRoundRobin, time.Second*0, time.Second*0, testutils.FixtureChainID) + + _, err := erroringClient.BalanceAt(ctx, common.Address{}, nil) + require.Equal(t, err, commonclient.ErroringNodeError) + + err = erroringClient.BatchCallContext(ctx, nil) + require.Equal(t, err, commonclient.ErroringNodeError) + + err = erroringClient.BatchCallContextAll(ctx, nil) + require.Equal(t, err, commonclient.ErroringNodeError) + + _, err = erroringClient.BlockByHash(ctx, common.Hash{}) + require.Equal(t, err, commonclient.ErroringNodeError) + + _, err = erroringClient.BlockByNumber(ctx, nil) + require.Equal(t, err, commonclient.ErroringNodeError) + + err = erroringClient.CallContext(ctx, nil, "") + require.Equal(t, err, commonclient.ErroringNodeError) + + _, err = erroringClient.CallContract(ctx, ethereum.CallMsg{}, nil) + require.Equal(t, err, commonclient.ErroringNodeError) + + // TODO-1663: test actual ChainID() call once client.go is deprecated. + id, err := erroringClient.ChainID() + require.Equal(t, id, testutils.FixtureChainID) + //require.Equal(t, err, commonclient.ErroringNodeError) + require.Equal(t, err, nil) + + _, err = erroringClient.CodeAt(ctx, common.Address{}, nil) + require.Equal(t, err, commonclient.ErroringNodeError) + + id = erroringClient.ConfiguredChainID() + require.Equal(t, id, testutils.FixtureChainID) + + err = erroringClient.Dial(ctx) + require.ErrorContains(t, err, "no available nodes for chain") + + _, err = erroringClient.EstimateGas(ctx, ethereum.CallMsg{}) + require.Equal(t, err, commonclient.ErroringNodeError) + + _, err = erroringClient.FilterLogs(ctx, ethereum.FilterQuery{}) + require.Equal(t, err, commonclient.ErroringNodeError) + + _, err = erroringClient.HeaderByHash(ctx, common.Hash{}) + require.Equal(t, err, commonclient.ErroringNodeError) + + _, err = erroringClient.HeaderByNumber(ctx, nil) + require.Equal(t, err, commonclient.ErroringNodeError) + + _, err = erroringClient.HeadByHash(ctx, common.Hash{}) + require.Equal(t, err, commonclient.ErroringNodeError) + + _, err = erroringClient.HeadByNumber(ctx, nil) + require.Equal(t, err, commonclient.ErroringNodeError) + + _, err = erroringClient.LINKBalance(ctx, common.Address{}, common.Address{}) + require.Equal(t, err, commonclient.ErroringNodeError) + + _, err = erroringClient.LatestBlockHeight(ctx) + require.Equal(t, err, commonclient.ErroringNodeError) + + _, err = erroringClient.PendingCodeAt(ctx, common.Address{}) + require.Equal(t, err, commonclient.ErroringNodeError) + + _, err = erroringClient.PendingNonceAt(ctx, common.Address{}) + require.Equal(t, err, commonclient.ErroringNodeError) + + err = erroringClient.SendTransaction(ctx, nil) + require.Equal(t, err, commonclient.ErroringNodeError) + + code, err := erroringClient.SendTransactionReturnCode(ctx, nil, common.Address{}) + require.Equal(t, code, commonclient.Unknown) + require.Equal(t, err, commonclient.ErroringNodeError) + + _, err = erroringClient.SequenceAt(ctx, common.Address{}, nil) + require.Equal(t, err, commonclient.ErroringNodeError) + + _, err = erroringClient.SubscribeFilterLogs(ctx, ethereum.FilterQuery{}, nil) + require.Equal(t, err, commonclient.ErroringNodeError) + + _, err = erroringClient.SubscribeNewHead(ctx, nil) + require.Equal(t, err, commonclient.ErroringNodeError) + + _, err = erroringClient.SuggestGasPrice(ctx) + require.Equal(t, err, commonclient.ErroringNodeError) + + _, err = erroringClient.SuggestGasTipCap(ctx) + require.Equal(t, err, commonclient.ErroringNodeError) + + _, err = erroringClient.TokenBalance(ctx, common.Address{}, common.Address{}) + require.Equal(t, err, commonclient.ErroringNodeError) + + _, err = erroringClient.TransactionByHash(ctx, common.Hash{}) + require.Equal(t, err, commonclient.ErroringNodeError) + + _, err = erroringClient.TransactionReceipt(ctx, common.Hash{}) + require.Equal(t, err, commonclient.ErroringNodeError) +} + +const headResult = client.HeadResult diff --git a/core/chains/evm/client/client.go b/core/chains/evm/client/client.go deleted file mode 100644 index 9628c74b9ab..00000000000 --- a/core/chains/evm/client/client.go +++ /dev/null @@ -1,380 +0,0 @@ -package client - -import ( - "context" - "fmt" - "math/big" - "strings" - "time" - - "github.com/smartcontractkit/chainlink-common/pkg/assets" - "github.com/smartcontractkit/chainlink-common/pkg/logger" - - commonclient "github.com/smartcontractkit/chainlink/v2/common/client" - "github.com/smartcontractkit/chainlink/v2/common/config" - htrktypes "github.com/smartcontractkit/chainlink/v2/common/headtracker/types" - evmtypes "github.com/smartcontractkit/chainlink/v2/core/chains/evm/types" - "github.com/smartcontractkit/chainlink/v2/core/chains/evm/utils" - ubig "github.com/smartcontractkit/chainlink/v2/core/chains/evm/utils/big" - - "github.com/ethereum/go-ethereum" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/hexutil" - "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/rpc" - pkgerrors "github.com/pkg/errors" -) - -const queryTimeout = 10 * time.Second -const BALANCE_OF_ADDRESS_FUNCTION_SELECTOR = "0x70a08231" - -//go:generate mockery --quiet --name Client --output ./mocks/ --case=underscore - -// Client is the interface used to interact with an ethereum node. -type Client interface { - Dial(ctx context.Context) error - Close() - // ChainID locally stored for quick access - ConfiguredChainID() *big.Int - // ChainID RPC call - ChainID() (*big.Int, error) - - // NodeStates returns a map of node Name->node state - // It might be nil or empty, e.g. for mock clients etc - NodeStates() map[string]string - - TokenBalance(ctx context.Context, address common.Address, contractAddress common.Address) (*big.Int, error) - BalanceAt(ctx context.Context, account common.Address, blockNumber *big.Int) (*big.Int, error) - LINKBalance(ctx context.Context, address common.Address, linkAddress common.Address) (*assets.Link, error) - - // Wrapped RPC methods - CallContext(ctx context.Context, result interface{}, method string, args ...interface{}) error - BatchCallContext(ctx context.Context, b []rpc.BatchElem) error - // BatchCallContextAll calls BatchCallContext for every single node including - // sendonlys. - // CAUTION: This should only be used for mass re-transmitting transactions, it - // might have unexpected effects to use it for anything else. - BatchCallContextAll(ctx context.Context, b []rpc.BatchElem) error - - // HeadByNumber and HeadByHash is a reimplemented version due to a - // difference in how block header hashes are calculated by Parity nodes - // running on Kovan, Avalanche and potentially others. We have to return our own wrapper type to capture the - // correct hash from the RPC response. - HeadByNumber(ctx context.Context, n *big.Int) (*evmtypes.Head, error) - HeadByHash(ctx context.Context, n common.Hash) (*evmtypes.Head, error) - SubscribeNewHead(ctx context.Context, ch chan<- *evmtypes.Head) (ethereum.Subscription, error) - LatestFinalizedBlock(ctx context.Context) (head *evmtypes.Head, err error) - - SendTransactionReturnCode(ctx context.Context, tx *types.Transaction, fromAddress common.Address) (commonclient.SendTxReturnCode, error) - - // Wrapped Geth client methods - // blockNumber can be specified as `nil` to imply latest block - // if blocks, transactions, or receipts are not found - a nil result and an error are returned - // these methods may not be compatible with non Ethereum chains as return types may follow different formats - // suggested options: use HeadByNumber/HeadByHash (above) or CallContext and parse with custom types - SendTransaction(ctx context.Context, tx *types.Transaction) error - CodeAt(ctx context.Context, account common.Address, blockNumber *big.Int) ([]byte, error) - PendingCodeAt(ctx context.Context, account common.Address) ([]byte, error) - PendingNonceAt(ctx context.Context, account common.Address) (uint64, error) - SequenceAt(ctx context.Context, account common.Address, blockNumber *big.Int) (evmtypes.Nonce, error) - TransactionByHash(ctx context.Context, txHash common.Hash) (*types.Transaction, error) - TransactionReceipt(ctx context.Context, txHash common.Hash) (*types.Receipt, error) - BlockByNumber(ctx context.Context, number *big.Int) (*types.Block, error) - BlockByHash(ctx context.Context, hash common.Hash) (*types.Block, error) - FilterLogs(ctx context.Context, q ethereum.FilterQuery) ([]types.Log, error) - SubscribeFilterLogs(ctx context.Context, q ethereum.FilterQuery, ch chan<- types.Log) (ethereum.Subscription, error) - EstimateGas(ctx context.Context, call ethereum.CallMsg) (uint64, error) - SuggestGasPrice(ctx context.Context) (*big.Int, error) - SuggestGasTipCap(ctx context.Context) (*big.Int, error) - LatestBlockHeight(ctx context.Context) (*big.Int, error) - - HeaderByNumber(ctx context.Context, n *big.Int) (*types.Header, error) - HeaderByHash(ctx context.Context, h common.Hash) (*types.Header, error) - - CallContract(ctx context.Context, msg ethereum.CallMsg, blockNumber *big.Int) ([]byte, error) - PendingCallContract(ctx context.Context, msg ethereum.CallMsg) ([]byte, error) - - IsL2() bool - - // Simulate the transaction prior to sending to catch zk out-of-counters errors ahead of time - CheckTxValidity(ctx context.Context, from common.Address, to common.Address, data []byte) *SendError -} - -func ContextWithDefaultTimeout() (ctx context.Context, cancel context.CancelFunc) { - return context.WithTimeout(context.Background(), queryTimeout) -} - -// client represents an abstract client that manages connections to -// multiple nodes for a single chain id -type client struct { - logger logger.SugaredLogger - pool *Pool -} - -var _ Client = (*client)(nil) -var _ htrktypes.Client[*evmtypes.Head, ethereum.Subscription, *big.Int, common.Hash] = (*client)(nil) - -// NewClientWithNodes instantiates a client from a list of nodes -// Currently only supports one primary -// -// Deprecated: use [NewChainClient] -func NewClientWithNodes(lggr logger.Logger, selectionMode string, leaseDuration time.Duration, noNewHeadsThreshold time.Duration, primaryNodes []Node, sendOnlyNodes []SendOnlyNode, chainID *big.Int, chainType config.ChainType) (*client, error) { - pool := NewPool(lggr, selectionMode, leaseDuration, noNewHeadsThreshold, primaryNodes, sendOnlyNodes, chainID, chainType) - return &client{ - logger: logger.Sugared(lggr), - pool: pool, - }, nil -} - -// Dial opens websocket connections if necessary and sanity-checks that the -// node's remote chain ID matches the local one -func (client *client) Dial(ctx context.Context) error { - if err := client.pool.Dial(ctx); err != nil { - return pkgerrors.Wrap(err, "failed to dial pool") - } - return nil -} - -func (client *client) Close() { - client.pool.Close() -} - -func (client *client) NodeStates() (states map[string]string) { - states = make(map[string]string) - for _, n := range client.pool.nodes { - states[n.Name()] = n.State().String() - } - for _, s := range client.pool.sendonlys { - states[s.Name()] = s.State().String() - } - return -} - -// CallArgs represents the data used to call the balance method of a contract. -// "To" is the address of the ERC contract. "Data" is the message sent -// to the contract. "From" is the sender address. -type CallArgs struct { - From common.Address `json:"from"` - To common.Address `json:"to"` - Data hexutil.Bytes `json:"data"` -} - -// TokenBalance returns the balance of the given address for the token contract address. -func (client *client) TokenBalance(ctx context.Context, address common.Address, contractAddress common.Address) (*big.Int, error) { - result := "" - numLinkBigInt := new(big.Int) - functionSelector := evmtypes.HexToFunctionSelector(BALANCE_OF_ADDRESS_FUNCTION_SELECTOR) // balanceOf(address) - data := utils.ConcatBytes(functionSelector.Bytes(), common.LeftPadBytes(address.Bytes(), utils.EVMWordByteLen)) - args := CallArgs{ - To: contractAddress, - Data: data, - } - err := client.CallContext(ctx, &result, "eth_call", args, "latest") - if err != nil { - return numLinkBigInt, err - } - if _, ok := numLinkBigInt.SetString(result, 0); !ok { - return nil, fmt.Errorf("failed to parse int: %s", result) - } - return numLinkBigInt, nil -} - -// LINKBalance returns the balance of LINK at the given address -func (client *client) LINKBalance(ctx context.Context, address common.Address, linkAddress common.Address) (*assets.Link, error) { - balance, err := client.TokenBalance(ctx, address, linkAddress) - if err != nil { - return assets.NewLinkFromJuels(0), err - } - return (*assets.Link)(balance), nil -} - -func (client *client) BalanceAt(ctx context.Context, account common.Address, blockNumber *big.Int) (*big.Int, error) { - return client.pool.BalanceAt(ctx, account, blockNumber) -} - -// We wrap the GethClient's `TransactionReceipt` method so that we can ignore the error that arises -// when we're talking to a Parity node that has no receipt yet. -func (client *client) TransactionReceipt(ctx context.Context, txHash common.Hash) (receipt *types.Receipt, err error) { - receipt, err = client.pool.TransactionReceipt(ctx, txHash) - - if err != nil && strings.Contains(err.Error(), "missing required field") { - return nil, ethereum.NotFound - } - return -} - -func (client *client) TransactionByHash(ctx context.Context, txHash common.Hash) (tx *types.Transaction, err error) { - return client.pool.TransactionByHash(ctx, txHash) -} - -func (client *client) ConfiguredChainID() *big.Int { - return client.pool.chainID -} - -func (client *client) ChainID() (*big.Int, error) { - return client.pool.ChainID(), nil -} - -func (client *client) HeaderByNumber(ctx context.Context, n *big.Int) (*types.Header, error) { - return client.pool.HeaderByNumber(ctx, n) -} - -func (client *client) HeaderByHash(ctx context.Context, h common.Hash) (*types.Header, error) { - return client.pool.HeaderByHash(ctx, h) -} - -func (client *client) SendTransactionReturnCode(ctx context.Context, tx *types.Transaction, fromAddress common.Address) (commonclient.SendTxReturnCode, error) { - err := client.SendTransaction(ctx, tx) - returnCode := ClassifySendError(err, nil, client.logger, tx, fromAddress, client.pool.ChainType().IsL2()) - return returnCode, err -} - -// SendTransaction also uses the sendonly HTTP RPC URLs if set -func (client *client) SendTransaction(ctx context.Context, tx *types.Transaction) error { - return client.pool.SendTransaction(ctx, tx) -} - -func (client *client) PendingNonceAt(ctx context.Context, account common.Address) (uint64, error) { - return client.pool.PendingNonceAt(ctx, account) -} - -func (client *client) SequenceAt(ctx context.Context, account common.Address, blockNumber *big.Int) (evmtypes.Nonce, error) { - nonce, err := client.pool.NonceAt(ctx, account, blockNumber) - return evmtypes.Nonce(nonce), err -} - -func (client *client) PendingCodeAt(ctx context.Context, account common.Address) ([]byte, error) { - return client.pool.PendingCodeAt(ctx, account) -} - -func (client *client) EstimateGas(ctx context.Context, call ethereum.CallMsg) (gas uint64, err error) { - return client.pool.EstimateGas(ctx, call) -} - -// SuggestGasPrice calls the RPC node to get a suggested gas price. -// WARNING: It is not recommended to ever use this result for anything -// important. There are a number of issues with asking the RPC node to provide a -// gas estimate; it is not reliable. Unless you really have a good reason to -// use this, you should probably use core node's internal gas estimator -// instead. -func (client *client) SuggestGasPrice(ctx context.Context) (*big.Int, error) { - return client.pool.SuggestGasPrice(ctx) -} - -func (client *client) CallContract(ctx context.Context, msg ethereum.CallMsg, blockNumber *big.Int) ([]byte, error) { - return client.pool.CallContract(ctx, msg, blockNumber) -} - -func (client *client) PendingCallContract(ctx context.Context, msg ethereum.CallMsg) ([]byte, error) { - return client.pool.PendingCallContract(ctx, msg) -} - -func (client *client) CodeAt(ctx context.Context, account common.Address, blockNumber *big.Int) ([]byte, error) { - return client.pool.CodeAt(ctx, account, blockNumber) -} - -func (client *client) BlockByNumber(ctx context.Context, number *big.Int) (*types.Block, error) { - return client.pool.BlockByNumber(ctx, number) -} - -func (client *client) BlockByHash(ctx context.Context, hash common.Hash) (*types.Block, error) { - return client.pool.BlockByHash(ctx, hash) -} - -func (client *client) LatestBlockHeight(ctx context.Context) (*big.Int, error) { - var height big.Int - h, err := client.pool.BlockNumber(ctx) - return height.SetUint64(h), err -} - -func (client *client) HeadByNumber(ctx context.Context, number *big.Int) (head *evmtypes.Head, err error) { - hex := ToBlockNumArg(number) - err = client.pool.CallContext(ctx, &head, "eth_getBlockByNumber", hex, false) - if err != nil { - return nil, err - } - if head == nil { - err = ethereum.NotFound - return - } - head.EVMChainID = ubig.New(client.ConfiguredChainID()) - return -} - -func (client *client) HeadByHash(ctx context.Context, hash common.Hash) (head *evmtypes.Head, err error) { - err = client.pool.CallContext(ctx, &head, "eth_getBlockByHash", hash.Hex(), false) - if err != nil { - return nil, err - } - if head == nil { - err = ethereum.NotFound - return - } - head.EVMChainID = ubig.New(client.ConfiguredChainID()) - return -} - -func ToBlockNumArg(number *big.Int) string { - if number == nil { - return "latest" - } - return hexutil.EncodeBig(number) -} - -func (client *client) FilterLogs(ctx context.Context, q ethereum.FilterQuery) ([]types.Log, error) { - return client.pool.FilterLogs(ctx, q) -} - -func (client *client) SubscribeFilterLogs(ctx context.Context, q ethereum.FilterQuery, ch chan<- types.Log) (ethereum.Subscription, error) { - client.logger.Debugw("evmclient.Client#SubscribeFilterLogs(...)", - "q", q, - ) - return client.pool.SubscribeFilterLogs(ctx, q, ch) -} - -func (client *client) SubscribeNewHead(ctx context.Context, ch chan<- *evmtypes.Head) (ethereum.Subscription, error) { - csf := newChainIDSubForwarder(client.ConfiguredChainID(), ch) - err := csf.start(client.pool.EthSubscribe(ctx, csf.srcCh, "newHeads")) - if err != nil { - return nil, err - } - return csf, nil -} - -func (client *client) EthSubscribe(ctx context.Context, channel chan<- *evmtypes.Head, args ...interface{}) (ethereum.Subscription, error) { - return client.pool.EthSubscribe(ctx, channel, args...) -} - -func (client *client) CallContext(ctx context.Context, result interface{}, method string, args ...interface{}) error { - return client.pool.CallContext(ctx, result, method, args...) -} - -func (client *client) BatchCallContext(ctx context.Context, b []rpc.BatchElem) error { - return client.pool.BatchCallContext(ctx, b) -} - -func (client *client) BatchCallContextAll(ctx context.Context, b []rpc.BatchElem) error { - return client.pool.BatchCallContextAll(ctx, b) -} - -// SuggestGasTipCap calls the RPC node to get a suggested gas tip cap. -// WARNING: It is not recommended to ever use this result for anything -// important. There are a number of issues with asking the RPC node to provide a -// gas estimate; it is not reliable. Unless you really have a good reason to -// use this, you should probably use core node's internal gas estimator -// instead. -func (client *client) SuggestGasTipCap(ctx context.Context) (tipCap *big.Int, err error) { - return client.pool.SuggestGasTipCap(ctx) -} - -func (client *client) IsL2() bool { - return client.pool.ChainType().IsL2() -} - -func (client *client) LatestFinalizedBlock(_ context.Context) (*evmtypes.Head, error) { - return nil, pkgerrors.New("not implemented. client was deprecated. New methods are added only to satisfy type constraints while we are migrating to new alternatives") -} - -func (client *client) CheckTxValidity(ctx context.Context, from common.Address, to common.Address, data []byte) *SendError { - return NewSendError(pkgerrors.New("not implemented. client was deprecated. New methods are added only to satisfy type constraints while we are migrating to new alternatives")) -} diff --git a/core/chains/evm/client/client_test.go b/core/chains/evm/client/client_test.go deleted file mode 100644 index 0aa457ceaca..00000000000 --- a/core/chains/evm/client/client_test.go +++ /dev/null @@ -1,915 +0,0 @@ -package client_test - -import ( - "context" - "encoding/json" - "fmt" - "math/big" - "net/http/httptest" - "net/url" - "os" - "strings" - "sync/atomic" - "testing" - "time" - - "github.com/ethereum/go-ethereum" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/hexutil" - "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/rpc" - pkgerrors "github.com/pkg/errors" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/tidwall/gjson" - - "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" - - commonclient "github.com/smartcontractkit/chainlink/v2/common/client" - - "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client" - "github.com/smartcontractkit/chainlink/v2/core/chains/evm/testutils" - evmtypes "github.com/smartcontractkit/chainlink/v2/core/chains/evm/types" - "github.com/smartcontractkit/chainlink/v2/core/chains/evm/utils" -) - -func mustNewClient(t *testing.T, wsURL string, sendonlys ...url.URL) client.Client { - return mustNewClientWithChainID(t, wsURL, testutils.FixtureChainID, sendonlys...) -} - -func mustNewClientWithChainID(t *testing.T, wsURL string, chainID *big.Int, sendonlys ...url.URL) client.Client { - cfg := client.TestNodePoolConfig{ - NodeSelectionMode: client.NodeSelectionMode_RoundRobin, - } - c, err := client.NewClientWithTestNode(t, cfg, time.Second*0, wsURL, nil, sendonlys, 42, chainID) - require.NoError(t, err) - return c -} - -func mustNewChainClient(t *testing.T, wsURL string, sendonlys ...url.URL) client.Client { - return mustNewChainClientWithChainID(t, wsURL, testutils.FixtureChainID, sendonlys...) -} - -func mustNewChainClientWithChainID(t *testing.T, wsURL string, chainID *big.Int, sendonlys ...url.URL) client.Client { - cfg := client.TestNodePoolConfig{ - NodeSelectionMode: client.NodeSelectionMode_RoundRobin, - } - c, err := client.NewChainClientWithTestNode(t, cfg, time.Second*0, cfg.NodeLeaseDuration, wsURL, nil, sendonlys, 42, chainID) - require.NoError(t, err) - return c -} - -func mustNewClients(t *testing.T, wsURL string, sendonlys ...url.URL) []client.Client { - var clients []client.Client - clients = append(clients, mustNewClient(t, wsURL, sendonlys...)) - clients = append(clients, mustNewChainClient(t, wsURL, sendonlys...)) - return clients -} - -func mustNewClientsWithChainID(t *testing.T, wsURL string, chainID *big.Int, sendonlys ...url.URL) []client.Client { - var clients []client.Client - clients = append(clients, mustNewClientWithChainID(t, wsURL, chainID, sendonlys...)) - clients = append(clients, mustNewChainClientWithChainID(t, wsURL, chainID, sendonlys...)) - return clients -} - -func TestEthClient_TransactionReceipt(t *testing.T) { - t.Parallel() - - txHash := "0xb903239f8543d04b5dc1ba6579132b143087c68db1b2168786408fcbce568238" - - mustReadResult := func(t *testing.T, file string) []byte { - response, err := os.ReadFile(file) - require.NoError(t, err) - var resp struct { - Result json.RawMessage `json:"result"` - } - err = json.Unmarshal(response, &resp) - require.NoError(t, err) - return resp.Result - } - - t.Run("happy path", func(t *testing.T) { - result := mustReadResult(t, "../../../testdata/jsonrpc/getTransactionReceipt.json") - - wsURL := testutils.NewWSServer(t, testutils.FixtureChainID, func(method string, params gjson.Result) (resp testutils.JSONRPCResponse) { - switch method { - case "eth_subscribe": - resp.Result = `"0x00"` - resp.Notify = headResult - return - case "eth_unsubscribe": - resp.Result = "true" - return - } - if assert.Equal(t, "eth_getTransactionReceipt", method) && assert.True(t, params.IsArray()) && - assert.Equal(t, txHash, params.Array()[0].String()) { - resp.Result = string(result) - } - return - }).WSURL().String() - - clients := mustNewClients(t, wsURL) - for _, ethClient := range clients { - err := ethClient.Dial(tests.Context(t)) - require.NoError(t, err) - - hash := common.HexToHash(txHash) - receipt, err := ethClient.TransactionReceipt(tests.Context(t), hash) - require.NoError(t, err) - assert.Equal(t, hash, receipt.TxHash) - assert.Equal(t, big.NewInt(11), receipt.BlockNumber) - } - }) - - t.Run("no tx hash, returns ethereum.NotFound", func(t *testing.T) { - result := mustReadResult(t, "../../../testdata/jsonrpc/getTransactionReceipt_notFound.json") - wsURL := testutils.NewWSServer(t, testutils.FixtureChainID, func(method string, params gjson.Result) (resp testutils.JSONRPCResponse) { - switch method { - case "eth_subscribe": - resp.Result = `"0x00"` - resp.Notify = headResult - return - case "eth_unsubscribe": - resp.Result = "true" - return - } - if assert.Equal(t, "eth_getTransactionReceipt", method) && assert.True(t, params.IsArray()) && - assert.Equal(t, txHash, params.Array()[0].String()) { - resp.Result = string(result) - } - return - }).WSURL().String() - - clients := mustNewClients(t, wsURL) - for _, ethClient := range clients { - err := ethClient.Dial(tests.Context(t)) - require.NoError(t, err) - - hash := common.HexToHash(txHash) - _, err = ethClient.TransactionReceipt(tests.Context(t), hash) - require.Equal(t, ethereum.NotFound, pkgerrors.Cause(err)) - } - }) -} - -func TestEthClient_PendingNonceAt(t *testing.T) { - t.Parallel() - - address := testutils.NewAddress() - - wsURL := testutils.NewWSServer(t, testutils.FixtureChainID, func(method string, params gjson.Result) (resp testutils.JSONRPCResponse) { - switch method { - case "eth_subscribe": - resp.Result = `"0x00"` - resp.Notify = headResult - return - case "eth_unsubscribe": - resp.Result = "true" - return - } - if !assert.Equal(t, "eth_getTransactionCount", method) || !assert.True(t, params.IsArray()) { - return - } - arr := params.Array() - if assert.Equal(t, strings.ToLower(address.Hex()), strings.ToLower(arr[0].String())) && - assert.Equal(t, "pending", arr[1].String()) { - resp.Result = `"0x100"` - } - return - }).WSURL().String() - - clients := mustNewClients(t, wsURL) - for _, ethClient := range clients { - err := ethClient.Dial(tests.Context(t)) - require.NoError(t, err) - - result, err := ethClient.PendingNonceAt(tests.Context(t), address) - require.NoError(t, err) - - var expected uint64 = 256 - require.Equal(t, result, expected) - } -} - -func TestEthClient_BalanceAt(t *testing.T) { - t.Parallel() - - largeBalance, _ := big.NewInt(0).SetString("100000000000000000000", 10) - address := testutils.NewAddress() - - cases := []struct { - name string - balance *big.Int - }{ - {"basic", big.NewInt(256)}, - {"larger than signed 64 bit integer", largeBalance}, - } - - for _, test := range cases { - test := test - t.Run(test.name, func(t *testing.T) { - wsURL := testutils.NewWSServer(t, testutils.FixtureChainID, func(method string, params gjson.Result) (resp testutils.JSONRPCResponse) { - switch method { - case "eth_subscribe": - resp.Result = `"0x00"` - resp.Notify = headResult - return - case "eth_unsubscribe": - resp.Result = "true" - return - } - if assert.Equal(t, "eth_getBalance", method) && assert.True(t, params.IsArray()) && - assert.Equal(t, strings.ToLower(address.Hex()), strings.ToLower(params.Array()[0].String())) { - resp.Result = `"` + hexutil.EncodeBig(test.balance) + `"` - } - return - }).WSURL().String() - - clients := mustNewClients(t, wsURL) - for _, ethClient := range clients { - err := ethClient.Dial(tests.Context(t)) - require.NoError(t, err) - - result, err := ethClient.BalanceAt(tests.Context(t), address, nil) - require.NoError(t, err) - assert.Equal(t, test.balance, result) - } - }) - } -} - -func TestEthClient_LatestBlockHeight(t *testing.T) { - t.Parallel() - - wsURL := testutils.NewWSServer(t, testutils.FixtureChainID, func(method string, params gjson.Result) (resp testutils.JSONRPCResponse) { - switch method { - case "eth_subscribe": - resp.Result = `"0x00"` - resp.Notify = headResult - return - case "eth_unsubscribe": - resp.Result = "true" - return - } - if !assert.Equal(t, "eth_blockNumber", method) { - return - } - resp.Result = `"0x100"` - return - }).WSURL().String() - - clients := mustNewClients(t, wsURL) - for _, ethClient := range clients { - err := ethClient.Dial(tests.Context(t)) - require.NoError(t, err) - - result, err := ethClient.LatestBlockHeight(tests.Context(t)) - require.NoError(t, err) - require.Equal(t, big.NewInt(256), result) - } -} - -func TestEthClient_GetERC20Balance(t *testing.T) { - t.Parallel() - ctx := tests.Context(t) - - expectedBig, _ := big.NewInt(0).SetString("100000000000000000000000000000000000000", 10) - - cases := []struct { - name string - balance *big.Int - }{ - {"small", big.NewInt(256)}, - {"big", expectedBig}, - } - - for _, test := range cases { - test := test - t.Run(test.name, func(t *testing.T) { - contractAddress := testutils.NewAddress() - userAddress := testutils.NewAddress() - functionSelector := evmtypes.HexToFunctionSelector(client.BALANCE_OF_ADDRESS_FUNCTION_SELECTOR) // balanceOf(address) - txData := utils.ConcatBytes(functionSelector.Bytes(), common.LeftPadBytes(userAddress.Bytes(), utils.EVMWordByteLen)) - - wsURL := testutils.NewWSServer(t, testutils.FixtureChainID, func(method string, params gjson.Result) (resp testutils.JSONRPCResponse) { - switch method { - case "eth_subscribe": - resp.Result = `"0x00"` - resp.Notify = headResult - return - case "eth_unsubscribe": - resp.Result = "true" - return - } - if !assert.Equal(t, "eth_call", method) || !assert.True(t, params.IsArray()) { - return - } - arr := params.Array() - callArgs := arr[0] - if assert.True(t, callArgs.IsObject()) && - assert.Equal(t, strings.ToLower(contractAddress.Hex()), callArgs.Get("to").String()) && - assert.Equal(t, hexutil.Encode(txData), callArgs.Get("data").String()) && - assert.Equal(t, "latest", arr[1].String()) { - resp.Result = `"` + hexutil.EncodeBig(test.balance) + `"` - } - return - }).WSURL().String() - - clients := mustNewClients(t, wsURL) - for _, ethClient := range clients { - err := ethClient.Dial(tests.Context(t)) - require.NoError(t, err) - - result, err := ethClient.TokenBalance(ctx, userAddress, contractAddress) - require.NoError(t, err) - assert.Equal(t, test.balance, result) - } - }) - } -} - -func TestReceipt_UnmarshalEmptyBlockHash(t *testing.T) { - t.Parallel() - - input := `{ - "transactionHash": "0x444172bef57ad978655171a8af2cfd89baa02a97fcb773067aef7794d6913374", - "gasUsed": "0x1", - "cumulativeGasUsed": "0x1", - "logs": [], - "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - "blockNumber": "0x8bf99b", - "blockHash": null - }` - - var receipt types.Receipt - err := json.Unmarshal([]byte(input), &receipt) - require.NoError(t, err) -} - -func TestEthClient_HeaderByNumber(t *testing.T) { - t.Parallel() - - expectedBlockNum := big.NewInt(1) - expectedBlockHash := "0x41800b5c3f1717687d85fc9018faac0a6e90b39deaa0b99e7fe4fe796ddeb26a" - - cases := []struct { - name string - expectedRequestBlock *big.Int - expectedResponseBlock int64 - error error - rpcResp string - }{ - {"happy geth", expectedBlockNum, expectedBlockNum.Int64(), nil, - `{"difficulty":"0xf3a00","extraData":"0xd883010503846765746887676f312e372e318664617277696e","gasLimit":"0xffc001","gasUsed":"0x0","hash":"0x41800b5c3f1717687d85fc9018faac0a6e90b39deaa0b99e7fe4fe796ddeb26a","logsBloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","miner":"0xd1aeb42885a43b72b518182ef893125814811048","mixHash":"0x0f98b15f1a4901a7e9204f3c500a7bd527b3fb2c3340e12176a44b83e414a69e","nonce":"0x0ece08ea8c49dfd9","number":"0x1","parentHash":"0x41941023680923e0fe4d74a34bdac8141f2540e3ae90623718e47d66d1ca4a2d","receiptsRoot":"0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421","sha3Uncles":"0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347","size":"0x218","stateRoot":"0xc7b01007a10da045eacb90385887dd0c38fcb5db7393006bdde24b93873c334b","timestamp":"0x58318da2","totalDifficulty":"0x1f3a00","transactions":[],"transactionsRoot":"0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421","uncles":[]}`}, - {"happy parity", expectedBlockNum, expectedBlockNum.Int64(), nil, - `{"author":"0xd1aeb42885a43b72b518182ef893125814811048","difficulty":"0xf3a00","extraData":"0xd883010503846765746887676f312e372e318664617277696e","gasLimit":"0xffc001","gasUsed":"0x0","hash":"0x41800b5c3f1717687d85fc9018faac0a6e90b39deaa0b99e7fe4fe796ddeb26a","logsBloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","miner":"0xd1aeb42885a43b72b518182ef893125814811048","mixHash":"0x0f98b15f1a4901a7e9204f3c500a7bd527b3fb2c3340e12176a44b83e414a69e","nonce":"0x0ece08ea8c49dfd9","number":"0x1","parentHash":"0x41941023680923e0fe4d74a34bdac8141f2540e3ae90623718e47d66d1ca4a2d","receiptsRoot":"0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421","sealFields":["0xa00f98b15f1a4901a7e9204f3c500a7bd527b3fb2c3340e12176a44b83e414a69e","0x880ece08ea8c49dfd9"],"sha3Uncles":"0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347","size":"0x218","stateRoot":"0xc7b01007a10da045eacb90385887dd0c38fcb5db7393006bdde24b93873c334b","timestamp":"0x58318da2","totalDifficulty":"0x1f3a00","transactions":[],"transactionsRoot":"0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421","uncles":[]}`}, - {"missing header", expectedBlockNum, 0, fmt.Errorf("no live nodes available for chain %s", testutils.FixtureChainID.String()), - `null`}, - } - - for _, test := range cases { - test := test - t.Run(test.name, func(t *testing.T) { - wsURL := testutils.NewWSServer(t, testutils.FixtureChainID, func(method string, params gjson.Result) (resp testutils.JSONRPCResponse) { - switch method { - case "eth_subscribe": - resp.Result = `"0x00"` - resp.Notify = headResult - return - case "eth_unsubscribe": - resp.Result = "true" - return - } - if !assert.Equal(t, "eth_getBlockByNumber", method) || !assert.True(t, params.IsArray()) { - return - } - arr := params.Array() - blockNumStr := arr[0].String() - var blockNum hexutil.Big - err := blockNum.UnmarshalText([]byte(blockNumStr)) - if assert.NoError(t, err) && assert.Equal(t, test.expectedRequestBlock, blockNum.ToInt()) && - assert.Equal(t, false, arr[1].Bool()) { - resp.Result = test.rpcResp - } - return - }).WSURL().String() - - clients := mustNewClients(t, wsURL) - for _, ethClient := range clients { - err := ethClient.Dial(tests.Context(t)) - require.NoError(t, err) - - ctx, cancel := context.WithTimeout(tests.Context(t), 5*time.Second) - result, err := ethClient.HeadByNumber(ctx, expectedBlockNum) - if test.error != nil { - require.Error(t, err, test.error) - } else { - require.NoError(t, err) - require.Equal(t, expectedBlockHash, result.Hash.Hex()) - require.Equal(t, test.expectedResponseBlock, result.Number) - require.Zero(t, testutils.FixtureChainID.Cmp(result.EVMChainID.ToInt())) - } - cancel() - } - }) - } -} - -func TestEthClient_SendTransaction_NoSecondaryURL(t *testing.T) { - t.Parallel() - - tx := testutils.NewLegacyTransaction(uint64(42), testutils.NewAddress(), big.NewInt(142), 242, big.NewInt(342), []byte{1, 2, 3}) - - wsURL := testutils.NewWSServer(t, testutils.FixtureChainID, func(method string, params gjson.Result) (resp testutils.JSONRPCResponse) { - switch method { - case "eth_subscribe": - resp.Result = `"0x00"` - resp.Notify = headResult - return - case "eth_unsubscribe": - resp.Result = "true" - return - } - if !assert.Equal(t, "eth_sendRawTransaction", method) { - return - } - resp.Result = `"` + tx.Hash().Hex() + `"` - return - }).WSURL().String() - - clients := mustNewClients(t, wsURL) - for _, ethClient := range clients { - err := ethClient.Dial(tests.Context(t)) - require.NoError(t, err) - - err = ethClient.SendTransaction(tests.Context(t), tx) - assert.NoError(t, err) - } -} - -func TestEthClient_SendTransaction_WithSecondaryURLs(t *testing.T) { - t.Parallel() - - tx := testutils.NewLegacyTransaction(uint64(42), testutils.NewAddress(), big.NewInt(142), 242, big.NewInt(342), []byte{1, 2, 3}) - - wsURL := testutils.NewWSServer(t, testutils.FixtureChainID, func(method string, params gjson.Result) (resp testutils.JSONRPCResponse) { - switch method { - case "eth_subscribe": - resp.Result = `"0x00"` - resp.Notify = headResult - return - case "eth_unsubscribe": - resp.Result = "true" - return - case "eth_sendRawTransaction": - resp.Result = `"` + tx.Hash().Hex() + `"` - } - return - }).WSURL().String() - - rpcSrv := rpc.NewServer() - t.Cleanup(rpcSrv.Stop) - service := sendTxService{chainID: testutils.FixtureChainID} - err := rpcSrv.RegisterName("eth", &service) - require.NoError(t, err) - ts := httptest.NewServer(rpcSrv) - t.Cleanup(ts.Close) - - sendonlyURL, err := url.Parse(ts.URL) - require.NoError(t, err) - - clients := mustNewClients(t, wsURL, *sendonlyURL, *sendonlyURL) - for _, ethClient := range clients { - err = ethClient.Dial(tests.Context(t)) - require.NoError(t, err) - - err = ethClient.SendTransaction(tests.Context(t), tx) - require.NoError(t, err) - } - - // Unfortunately it's a bit tricky to test this, since there is no - // synchronization. We have to rely on timing instead. - require.Eventually(t, func() bool { return service.sentCount.Load() == int32(len(clients)*2) }, tests.WaitTimeout(t), 500*time.Millisecond) -} - -func TestEthClient_SendTransactionReturnCode(t *testing.T) { - t.Parallel() - - fromAddress := testutils.NewAddress() - tx := testutils.NewLegacyTransaction(uint64(42), testutils.NewAddress(), big.NewInt(142), 242, big.NewInt(342), []byte{1, 2, 3}) - - t.Run("returns Fatal error type when error message is fatal", func(t *testing.T) { - wsURL := testutils.NewWSServer(t, testutils.FixtureChainID, func(method string, params gjson.Result) (resp testutils.JSONRPCResponse) { - switch method { - case "eth_subscribe": - resp.Result = `"0x00"` - resp.Notify = headResult - return - case "eth_unsubscribe": - resp.Result = "true" - return - case "eth_sendRawTransaction": - resp.Result = `"` + tx.Hash().Hex() + `"` - resp.Error.Message = "invalid sender" - } - return - }).WSURL().String() - - clients := mustNewClients(t, wsURL) - for _, ethClient := range clients { - err := ethClient.Dial(tests.Context(t)) - require.NoError(t, err) - - errType, err := ethClient.SendTransactionReturnCode(tests.Context(t), tx, fromAddress) - assert.Error(t, err) - assert.Equal(t, errType, commonclient.Fatal) - } - }) - - t.Run("returns TransactionAlreadyKnown error type when error message is nonce too low", func(t *testing.T) { - wsURL := testutils.NewWSServer(t, testutils.FixtureChainID, func(method string, params gjson.Result) (resp testutils.JSONRPCResponse) { - switch method { - case "eth_subscribe": - resp.Result = `"0x00"` - resp.Notify = headResult - return - case "eth_unsubscribe": - resp.Result = "true" - return - case "eth_sendRawTransaction": - resp.Result = `"` + tx.Hash().Hex() + `"` - resp.Error.Message = "nonce too low" - } - return - }).WSURL().String() - - clients := mustNewClients(t, wsURL) - for _, ethClient := range clients { - err := ethClient.Dial(tests.Context(t)) - require.NoError(t, err) - - errType, err := ethClient.SendTransactionReturnCode(tests.Context(t), tx, fromAddress) - assert.Error(t, err) - assert.Equal(t, errType, commonclient.TransactionAlreadyKnown) - } - }) - - t.Run("returns Successful error type when there is no error message", func(t *testing.T) { - wsURL := testutils.NewWSServer(t, testutils.FixtureChainID, func(method string, params gjson.Result) (resp testutils.JSONRPCResponse) { - switch method { - case "eth_subscribe": - resp.Result = `"0x00"` - resp.Notify = headResult - return - case "eth_unsubscribe": - resp.Result = "true" - return - case "eth_sendRawTransaction": - resp.Result = `"` + tx.Hash().Hex() + `"` - } - return - }).WSURL().String() - - clients := mustNewClients(t, wsURL) - for _, ethClient := range clients { - err := ethClient.Dial(tests.Context(t)) - require.NoError(t, err) - - errType, err := ethClient.SendTransactionReturnCode(tests.Context(t), tx, fromAddress) - assert.NoError(t, err) - assert.Equal(t, errType, commonclient.Successful) - } - }) - - t.Run("returns Underpriced error type when transaction is terminally underpriced", func(t *testing.T) { - wsURL := testutils.NewWSServer(t, testutils.FixtureChainID, func(method string, params gjson.Result) (resp testutils.JSONRPCResponse) { - switch method { - case "eth_subscribe": - resp.Result = `"0x00"` - resp.Notify = headResult - return - case "eth_unsubscribe": - resp.Result = "true" - return - case "eth_sendRawTransaction": - resp.Result = `"` + tx.Hash().Hex() + `"` - resp.Error.Message = "transaction underpriced" - } - return - }).WSURL().String() - - clients := mustNewClients(t, wsURL) - for _, ethClient := range clients { - err := ethClient.Dial(tests.Context(t)) - require.NoError(t, err) - - errType, err := ethClient.SendTransactionReturnCode(tests.Context(t), tx, fromAddress) - assert.Error(t, err) - assert.Equal(t, errType, commonclient.Underpriced) - } - }) - - t.Run("returns Unsupported error type when error message is queue full", func(t *testing.T) { - wsURL := testutils.NewWSServer(t, testutils.FixtureChainID, func(method string, params gjson.Result) (resp testutils.JSONRPCResponse) { - switch method { - case "eth_subscribe": - resp.Result = `"0x00"` - resp.Notify = headResult - return - case "eth_unsubscribe": - resp.Result = "true" - return - case "eth_sendRawTransaction": - resp.Result = `"` + tx.Hash().Hex() + `"` - resp.Error.Message = "queue full" - } - return - }).WSURL().String() - - clients := mustNewClients(t, wsURL) - for _, ethClient := range clients { - err := ethClient.Dial(tests.Context(t)) - require.NoError(t, err) - - errType, err := ethClient.SendTransactionReturnCode(tests.Context(t), tx, fromAddress) - assert.Error(t, err) - assert.Equal(t, errType, commonclient.Unsupported) - } - }) - - t.Run("returns Retryable error type when there is a transaction gap", func(t *testing.T) { - wsURL := testutils.NewWSServer(t, testutils.FixtureChainID, func(method string, params gjson.Result) (resp testutils.JSONRPCResponse) { - switch method { - case "eth_subscribe": - resp.Result = `"0x00"` - resp.Notify = headResult - return - case "eth_unsubscribe": - resp.Result = "true" - return - case "eth_sendRawTransaction": - resp.Result = `"` + tx.Hash().Hex() + `"` - resp.Error.Message = "NonceGap" - } - return - }).WSURL().String() - - clients := mustNewClients(t, wsURL) - for _, ethClient := range clients { - err := ethClient.Dial(tests.Context(t)) - require.NoError(t, err) - - errType, err := ethClient.SendTransactionReturnCode(tests.Context(t), tx, fromAddress) - assert.Error(t, err) - assert.Equal(t, errType, commonclient.Retryable) - } - }) - - t.Run("returns InsufficientFunds error type when the sender address doesn't have enough funds", func(t *testing.T) { - wsURL := testutils.NewWSServer(t, testutils.FixtureChainID, func(method string, params gjson.Result) (resp testutils.JSONRPCResponse) { - switch method { - case "eth_subscribe": - resp.Result = `"0x00"` - resp.Notify = headResult - return - case "eth_unsubscribe": - resp.Result = "true" - return - case "eth_sendRawTransaction": - resp.Result = `"` + tx.Hash().Hex() + `"` - resp.Error.Message = "insufficient funds for transfer" - } - return - }).WSURL().String() - - clients := mustNewClients(t, wsURL) - for _, ethClient := range clients { - err := ethClient.Dial(tests.Context(t)) - require.NoError(t, err) - - errType, err := ethClient.SendTransactionReturnCode(tests.Context(t), tx, fromAddress) - assert.Error(t, err) - assert.Equal(t, errType, commonclient.InsufficientFunds) - } - }) - - t.Run("returns ExceedsFeeCap error type when gas price is too high for the node", func(t *testing.T) { - wsURL := testutils.NewWSServer(t, testutils.FixtureChainID, func(method string, params gjson.Result) (resp testutils.JSONRPCResponse) { - switch method { - case "eth_subscribe": - resp.Result = `"0x00"` - resp.Notify = headResult - return - case "eth_unsubscribe": - resp.Result = "true" - return - case "eth_sendRawTransaction": - resp.Result = `"` + tx.Hash().Hex() + `"` - resp.Error.Message = "Transaction fee cap exceeded" - } - return - }).WSURL().String() - - clients := mustNewClients(t, wsURL) - for _, ethClient := range clients { - err := ethClient.Dial(tests.Context(t)) - require.NoError(t, err) - - errType, err := ethClient.SendTransactionReturnCode(tests.Context(t), tx, fromAddress) - assert.Error(t, err) - assert.Equal(t, errType, commonclient.ExceedsMaxFee) - } - }) - - t.Run("returns Unknown error type when the error can't be categorized", func(t *testing.T) { - wsURL := testutils.NewWSServer(t, testutils.FixtureChainID, func(method string, params gjson.Result) (resp testutils.JSONRPCResponse) { - switch method { - case "eth_subscribe": - resp.Result = `"0x00"` - resp.Notify = headResult - return - case "eth_unsubscribe": - resp.Result = "true" - return - case "eth_sendRawTransaction": - resp.Result = `"` + tx.Hash().Hex() + `"` - resp.Error.Message = "some random error" - } - return - }).WSURL().String() - - clients := mustNewClients(t, wsURL) - for _, ethClient := range clients { - err := ethClient.Dial(tests.Context(t)) - require.NoError(t, err) - - errType, err := ethClient.SendTransactionReturnCode(tests.Context(t), tx, fromAddress) - assert.Error(t, err) - assert.Equal(t, errType, commonclient.Unknown) - } - }) -} - -type sendTxService struct { - chainID *big.Int - sentCount atomic.Int32 -} - -func (x *sendTxService) ChainId(ctx context.Context) (*hexutil.Big, error) { - return (*hexutil.Big)(x.chainID), nil -} - -func (x *sendTxService) SendRawTransaction(ctx context.Context, signRawTx hexutil.Bytes) error { - x.sentCount.Add(1) - return nil -} - -func TestEthClient_SubscribeNewHead(t *testing.T) { - t.Parallel() - - ctx, cancel := context.WithTimeout(tests.Context(t), tests.WaitTimeout(t)) - defer cancel() - - chainId := big.NewInt(123456) - wsURL := testutils.NewWSServer(t, chainId, func(method string, params gjson.Result) (resp testutils.JSONRPCResponse) { - if method == "eth_unsubscribe" { - resp.Result = "true" - return - } - assert.Equal(t, "eth_subscribe", method) - if assert.True(t, params.IsArray()) && assert.Equal(t, "newHeads", params.Array()[0].String()) { - resp.Result = `"0x00"` - resp.Notify = headResult - } - return - }).WSURL().String() - - clients := mustNewClientsWithChainID(t, wsURL, chainId) - for _, ethClient := range clients { - err := ethClient.Dial(tests.Context(t)) - require.NoError(t, err) - - headCh := make(chan *evmtypes.Head) - sub, err := ethClient.SubscribeNewHead(ctx, headCh) - require.NoError(t, err) - - select { - case err := <-sub.Err(): - t.Fatal(err) - case <-ctx.Done(): - t.Fatal(ctx.Err()) - case h := <-headCh: - require.NotNil(t, h.EVMChainID) - require.Zero(t, chainId.Cmp(h.EVMChainID.ToInt())) - } - sub.Unsubscribe() - } -} - -func TestEthClient_ErroringClient(t *testing.T) { - t.Parallel() - ctx := tests.Context(t) - - // Empty node means there are no active nodes to select from, causing client to always return error. - erroringClient := client.NewChainClientWithEmptyNode(t, commonclient.NodeSelectionModeRoundRobin, time.Second*0, time.Second*0, testutils.FixtureChainID) - - _, err := erroringClient.BalanceAt(ctx, common.Address{}, nil) - require.Equal(t, err, commonclient.ErroringNodeError) - - err = erroringClient.BatchCallContext(ctx, nil) - require.Equal(t, err, commonclient.ErroringNodeError) - - err = erroringClient.BatchCallContextAll(ctx, nil) - require.Equal(t, err, commonclient.ErroringNodeError) - - _, err = erroringClient.BlockByHash(ctx, common.Hash{}) - require.Equal(t, err, commonclient.ErroringNodeError) - - _, err = erroringClient.BlockByNumber(ctx, nil) - require.Equal(t, err, commonclient.ErroringNodeError) - - err = erroringClient.CallContext(ctx, nil, "") - require.Equal(t, err, commonclient.ErroringNodeError) - - _, err = erroringClient.CallContract(ctx, ethereum.CallMsg{}, nil) - require.Equal(t, err, commonclient.ErroringNodeError) - - // TODO-1663: test actual ChainID() call once client.go is deprecated. - id, err := erroringClient.ChainID() - require.Equal(t, id, testutils.FixtureChainID) - //require.Equal(t, err, commonclient.ErroringNodeError) - require.Equal(t, err, nil) - - _, err = erroringClient.CodeAt(ctx, common.Address{}, nil) - require.Equal(t, err, commonclient.ErroringNodeError) - - id = erroringClient.ConfiguredChainID() - require.Equal(t, id, testutils.FixtureChainID) - - err = erroringClient.Dial(ctx) - require.ErrorContains(t, err, "no available nodes for chain") - - _, err = erroringClient.EstimateGas(ctx, ethereum.CallMsg{}) - require.Equal(t, err, commonclient.ErroringNodeError) - - _, err = erroringClient.FilterLogs(ctx, ethereum.FilterQuery{}) - require.Equal(t, err, commonclient.ErroringNodeError) - - _, err = erroringClient.HeaderByHash(ctx, common.Hash{}) - require.Equal(t, err, commonclient.ErroringNodeError) - - _, err = erroringClient.HeaderByNumber(ctx, nil) - require.Equal(t, err, commonclient.ErroringNodeError) - - _, err = erroringClient.HeadByHash(ctx, common.Hash{}) - require.Equal(t, err, commonclient.ErroringNodeError) - - _, err = erroringClient.HeadByNumber(ctx, nil) - require.Equal(t, err, commonclient.ErroringNodeError) - - _, err = erroringClient.LINKBalance(ctx, common.Address{}, common.Address{}) - require.Equal(t, err, commonclient.ErroringNodeError) - - _, err = erroringClient.LatestBlockHeight(ctx) - require.Equal(t, err, commonclient.ErroringNodeError) - - _, err = erroringClient.PendingCodeAt(ctx, common.Address{}) - require.Equal(t, err, commonclient.ErroringNodeError) - - _, err = erroringClient.PendingNonceAt(ctx, common.Address{}) - require.Equal(t, err, commonclient.ErroringNodeError) - - err = erroringClient.SendTransaction(ctx, nil) - require.Equal(t, err, commonclient.ErroringNodeError) - - code, err := erroringClient.SendTransactionReturnCode(ctx, nil, common.Address{}) - require.Equal(t, code, commonclient.Unknown) - require.Equal(t, err, commonclient.ErroringNodeError) - - _, err = erroringClient.SequenceAt(ctx, common.Address{}, nil) - require.Equal(t, err, commonclient.ErroringNodeError) - - _, err = erroringClient.SubscribeFilterLogs(ctx, ethereum.FilterQuery{}, nil) - require.Equal(t, err, commonclient.ErroringNodeError) - - _, err = erroringClient.SubscribeNewHead(ctx, nil) - require.Equal(t, err, commonclient.ErroringNodeError) - - _, err = erroringClient.SuggestGasPrice(ctx) - require.Equal(t, err, commonclient.ErroringNodeError) - - _, err = erroringClient.SuggestGasTipCap(ctx) - require.Equal(t, err, commonclient.ErroringNodeError) - - _, err = erroringClient.TokenBalance(ctx, common.Address{}, common.Address{}) - require.Equal(t, err, commonclient.ErroringNodeError) - - _, err = erroringClient.TransactionByHash(ctx, common.Hash{}) - require.Equal(t, err, commonclient.ErroringNodeError) - - _, err = erroringClient.TransactionReceipt(ctx, common.Hash{}) - require.Equal(t, err, commonclient.ErroringNodeError) -} - -const headResult = client.HeadResult diff --git a/core/chains/evm/client/erroring_node.go b/core/chains/evm/client/erroring_node.go deleted file mode 100644 index 00e8465bca3..00000000000 --- a/core/chains/evm/client/erroring_node.go +++ /dev/null @@ -1,150 +0,0 @@ -package client - -import ( - "context" - "math/big" - - evmtypes "github.com/smartcontractkit/chainlink/v2/core/chains/evm/types" - - "github.com/ethereum/go-ethereum" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/rpc" - pkgerrors "github.com/pkg/errors" -) - -var _ Node = (*erroringNode)(nil) - -type erroringNode struct { - errMsg string -} - -func (e *erroringNode) UnsubscribeAllExceptAliveLoop() {} - -func (e *erroringNode) SubscribersCount() int32 { - return 0 -} - -func (e *erroringNode) ChainID() (chainID *big.Int) { return nil } - -func (e *erroringNode) Start(ctx context.Context) error { return pkgerrors.New(e.errMsg) } - -func (e *erroringNode) Close() error { return nil } - -func (e *erroringNode) Verify(ctx context.Context, expectedChainID *big.Int) (err error) { - return pkgerrors.New(e.errMsg) -} - -func (e *erroringNode) CallContext(ctx context.Context, result interface{}, method string, args ...interface{}) error { - return pkgerrors.New(e.errMsg) -} - -func (e *erroringNode) BatchCallContext(ctx context.Context, b []rpc.BatchElem) error { - return pkgerrors.New(e.errMsg) -} - -func (e *erroringNode) SendTransaction(ctx context.Context, tx *types.Transaction) error { - return pkgerrors.New(e.errMsg) -} - -func (e *erroringNode) PendingCodeAt(ctx context.Context, account common.Address) ([]byte, error) { - return nil, pkgerrors.New(e.errMsg) -} - -func (e *erroringNode) PendingNonceAt(ctx context.Context, account common.Address) (uint64, error) { - return 0, pkgerrors.New(e.errMsg) -} - -func (e *erroringNode) NonceAt(ctx context.Context, account common.Address, blockNumber *big.Int) (uint64, error) { - return 0, pkgerrors.New(e.errMsg) -} - -func (e *erroringNode) TransactionReceipt(ctx context.Context, txHash common.Hash) (*types.Receipt, error) { - return nil, pkgerrors.New(e.errMsg) -} - -func (e *erroringNode) TransactionByHash(ctx context.Context, txHash common.Hash) (*types.Transaction, error) { - return nil, pkgerrors.New(e.errMsg) -} - -func (e *erroringNode) BlockByNumber(ctx context.Context, number *big.Int) (*types.Block, error) { - return nil, pkgerrors.New(e.errMsg) -} - -func (e *erroringNode) BlockByHash(ctx context.Context, hash common.Hash) (*types.Block, error) { - return nil, pkgerrors.New(e.errMsg) -} - -func (e *erroringNode) BlockNumber(ctx context.Context) (uint64, error) { - return 0, pkgerrors.New(e.errMsg) -} - -func (e *erroringNode) BalanceAt(ctx context.Context, account common.Address, blockNumber *big.Int) (*big.Int, error) { - return nil, pkgerrors.New(e.errMsg) -} - -func (e *erroringNode) FilterLogs(ctx context.Context, q ethereum.FilterQuery) ([]types.Log, error) { - return nil, pkgerrors.New(e.errMsg) -} - -func (e *erroringNode) SubscribeFilterLogs(ctx context.Context, q ethereum.FilterQuery, ch chan<- types.Log) (ethereum.Subscription, error) { - return nil, pkgerrors.New(e.errMsg) -} - -func (e *erroringNode) EstimateGas(ctx context.Context, call ethereum.CallMsg) (uint64, error) { - return 0, pkgerrors.New(e.errMsg) -} - -func (e *erroringNode) SuggestGasPrice(ctx context.Context) (*big.Int, error) { - return nil, pkgerrors.New(e.errMsg) -} - -func (e *erroringNode) CallContract(ctx context.Context, msg ethereum.CallMsg, blockNumber *big.Int) ([]byte, error) { - return nil, pkgerrors.New(e.errMsg) -} - -func (e *erroringNode) PendingCallContract(ctx context.Context, msg ethereum.CallMsg) ([]byte, error) { - return nil, pkgerrors.New(e.errMsg) -} - -func (e *erroringNode) CodeAt(ctx context.Context, account common.Address, blockNumber *big.Int) ([]byte, error) { - return nil, pkgerrors.New(e.errMsg) -} - -func (e *erroringNode) HeaderByNumber(_ context.Context, _ *big.Int) (*types.Header, error) { - return nil, pkgerrors.New(e.errMsg) -} - -func (e *erroringNode) HeaderByHash(_ context.Context, _ common.Hash) (*types.Header, error) { - return nil, pkgerrors.New(e.errMsg) -} - -func (e *erroringNode) SuggestGasTipCap(ctx context.Context) (*big.Int, error) { - return nil, pkgerrors.New(e.errMsg) -} - -func (e *erroringNode) EthSubscribe(ctx context.Context, channel chan<- *evmtypes.Head, args ...interface{}) (ethereum.Subscription, error) { - return nil, pkgerrors.New(e.errMsg) -} - -func (e *erroringNode) String() string { - return "" -} - -func (e *erroringNode) State() NodeState { - return NodeStateUnreachable -} - -func (e *erroringNode) StateAndLatest() (NodeState, int64, *big.Int) { - return NodeStateUnreachable, -1, nil -} - -func (e *erroringNode) Order() int32 { - return 100 -} - -func (e *erroringNode) DeclareOutOfSync() {} -func (e *erroringNode) DeclareInSync() {} -func (e *erroringNode) DeclareUnreachable() {} -func (e *erroringNode) Name() string { return "" } -func (e *erroringNode) NodeStates() map[int32]string { return nil } diff --git a/core/chains/evm/client/erroring_node_test.go b/core/chains/evm/client/erroring_node_test.go deleted file mode 100644 index 70a555485f0..00000000000 --- a/core/chains/evm/client/erroring_node_test.go +++ /dev/null @@ -1,101 +0,0 @@ -package client - -import ( - "testing" - - "github.com/ethereum/go-ethereum" - "github.com/ethereum/go-ethereum/common" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" -) - -func TestErroringNode(t *testing.T) { - t.Parallel() - - ctx := testutils.Context(t) - n := &erroringNode{ - "boo", - } - - require.Nil(t, n.ChainID()) - err := n.Start(ctx) - require.Equal(t, n.errMsg, err.Error()) - - defer func() { assert.NoError(t, n.Close()) }() - - err = n.Verify(ctx, nil) - require.Equal(t, n.errMsg, err.Error()) - - err = n.CallContext(ctx, nil, "") - require.Equal(t, n.errMsg, err.Error()) - - err = n.BatchCallContext(ctx, nil) - require.Equal(t, n.errMsg, err.Error()) - - err = n.SendTransaction(ctx, nil) - require.Equal(t, n.errMsg, err.Error()) - - _, err = n.PendingCodeAt(ctx, common.Address{}) - require.Equal(t, n.errMsg, err.Error()) - - _, err = n.PendingNonceAt(ctx, common.Address{}) - require.Equal(t, n.errMsg, err.Error()) - - _, err = n.NonceAt(ctx, common.Address{}, nil) - require.Equal(t, n.errMsg, err.Error()) - - _, err = n.TransactionReceipt(ctx, common.Hash{}) - require.Equal(t, n.errMsg, err.Error()) - - _, err = n.BlockByNumber(ctx, nil) - require.Equal(t, n.errMsg, err.Error()) - - _, err = n.BlockByHash(ctx, common.Hash{}) - require.Equal(t, n.errMsg, err.Error()) - - _, err = n.BalanceAt(ctx, common.Address{}, nil) - require.Equal(t, n.errMsg, err.Error()) - - _, err = n.FilterLogs(ctx, ethereum.FilterQuery{}) - require.Equal(t, n.errMsg, err.Error()) - - _, err = n.SubscribeFilterLogs(ctx, ethereum.FilterQuery{}, nil) - require.Equal(t, n.errMsg, err.Error()) - - _, err = n.EstimateGas(ctx, ethereum.CallMsg{}) - require.Equal(t, n.errMsg, err.Error()) - - _, err = n.SuggestGasPrice(ctx) - require.Equal(t, n.errMsg, err.Error()) - - _, err = n.CallContract(ctx, ethereum.CallMsg{}, nil) - require.Equal(t, n.errMsg, err.Error()) - - _, err = n.CodeAt(ctx, common.Address{}, nil) - require.Equal(t, n.errMsg, err.Error()) - - _, err = n.HeaderByNumber(ctx, nil) - require.Equal(t, n.errMsg, err.Error()) - - _, err = n.SuggestGasTipCap(ctx) - require.Equal(t, n.errMsg, err.Error()) - - _, err = n.EthSubscribe(ctx, nil) - require.Equal(t, n.errMsg, err.Error()) - - require.Equal(t, "", n.String()) - require.Equal(t, NodeStateUnreachable, n.State()) - - state, num, _ := n.StateAndLatest() - require.Equal(t, NodeStateUnreachable, state) - require.Equal(t, int64(-1), num) - - n.DeclareInSync() - n.DeclareOutOfSync() - n.DeclareUnreachable() - - require.Zero(t, n.Name()) - require.Nil(t, n.NodeStates()) -} diff --git a/core/chains/evm/client/helpers_test.go b/core/chains/evm/client/helpers_test.go index 7e2771a67d5..0fd33041896 100644 --- a/core/chains/evm/client/helpers_test.go +++ b/core/chains/evm/client/helpers_test.go @@ -109,40 +109,6 @@ func (tc TestNodePoolConfig) Errors() config.ClientErrors { return tc.NodeErrors } -func NewClientWithTestNode(t *testing.T, nodePoolCfg config.NodePool, noNewHeadsThreshold time.Duration, rpcUrl string, rpcHTTPURL *url.URL, sendonlyRPCURLs []url.URL, id int32, chainID *big.Int) (*client, error) { - parsed, err := url.ParseRequestURI(rpcUrl) - if err != nil { - return nil, err - } - - if parsed.Scheme != "ws" && parsed.Scheme != "wss" { - return nil, pkgerrors.Errorf("ethereum url scheme must be websocket: %s", parsed.String()) - } - - lggr := logger.Sugared(logger.Test(t)) - n := NewNode(nodePoolCfg, noNewHeadsThreshold, lggr, *parsed, rpcHTTPURL, "eth-primary-0", id, chainID, 1) - n.(*node).setLatestReceived(0, big.NewInt(0)) - primaries := []Node{n} - - var sendonlys []SendOnlyNode - for i, url := range sendonlyRPCURLs { - if url.Scheme != "http" && url.Scheme != "https" { - return nil, pkgerrors.Errorf("sendonly ethereum rpc url scheme must be http(s): %s", url.String()) - } - s := NewSendOnlyNode(lggr, url, fmt.Sprintf("eth-sendonly-%d", i), chainID) - sendonlys = append(sendonlys, s) - } - - pool := NewPool(lggr, nodePoolCfg.SelectionMode(), nodePoolCfg.LeaseDuration(), noNewHeadsThreshold, primaries, sendonlys, chainID, "") - c := &client{logger: lggr, pool: pool} - t.Cleanup(c.Close) - return c, nil -} - -func Wrap(err error, s string) error { - return wrap(err, s) -} - func NewChainClientWithTestNode( t *testing.T, nodeCfg commonclient.NodeConfig, @@ -217,7 +183,7 @@ func NewChainClientWithMockedRpc( var chainType commonconfig.ChainType cfg := TestNodePoolConfig{ - NodeSelectionMode: NodeSelectionMode_RoundRobin, + NodeSelectionMode: commonclient.NodeSelectionModeRoundRobin, } parsed, _ := url.ParseRequestURI("ws://test") @@ -230,17 +196,8 @@ func NewChainClientWithMockedRpc( return c } -type TestableSendOnlyNode interface { - SendOnlyNode - SetEthClient(newBatchSender BatchSender, newSender TxSender) -} - const HeadResult = `{"difficulty":"0xf3a00","extraData":"0xd883010503846765746887676f312e372e318664617277696e","gasLimit":"0xffc001","gasUsed":"0x0","hash":"0x41800b5c3f1717687d85fc9018faac0a6e90b39deaa0b99e7fe4fe796ddeb26a","logsBloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","miner":"0xd1aeb42885a43b72b518182ef893125814811048","mixHash":"0x0f98b15f1a4901a7e9204f3c500a7bd527b3fb2c3340e12176a44b83e414a69e","nonce":"0x0ece08ea8c49dfd9","number":"0x1","parentHash":"0x41941023680923e0fe4d74a34bdac8141f2540e3ae90623718e47d66d1ca4a2d","receiptsRoot":"0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421","sha3Uncles":"0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347","size":"0x218","stateRoot":"0xc7b01007a10da045eacb90385887dd0c38fcb5db7393006bdde24b93873c334b","timestamp":"0x58318da2","totalDifficulty":"0x1f3a00","transactions":[],"transactionsRoot":"0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421","uncles":[]}` -func IsDialed(s SendOnlyNode) bool { - return s.(*sendOnlyNode).dialed -} - type mockSubscription struct { unsubscribed bool Errors chan error diff --git a/core/chains/evm/client/node.go b/core/chains/evm/client/node.go deleted file mode 100644 index 968cb34b9fe..00000000000 --- a/core/chains/evm/client/node.go +++ /dev/null @@ -1,1160 +0,0 @@ -package client - -import ( - "context" - "fmt" - "math/big" - "net/url" - "strconv" - "sync" - "time" - - "github.com/ethereum/go-ethereum" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/ethclient" - "github.com/ethereum/go-ethereum/rpc" - "github.com/google/uuid" - pkgerrors "github.com/pkg/errors" - "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/client_golang/prometheus/promauto" - - "github.com/smartcontractkit/chainlink-common/pkg/logger" - "github.com/smartcontractkit/chainlink-common/pkg/services" - - "github.com/smartcontractkit/chainlink/v2/core/chains/evm/config" - evmtypes "github.com/smartcontractkit/chainlink/v2/core/chains/evm/types" -) - -var ( - promEVMPoolRPCNodeDials = promauto.NewCounterVec(prometheus.CounterOpts{ - Name: "evm_pool_rpc_node_dials_total", - Help: "The total number of dials for the given RPC node", - }, []string{"evmChainID", "nodeName"}) - promEVMPoolRPCNodeDialsFailed = promauto.NewCounterVec(prometheus.CounterOpts{ - Name: "evm_pool_rpc_node_dials_failed", - Help: "The total number of failed dials for the given RPC node", - }, []string{"evmChainID", "nodeName"}) - promEVMPoolRPCNodeDialsSuccess = promauto.NewCounterVec(prometheus.CounterOpts{ - Name: "evm_pool_rpc_node_dials_success", - Help: "The total number of successful dials for the given RPC node", - }, []string{"evmChainID", "nodeName"}) - promEVMPoolRPCNodeVerifies = promauto.NewCounterVec(prometheus.CounterOpts{ - Name: "evm_pool_rpc_node_verifies", - Help: "The total number of chain ID verifications for the given RPC node", - }, []string{"evmChainID", "nodeName"}) - promEVMPoolRPCNodeVerifiesFailed = promauto.NewCounterVec(prometheus.CounterOpts{ - Name: "evm_pool_rpc_node_verifies_failed", - Help: "The total number of failed chain ID verifications for the given RPC node", - }, []string{"evmChainID", "nodeName"}) - promEVMPoolRPCNodeVerifiesSuccess = promauto.NewCounterVec(prometheus.CounterOpts{ - Name: "evm_pool_rpc_node_verifies_success", - Help: "The total number of successful chain ID verifications for the given RPC node", - }, []string{"evmChainID", "nodeName"}) - - promEVMPoolRPCNodeCalls = promauto.NewCounterVec(prometheus.CounterOpts{ - Name: "evm_pool_rpc_node_calls_total", - Help: "The approximate total number of RPC calls for the given RPC node", - }, []string{"evmChainID", "nodeName"}) - promEVMPoolRPCNodeCallsFailed = promauto.NewCounterVec(prometheus.CounterOpts{ - Name: "evm_pool_rpc_node_calls_failed", - Help: "The approximate total number of failed RPC calls for the given RPC node", - }, []string{"evmChainID", "nodeName"}) - promEVMPoolRPCNodeCallsSuccess = promauto.NewCounterVec(prometheus.CounterOpts{ - Name: "evm_pool_rpc_node_calls_success", - Help: "The approximate total number of successful RPC calls for the given RPC node", - }, []string{"evmChainID", "nodeName"}) - promEVMPoolRPCCallTiming = promauto.NewHistogramVec(prometheus.HistogramOpts{ - Name: "evm_pool_rpc_node_rpc_call_time", - Help: "The duration of an RPC call in nanoseconds", - Buckets: []float64{ - float64(50 * time.Millisecond), - float64(100 * time.Millisecond), - float64(200 * time.Millisecond), - float64(500 * time.Millisecond), - float64(1 * time.Second), - float64(2 * time.Second), - float64(4 * time.Second), - float64(8 * time.Second), - }, - }, []string{"evmChainID", "nodeName", "rpcHost", "isSendOnly", "success", "rpcCallName"}) -) - -//go:generate mockery --quiet --name Node --output ../mocks/ --case=underscore - -// Node represents a client that connects to an ethereum-compatible RPC node -// -// Deprecated: use [pkg/github.com/smartcontractkit/chainlink/v2/common/client.Node] -type Node interface { - Start(ctx context.Context) error - Close() error - - // State returns NodeState - State() NodeState - // StateAndLatest returns NodeState with the latest received block number & total difficulty. - StateAndLatest() (state NodeState, blockNum int64, totalDifficulty *big.Int) - // Name is a unique identifier for this node. - Name() string - ChainID() *big.Int - Order() int32 - SubscribersCount() int32 - UnsubscribeAllExceptAliveLoop() - - CallContext(ctx context.Context, result interface{}, method string, args ...interface{}) error - BatchCallContext(ctx context.Context, b []rpc.BatchElem) error - SendTransaction(ctx context.Context, tx *types.Transaction) error - PendingCodeAt(ctx context.Context, account common.Address) ([]byte, error) - PendingNonceAt(ctx context.Context, account common.Address) (uint64, error) - NonceAt(ctx context.Context, account common.Address, blockNumber *big.Int) (uint64, error) - TransactionReceipt(ctx context.Context, txHash common.Hash) (*types.Receipt, error) - TransactionByHash(ctx context.Context, txHash common.Hash) (*types.Transaction, error) - BlockByNumber(ctx context.Context, number *big.Int) (*types.Block, error) - BlockByHash(ctx context.Context, hash common.Hash) (*types.Block, error) - BlockNumber(ctx context.Context) (uint64, error) - BalanceAt(ctx context.Context, account common.Address, blockNumber *big.Int) (*big.Int, error) - FilterLogs(ctx context.Context, q ethereum.FilterQuery) ([]types.Log, error) - SubscribeFilterLogs(ctx context.Context, q ethereum.FilterQuery, ch chan<- types.Log) (ethereum.Subscription, error) - EstimateGas(ctx context.Context, call ethereum.CallMsg) (uint64, error) - SuggestGasPrice(ctx context.Context) (*big.Int, error) - CallContract(ctx context.Context, msg ethereum.CallMsg, blockNumber *big.Int) ([]byte, error) - PendingCallContract(ctx context.Context, msg ethereum.CallMsg) ([]byte, error) - CodeAt(ctx context.Context, account common.Address, blockNumber *big.Int) ([]byte, error) - HeaderByNumber(context.Context, *big.Int) (*types.Header, error) - HeaderByHash(context.Context, common.Hash) (*types.Header, error) - SuggestGasTipCap(ctx context.Context) (*big.Int, error) - EthSubscribe(ctx context.Context, channel chan<- *evmtypes.Head, args ...interface{}) (ethereum.Subscription, error) - - String() string -} - -type rawclient struct { - rpc *rpc.Client - geth *ethclient.Client - uri url.URL -} - -// Node represents one ethereum node. -// It must have a ws url and may have a http url -type node struct { - services.StateMachine - lfcLog logger.Logger - rpcLog logger.SugaredLogger - name string - id int32 - chainID *big.Int - nodePoolCfg config.NodePool - noNewHeadsThreshold time.Duration - order int32 - - ws rawclient - http *rawclient - - stateMu sync.RWMutex // protects state* fields - state NodeState - // Each node is tracking the last received head number and total difficulty - stateLatestBlockNumber int64 - stateLatestTotalDifficulty *big.Int - - // Need to track subscriptions because closing the RPC does not (always?) - // close the underlying subscription - subs []ethereum.Subscription - - // Need to track the aliveLoop subscription, so we do not cancel it when checking lease - aliveLoopSub ethereum.Subscription - - // chStopInFlight can be closed to immediately cancel all in-flight requests on - // this node. Closing and replacing should be serialized through - // stateMu since it can happen on state transitions as well as node Close. - chStopInFlight chan struct{} - - stopCh services.StopChan - // wg waits for subsidiary goroutines - wg sync.WaitGroup - - // nLiveNodes is a passed in function that allows this node to: - // 1. see how many live nodes there are in total, so we can prevent the last alive node in a pool from being - // moved to out-of-sync state. It is better to have one out-of-sync node than no nodes at all. - // 2. compare against the highest head (by number or difficulty) to ensure we don't fall behind too far. - nLiveNodes func() (count int, blockNumber int64, totalDifficulty *big.Int) -} - -// NewNode returns a new *node as Node -// -// Deprecated: use [pkg/github.com/smartcontractkit/chainlink/v2/common/client.NewNode] -func NewNode(nodeCfg config.NodePool, noNewHeadsThreshold time.Duration, lggr logger.Logger, wsuri url.URL, httpuri *url.URL, name string, id int32, chainID *big.Int, nodeOrder int32) Node { - n := new(node) - n.name = name - n.id = id - n.chainID = chainID - n.nodePoolCfg = nodeCfg - n.noNewHeadsThreshold = noNewHeadsThreshold - n.ws.uri = wsuri - n.order = nodeOrder - if httpuri != nil { - n.http = &rawclient{uri: *httpuri} - } - n.chStopInFlight = make(chan struct{}) - n.stopCh = make(chan struct{}) - lggr = logger.Named(lggr, "Node") - lggr = logger.With(lggr, - "nodeTier", "primary", - "nodeName", name, - "node", n.String(), - "evmChainID", chainID, - "nodeOrder", n.order, - "mode", n.getNodeMode(), - ) - n.lfcLog = logger.Named(lggr, "Lifecycle") - n.rpcLog = logger.Sugared(lggr).Named("RPC") - n.stateLatestBlockNumber = -1 - - return n -} - -// Start dials and verifies the node -// Should only be called once in a node's lifecycle -// Return value is necessary to conform to interface but this will never -// actually return an error. -func (n *node) Start(startCtx context.Context) error { - return n.StartOnce(n.name, func() error { - n.start(startCtx) - return nil - }) -} - -// start initially dials the node and verifies chain ID -// This spins off lifecycle goroutines. -// Not thread-safe. -// Node lifecycle is synchronous: only one goroutine should be running at a -// time. -func (n *node) start(startCtx context.Context) { - if n.state != NodeStateUndialed { - panic(fmt.Sprintf("cannot dial node with state %v", n.state)) - } - - dialCtx, dialCancel := n.makeQueryCtx(startCtx) - defer dialCancel() - if err := n.dial(dialCtx); err != nil { - n.lfcLog.Errorw("Dial failed: EVM Node is unreachable", "err", err) - n.declareUnreachable() - return - } - n.setState(NodeStateDialed) - - verifyCtx, verifyCancel := n.makeQueryCtx(startCtx) - defer verifyCancel() - if err := n.verify(verifyCtx); pkgerrors.Is(err, errInvalidChainID) { - n.lfcLog.Errorw("Verify failed: EVM Node has the wrong chain ID", "err", err) - n.declareInvalidChainID() - return - } else if err != nil { - n.lfcLog.Errorw(fmt.Sprintf("Verify failed: %v", err), "err", err) - n.declareUnreachable() - return - } - - n.declareAlive() -} - -// Not thread-safe -// Pure dial: does not mutate node "state" field. -func (n *node) dial(callerCtx context.Context) error { - ctx, cancel := n.makeQueryCtx(callerCtx) - defer cancel() - - promEVMPoolRPCNodeDials.WithLabelValues(n.chainID.String(), n.name).Inc() - lggr := logger.With(n.lfcLog, "wsuri", n.ws.uri.Redacted()) - if n.http != nil { - lggr = logger.With(lggr, "httpuri", n.http.uri.Redacted()) - } - lggr.Debugw("RPC dial: evmclient.Client#dial") - - wsrpc, err := rpc.DialWebsocket(ctx, n.ws.uri.String(), "") - if err != nil { - promEVMPoolRPCNodeDialsFailed.WithLabelValues(n.chainID.String(), n.name).Inc() - return pkgerrors.Wrapf(err, "error while dialing websocket: %v", n.ws.uri.Redacted()) - } - - var httprpc *rpc.Client - if n.http != nil { - httprpc, err = rpc.DialHTTP(n.http.uri.String()) - if err != nil { - promEVMPoolRPCNodeDialsFailed.WithLabelValues(n.chainID.String(), n.name).Inc() - return pkgerrors.Wrapf(err, "error while dialing HTTP: %v", n.http.uri.Redacted()) - } - } - - n.ws.rpc = wsrpc - n.ws.geth = ethclient.NewClient(wsrpc) - - if n.http != nil { - n.http.rpc = httprpc - n.http.geth = ethclient.NewClient(httprpc) - } - - promEVMPoolRPCNodeDialsSuccess.WithLabelValues(n.chainID.String(), n.name).Inc() - - return nil -} - -var errInvalidChainID = pkgerrors.New("invalid chain id") - -// verify checks that all connections to eth nodes match the given chain ID -// Not thread-safe -// Pure verify: does not mutate node "state" field. -func (n *node) verify(callerCtx context.Context) (err error) { - ctx, cancel := n.makeQueryCtx(callerCtx) - defer cancel() - - promEVMPoolRPCNodeVerifies.WithLabelValues(n.chainID.String(), n.name).Inc() - promFailed := func() { - promEVMPoolRPCNodeVerifiesFailed.WithLabelValues(n.chainID.String(), n.name).Inc() - } - - st := n.State() - switch st { - case NodeStateDialed, NodeStateOutOfSync, NodeStateInvalidChainID: - default: - panic(fmt.Sprintf("cannot verify node in state %v", st)) - } - - var chainID *big.Int - if chainID, err = n.ws.geth.ChainID(ctx); err != nil { - promFailed() - return pkgerrors.Wrapf(err, "failed to verify chain ID for node %s", n.name) - } else if chainID.Cmp(n.chainID) != 0 { - promFailed() - return pkgerrors.Wrapf( - errInvalidChainID, - "websocket rpc ChainID doesn't match local chain ID: RPC ID=%s, local ID=%s, node name=%s", - chainID.String(), - n.chainID.String(), - n.name, - ) - } - if n.http != nil { - if chainID, err = n.http.geth.ChainID(ctx); err != nil { - promFailed() - return pkgerrors.Wrapf(err, "failed to verify chain ID for node %s", n.name) - } else if chainID.Cmp(n.chainID) != 0 { - promFailed() - return pkgerrors.Wrapf( - errInvalidChainID, - "http rpc ChainID doesn't match local chain ID: RPC ID=%s, local ID=%s, node name=%s", - chainID.String(), - n.chainID.String(), - n.name, - ) - } - } - - promEVMPoolRPCNodeVerifiesSuccess.WithLabelValues(n.chainID.String(), n.name).Inc() - - return nil -} - -func (n *node) Close() error { - return n.StopOnce(n.name, func() error { - defer func() { - n.wg.Wait() - if n.ws.rpc != nil { - n.ws.rpc.Close() - } - }() - - n.stateMu.Lock() - defer n.stateMu.Unlock() - - close(n.stopCh) - n.cancelInflightRequests() - n.state = NodeStateClosed - return nil - }) -} - -// registerSub adds the sub to the node list -func (n *node) registerSub(sub ethereum.Subscription) { - n.stateMu.Lock() - defer n.stateMu.Unlock() - n.subs = append(n.subs, sub) -} - -// disconnectAll disconnects all clients connected to the node -// WARNING: NOT THREAD-SAFE -// This must be called from within the n.stateMu lock -func (n *node) disconnectAll() { - if n.ws.rpc != nil { - n.ws.rpc.Close() - } - n.cancelInflightRequests() - n.unsubscribeAll() -} - -// SubscribersCount returns the number of client subscribed to the node -func (n *node) SubscribersCount() int32 { - n.stateMu.RLock() - defer n.stateMu.RUnlock() - return int32(len(n.subs)) -} - -// UnsubscribeAllExceptAliveLoop disconnects all subscriptions to the node except the alive loop subscription -// while holding the n.stateMu lock -func (n *node) UnsubscribeAllExceptAliveLoop() { - n.stateMu.Lock() - defer n.stateMu.Unlock() - - for _, s := range n.subs { - if s != n.aliveLoopSub { - s.Unsubscribe() - } - } -} - -// cancelInflightRequests closes and replaces the chStopInFlight -// WARNING: NOT THREAD-SAFE -// This must be called from within the n.stateMu lock -func (n *node) cancelInflightRequests() { - close(n.chStopInFlight) - n.chStopInFlight = make(chan struct{}) -} - -// unsubscribeAll unsubscribes all subscriptions -// WARNING: NOT THREAD-SAFE -// This must be called from within the n.stateMu lock -func (n *node) unsubscribeAll() { - for _, sub := range n.subs { - sub.Unsubscribe() - } - n.subs = nil -} - -// getChStopInflight provides a convenience helper that mutex wraps a -// read to the chStopInFlight -func (n *node) getChStopInflight() chan struct{} { - n.stateMu.RLock() - defer n.stateMu.RUnlock() - return n.chStopInFlight -} - -func (n *node) getRPCDomain() string { - if n.http != nil { - return n.http.uri.Host - } - return n.ws.uri.Host -} - -// RPC wrappers - -// CallContext implementation -func (n *node) CallContext(ctx context.Context, result interface{}, method string, args ...interface{}) error { - ctx, cancel, ws, http, err := n.makeLiveQueryCtxAndSafeGetClients(ctx) - if err != nil { - return err - } - defer cancel() - lggr := n.newRqLggr().With( - "method", method, - "args", args, - ) - - lggr.Debug("RPC call: evmclient.Client#CallContext") - start := time.Now() - if http != nil { - err = n.wrapHTTP(http.rpc.CallContext(ctx, result, method, args...)) - } else { - err = n.wrapWS(ws.rpc.CallContext(ctx, result, method, args...)) - } - duration := time.Since(start) - - n.logResult(lggr, err, duration, n.getRPCDomain(), "CallContext") - - return err -} - -func (n *node) BatchCallContext(ctx context.Context, b []rpc.BatchElem) error { - ctx, cancel, ws, http, err := n.makeLiveQueryCtxAndSafeGetClients(ctx) - if err != nil { - return err - } - defer cancel() - lggr := n.newRqLggr().With("nBatchElems", len(b), "batchElems", b) - - lggr.Trace("RPC call: evmclient.Client#BatchCallContext") - start := time.Now() - if http != nil { - err = n.wrapHTTP(http.rpc.BatchCallContext(ctx, b)) - } else { - err = n.wrapWS(ws.rpc.BatchCallContext(ctx, b)) - } - duration := time.Since(start) - - n.logResult(lggr, err, duration, n.getRPCDomain(), "BatchCallContext") - - return err -} - -func (n *node) EthSubscribe(ctx context.Context, channel chan<- *evmtypes.Head, args ...interface{}) (ethereum.Subscription, error) { - ctx, cancel, ws, _, err := n.makeLiveQueryCtxAndSafeGetClients(ctx) - if err != nil { - return nil, err - } - defer cancel() - lggr := n.newRqLggr().With("args", args) - - lggr.Debug("RPC call: evmclient.Client#EthSubscribe") - start := time.Now() - sub, err := ws.rpc.EthSubscribe(ctx, channel, args...) - if err == nil { - n.registerSub(sub) - } - duration := time.Since(start) - - n.logResult(lggr, err, duration, n.getRPCDomain(), "EthSubscribe") - - return sub, err -} - -// GethClient wrappers - -func (n *node) TransactionReceipt(ctx context.Context, txHash common.Hash) (receipt *types.Receipt, err error) { - ctx, cancel, ws, http, err := n.makeLiveQueryCtxAndSafeGetClients(ctx) - if err != nil { - return nil, err - } - defer cancel() - lggr := n.newRqLggr().With("txHash", txHash) - - lggr.Debug("RPC call: evmclient.Client#TransactionReceipt") - - start := time.Now() - if http != nil { - receipt, err = http.geth.TransactionReceipt(ctx, txHash) - err = n.wrapHTTP(err) - } else { - receipt, err = ws.geth.TransactionReceipt(ctx, txHash) - err = n.wrapWS(err) - } - duration := time.Since(start) - - n.logResult(lggr, err, duration, n.getRPCDomain(), "TransactionReceipt", - "receipt", receipt, - ) - - return -} - -func (n *node) TransactionByHash(ctx context.Context, txHash common.Hash) (tx *types.Transaction, err error) { - ctx, cancel, ws, http, err := n.makeLiveQueryCtxAndSafeGetClients(ctx) - if err != nil { - return nil, err - } - defer cancel() - lggr := n.newRqLggr().With("txHash", txHash) - - lggr.Debug("RPC call: evmclient.Client#TransactionByHash") - - start := time.Now() - if http != nil { - tx, _, err = http.geth.TransactionByHash(ctx, txHash) - err = n.wrapHTTP(err) - } else { - tx, _, err = ws.geth.TransactionByHash(ctx, txHash) - err = n.wrapWS(err) - } - duration := time.Since(start) - - n.logResult(lggr, err, duration, n.getRPCDomain(), "TransactionByHash", - "receipt", tx, - ) - - return -} - -func (n *node) HeaderByNumber(ctx context.Context, number *big.Int) (header *types.Header, err error) { - ctx, cancel, ws, http, err := n.makeLiveQueryCtxAndSafeGetClients(ctx) - if err != nil { - return nil, err - } - defer cancel() - lggr := n.newRqLggr().With("number", number) - - lggr.Debug("RPC call: evmclient.Client#HeaderByNumber") - start := time.Now() - if http != nil { - header, err = http.geth.HeaderByNumber(ctx, number) - err = n.wrapHTTP(err) - } else { - header, err = ws.geth.HeaderByNumber(ctx, number) - err = n.wrapWS(err) - } - duration := time.Since(start) - - n.logResult(lggr, err, duration, n.getRPCDomain(), "HeaderByNumber", "header", header) - - return -} - -func (n *node) HeaderByHash(ctx context.Context, hash common.Hash) (header *types.Header, err error) { - ctx, cancel, ws, http, err := n.makeLiveQueryCtxAndSafeGetClients(ctx) - if err != nil { - return nil, err - } - defer cancel() - lggr := n.newRqLggr().With("hash", hash) - - lggr.Debug("RPC call: evmclient.Client#HeaderByHash") - start := time.Now() - if http != nil { - header, err = http.geth.HeaderByHash(ctx, hash) - err = n.wrapHTTP(err) - } else { - header, err = ws.geth.HeaderByHash(ctx, hash) - err = n.wrapWS(err) - } - duration := time.Since(start) - - n.logResult(lggr, err, duration, n.getRPCDomain(), "HeaderByHash", - "header", header, - ) - - return -} - -func (n *node) SendTransaction(ctx context.Context, tx *types.Transaction) error { - ctx, cancel, ws, http, err := n.makeLiveQueryCtxAndSafeGetClients(ctx) - if err != nil { - return err - } - defer cancel() - lggr := n.newRqLggr().With("tx", tx) - - lggr.Debug("RPC call: evmclient.Client#SendTransaction") - start := time.Now() - if http != nil { - err = n.wrapHTTP(http.geth.SendTransaction(ctx, tx)) - } else { - err = n.wrapWS(ws.geth.SendTransaction(ctx, tx)) - } - duration := time.Since(start) - - n.logResult(lggr, err, duration, n.getRPCDomain(), "SendTransaction") - - return err -} - -// PendingNonceAt returns one higher than the highest nonce from both mempool and mined transactions -func (n *node) PendingNonceAt(ctx context.Context, account common.Address) (nonce uint64, err error) { - ctx, cancel, ws, http, err := n.makeLiveQueryCtxAndSafeGetClients(ctx) - if err != nil { - return 0, err - } - defer cancel() - lggr := n.newRqLggr().With("account", account) - - lggr.Debug("RPC call: evmclient.Client#PendingNonceAt") - start := time.Now() - if http != nil { - nonce, err = http.geth.PendingNonceAt(ctx, account) - err = n.wrapHTTP(err) - } else { - nonce, err = ws.geth.PendingNonceAt(ctx, account) - err = n.wrapWS(err) - } - duration := time.Since(start) - - n.logResult(lggr, err, duration, n.getRPCDomain(), "PendingNonceAt", - "nonce", nonce, - ) - - return -} - -// NonceAt is a bit of a misnomer. You might expect it to return the highest -// mined nonce at the given block number, but it actually returns the total -// transaction count which is the highest mined nonce + 1 -func (n *node) NonceAt(ctx context.Context, account common.Address, blockNumber *big.Int) (nonce uint64, err error) { - ctx, cancel, ws, http, err := n.makeLiveQueryCtxAndSafeGetClients(ctx) - if err != nil { - return 0, err - } - defer cancel() - lggr := n.newRqLggr().With("account", account, "blockNumber", blockNumber) - - lggr.Debug("RPC call: evmclient.Client#NonceAt") - start := time.Now() - if http != nil { - nonce, err = http.geth.NonceAt(ctx, account, blockNumber) - err = n.wrapHTTP(err) - } else { - nonce, err = ws.geth.NonceAt(ctx, account, blockNumber) - err = n.wrapWS(err) - } - duration := time.Since(start) - - n.logResult(lggr, err, duration, n.getRPCDomain(), "NonceAt", - "nonce", nonce, - ) - - return -} - -func (n *node) PendingCodeAt(ctx context.Context, account common.Address) (code []byte, err error) { - ctx, cancel, ws, http, err := n.makeLiveQueryCtxAndSafeGetClients(ctx) - if err != nil { - return nil, err - } - defer cancel() - lggr := n.newRqLggr().With("account", account) - - lggr.Debug("RPC call: evmclient.Client#PendingCodeAt") - start := time.Now() - if http != nil { - code, err = http.geth.PendingCodeAt(ctx, account) - err = n.wrapHTTP(err) - } else { - code, err = ws.geth.PendingCodeAt(ctx, account) - err = n.wrapWS(err) - } - duration := time.Since(start) - - n.logResult(lggr, err, duration, n.getRPCDomain(), "PendingCodeAt", - "code", code, - ) - - return -} - -func (n *node) CodeAt(ctx context.Context, account common.Address, blockNumber *big.Int) (code []byte, err error) { - ctx, cancel, ws, http, err := n.makeLiveQueryCtxAndSafeGetClients(ctx) - if err != nil { - return nil, err - } - defer cancel() - lggr := n.newRqLggr().With("account", account, "blockNumber", blockNumber) - - lggr.Debug("RPC call: evmclient.Client#CodeAt") - start := time.Now() - if http != nil { - code, err = http.geth.CodeAt(ctx, account, blockNumber) - err = n.wrapHTTP(err) - } else { - code, err = ws.geth.CodeAt(ctx, account, blockNumber) - err = n.wrapWS(err) - } - duration := time.Since(start) - - n.logResult(lggr, err, duration, n.getRPCDomain(), "CodeAt", - "code", code, - ) - - return -} - -func (n *node) EstimateGas(ctx context.Context, call ethereum.CallMsg) (gas uint64, err error) { - ctx, cancel, ws, http, err := n.makeLiveQueryCtxAndSafeGetClients(ctx) - if err != nil { - return 0, err - } - defer cancel() - lggr := n.newRqLggr().With("call", call) - - lggr.Debug("RPC call: evmclient.Client#EstimateGas") - start := time.Now() - if http != nil { - gas, err = http.geth.EstimateGas(ctx, call) - err = n.wrapHTTP(err) - } else { - gas, err = ws.geth.EstimateGas(ctx, call) - err = n.wrapWS(err) - } - duration := time.Since(start) - - n.logResult(lggr, err, duration, n.getRPCDomain(), "EstimateGas", - "gas", gas, - ) - - return -} - -func (n *node) SuggestGasPrice(ctx context.Context) (price *big.Int, err error) { - ctx, cancel, ws, http, err := n.makeLiveQueryCtxAndSafeGetClients(ctx) - if err != nil { - return nil, err - } - defer cancel() - lggr := n.newRqLggr() - - lggr.Debug("RPC call: evmclient.Client#SuggestGasPrice") - start := time.Now() - if http != nil { - price, err = http.geth.SuggestGasPrice(ctx) - err = n.wrapHTTP(err) - } else { - price, err = ws.geth.SuggestGasPrice(ctx) - err = n.wrapWS(err) - } - duration := time.Since(start) - - n.logResult(lggr, err, duration, n.getRPCDomain(), "SuggestGasPrice", - "price", price, - ) - - return -} - -func (n *node) CallContract(ctx context.Context, msg ethereum.CallMsg, blockNumber *big.Int) (val []byte, err error) { - ctx, cancel, ws, http, err := n.makeLiveQueryCtxAndSafeGetClients(ctx) - if err != nil { - return nil, err - } - defer cancel() - lggr := n.newRqLggr().With("callMsg", msg, "blockNumber", blockNumber) - - lggr.Debug("RPC call: evmclient.Client#CallContract") - start := time.Now() - if http != nil { - val, err = http.geth.CallContract(ctx, msg, blockNumber) - err = n.wrapHTTP(err) - } else { - val, err = ws.geth.CallContract(ctx, msg, blockNumber) - err = n.wrapWS(err) - } - duration := time.Since(start) - - n.logResult(lggr, err, duration, n.getRPCDomain(), "CallContract", - "val", val, - ) - - return -} - -func (n *node) PendingCallContract(ctx context.Context, msg ethereum.CallMsg) (val []byte, err error) { - ctx, cancel, ws, http, err := n.makeLiveQueryCtxAndSafeGetClients(ctx) - if err != nil { - return nil, err - } - defer cancel() - lggr := n.newRqLggr().With("callMsg", msg) - - lggr.Debug("RPC call: evmclient.Client#PendingCallContract") - start := time.Now() - if http != nil { - val, err = http.geth.PendingCallContract(ctx, msg) - err = n.wrapHTTP(err) - } else { - val, err = ws.geth.PendingCallContract(ctx, msg) - err = n.wrapWS(err) - } - duration := time.Since(start) - - n.logResult(lggr, err, duration, n.getRPCDomain(), "PendingCallContract", - "val", val, - ) - - return -} - -func (n *node) BlockByNumber(ctx context.Context, number *big.Int) (b *types.Block, err error) { - ctx, cancel, ws, http, err := n.makeLiveQueryCtxAndSafeGetClients(ctx) - if err != nil { - return nil, err - } - defer cancel() - lggr := n.newRqLggr().With("number", number) - - lggr.Debug("RPC call: evmclient.Client#BlockByNumber") - start := time.Now() - if http != nil { - b, err = http.geth.BlockByNumber(ctx, number) - err = n.wrapHTTP(err) - } else { - b, err = ws.geth.BlockByNumber(ctx, number) - err = n.wrapWS(err) - } - duration := time.Since(start) - - n.logResult(lggr, err, duration, n.getRPCDomain(), "BlockByNumber", - "block", b, - ) - - return -} - -func (n *node) BlockByHash(ctx context.Context, hash common.Hash) (b *types.Block, err error) { - ctx, cancel, ws, http, err := n.makeLiveQueryCtxAndSafeGetClients(ctx) - if err != nil { - return nil, err - } - defer cancel() - lggr := n.newRqLggr().With("hash", hash) - - lggr.Debug("RPC call: evmclient.Client#BlockByHash") - start := time.Now() - if http != nil { - b, err = http.geth.BlockByHash(ctx, hash) - err = n.wrapHTTP(err) - } else { - b, err = ws.geth.BlockByHash(ctx, hash) - err = n.wrapWS(err) - } - duration := time.Since(start) - - n.logResult(lggr, err, duration, n.getRPCDomain(), "BlockByHash", - "block", b, - ) - - return -} - -func (n *node) BlockNumber(ctx context.Context) (height uint64, err error) { - ctx, cancel, ws, http, err := n.makeLiveQueryCtxAndSafeGetClients(ctx) - if err != nil { - return 0, err - } - defer cancel() - lggr := n.newRqLggr() - - lggr.Debug("RPC call: evmclient.Client#BlockNumber") - start := time.Now() - if http != nil { - height, err = http.geth.BlockNumber(ctx) - err = n.wrapHTTP(err) - } else { - height, err = ws.geth.BlockNumber(ctx) - err = n.wrapWS(err) - } - duration := time.Since(start) - - n.logResult(lggr, err, duration, n.getRPCDomain(), "BlockNumber", - "height", height, - ) - - return -} - -func (n *node) BalanceAt(ctx context.Context, account common.Address, blockNumber *big.Int) (balance *big.Int, err error) { - ctx, cancel, ws, http, err := n.makeLiveQueryCtxAndSafeGetClients(ctx) - if err != nil { - return nil, err - } - defer cancel() - lggr := n.newRqLggr().With("account", account.Hex(), "blockNumber", blockNumber) - - lggr.Debug("RPC call: evmclient.Client#BalanceAt") - start := time.Now() - if http != nil { - balance, err = http.geth.BalanceAt(ctx, account, blockNumber) - err = n.wrapHTTP(err) - } else { - balance, err = ws.geth.BalanceAt(ctx, account, blockNumber) - err = n.wrapWS(err) - } - duration := time.Since(start) - - n.logResult(lggr, err, duration, n.getRPCDomain(), "BalanceAt", - "balance", balance, - ) - - return -} - -func (n *node) FilterLogs(ctx context.Context, q ethereum.FilterQuery) (l []types.Log, err error) { - ctx, cancel, ws, http, err := n.makeLiveQueryCtxAndSafeGetClients(ctx) - if err != nil { - return nil, err - } - defer cancel() - lggr := n.newRqLggr().With("q", q) - - lggr.Debug("RPC call: evmclient.Client#FilterLogs") - start := time.Now() - if http != nil { - l, err = http.geth.FilterLogs(ctx, q) - err = n.wrapHTTP(err) - } else { - l, err = ws.geth.FilterLogs(ctx, q) - err = n.wrapWS(err) - } - duration := time.Since(start) - - n.logResult(lggr, err, duration, n.getRPCDomain(), "FilterLogs", - "log", l, - ) - - return -} - -func (n *node) SubscribeFilterLogs(ctx context.Context, q ethereum.FilterQuery, ch chan<- types.Log) (sub ethereum.Subscription, err error) { - ctx, cancel, ws, _, err := n.makeLiveQueryCtxAndSafeGetClients(ctx) - if err != nil { - return nil, err - } - defer cancel() - lggr := n.newRqLggr().With("q", q) - - lggr.Debug("RPC call: evmclient.Client#SubscribeFilterLogs") - start := time.Now() - sub, err = ws.geth.SubscribeFilterLogs(ctx, q, ch) - if err == nil { - n.registerSub(sub) - } - err = n.wrapWS(err) - duration := time.Since(start) - - n.logResult(lggr, err, duration, n.getRPCDomain(), "SubscribeFilterLogs") - - return -} - -func (n *node) SuggestGasTipCap(ctx context.Context) (tipCap *big.Int, err error) { - ctx, cancel, ws, http, err := n.makeLiveQueryCtxAndSafeGetClients(ctx) - if err != nil { - return nil, err - } - defer cancel() - lggr := n.newRqLggr() - - lggr.Debug("RPC call: evmclient.Client#SuggestGasTipCap") - start := time.Now() - if http != nil { - tipCap, err = http.geth.SuggestGasTipCap(ctx) - err = n.wrapHTTP(err) - } else { - tipCap, err = ws.geth.SuggestGasTipCap(ctx) - err = n.wrapWS(err) - } - duration := time.Since(start) - - n.logResult(lggr, err, duration, n.getRPCDomain(), "SuggestGasTipCap", - "tipCap", tipCap, - ) - - return -} - -func (n *node) ChainID() (chainID *big.Int) { return n.chainID } - -// newRqLggr generates a new logger with a unique request ID -func (n *node) newRqLggr() logger.SugaredLogger { - return n.rpcLog.With("requestID", uuid.New()) -} - -func (n *node) logResult( - lggr logger.Logger, - err error, - callDuration time.Duration, - rpcDomain, - callName string, - results ...interface{}, -) { - slggr := logger.Sugared(lggr).With("duration", callDuration, "rpcDomain", rpcDomain, "callName", callName) - promEVMPoolRPCNodeCalls.WithLabelValues(n.chainID.String(), n.name).Inc() - if err == nil { - promEVMPoolRPCNodeCallsSuccess.WithLabelValues(n.chainID.String(), n.name).Inc() - slggr.Tracew(fmt.Sprintf("evmclient.Client#%s RPC call success", callName), results...) - } else { - promEVMPoolRPCNodeCallsFailed.WithLabelValues(n.chainID.String(), n.name).Inc() - slggr.Debugw( - fmt.Sprintf("evmclient.Client#%s RPC call failure", callName), - append(results, "err", err)..., - ) - } - promEVMPoolRPCCallTiming. - WithLabelValues( - n.chainID.String(), // chain id - n.name, // node name - rpcDomain, // rpc domain - "false", // is send only - strconv.FormatBool(err == nil), // is successful - callName, // rpc call name - ). - Observe(float64(callDuration)) -} - -func (n *node) wrapWS(err error) error { - err = wrap(err, fmt.Sprintf("primary websocket (%s)", n.ws.uri.Redacted())) - return err -} - -func (n *node) wrapHTTP(err error) error { - err = wrap(err, fmt.Sprintf("primary http (%s)", n.http.uri.Redacted())) - if err != nil { - n.rpcLog.Debugw("Call failed", "err", err) - } else { - n.rpcLog.Trace("Call succeeded") - } - return err -} - -func wrap(err error, tp string) error { - if err == nil { - return nil - } - if pkgerrors.Cause(err).Error() == "context deadline exceeded" { - err = pkgerrors.Wrap(err, "remote eth node timed out") - } - return pkgerrors.Wrapf(err, "%s call failed", tp) -} - -// makeLiveQueryCtxAndSafeGetClients wraps makeQueryCtx but returns error if node is not NodeStateAlive. -func (n *node) makeLiveQueryCtxAndSafeGetClients(parentCtx context.Context) (ctx context.Context, cancel context.CancelFunc, ws rawclient, http *rawclient, err error) { - // Need to wrap in mutex because state transition can cancel and replace the - // context - n.stateMu.RLock() - if n.state != NodeStateAlive { - err = pkgerrors.Errorf("cannot execute RPC call on node with state: %s", n.state) - n.stateMu.RUnlock() - return - } - cancelCh := n.chStopInFlight - ws = n.ws - if n.http != nil { - cp := *n.http - http = &cp - } - n.stateMu.RUnlock() - ctx, cancel = makeQueryCtx(parentCtx, cancelCh) - return -} - -func (n *node) makeQueryCtx(ctx context.Context) (context.Context, context.CancelFunc) { - return makeQueryCtx(ctx, n.getChStopInflight()) -} - -// makeQueryCtx returns a context that cancels if: -// 1. Passed in ctx cancels -// 2. Passed in channel is closed -// 3. Default timeout is reached (queryTimeout) -func makeQueryCtx(ctx context.Context, ch services.StopChan) (context.Context, context.CancelFunc) { - var chCancel, timeoutCancel context.CancelFunc - ctx, chCancel = ch.Ctx(ctx) - ctx, timeoutCancel = context.WithTimeout(ctx, queryTimeout) - cancel := func() { - chCancel() - timeoutCancel() - } - return ctx, cancel -} - -func (n *node) getNodeMode() string { - if n.http != nil { - return "http" - } - return "websocket" -} - -func (n *node) String() string { - s := fmt.Sprintf("(primary)%s:%s", n.name, n.ws.uri.Redacted()) - if n.http != nil { - s = s + fmt.Sprintf(":%s", n.http.uri.Redacted()) - } - return s -} - -func (n *node) Name() string { - return n.name -} - -func (n *node) Order() int32 { - return n.order -} diff --git a/core/chains/evm/client/node_fsm.go b/core/chains/evm/client/node_fsm.go deleted file mode 100644 index c92af3b4120..00000000000 --- a/core/chains/evm/client/node_fsm.go +++ /dev/null @@ -1,259 +0,0 @@ -package client - -import ( - "fmt" - "math/big" - - "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/client_golang/prometheus/promauto" -) - -var ( - promEVMPoolRPCNodeTransitionsToAlive = promauto.NewCounterVec(prometheus.CounterOpts{ - Name: "evm_pool_rpc_node_num_transitions_to_alive", - Help: fmt.Sprintf("Total number of times node has transitioned to %s", NodeStateAlive), - }, []string{"evmChainID", "nodeName"}) - promEVMPoolRPCNodeTransitionsToInSync = promauto.NewCounterVec(prometheus.CounterOpts{ - Name: "evm_pool_rpc_node_num_transitions_to_in_sync", - Help: fmt.Sprintf("Total number of times node has transitioned from %s to %s", NodeStateOutOfSync, NodeStateAlive), - }, []string{"evmChainID", "nodeName"}) - promEVMPoolRPCNodeTransitionsToOutOfSync = promauto.NewCounterVec(prometheus.CounterOpts{ - Name: "evm_pool_rpc_node_num_transitions_to_out_of_sync", - Help: fmt.Sprintf("Total number of times node has transitioned to %s", NodeStateOutOfSync), - }, []string{"evmChainID", "nodeName"}) - promEVMPoolRPCNodeTransitionsToUnreachable = promauto.NewCounterVec(prometheus.CounterOpts{ - Name: "evm_pool_rpc_node_num_transitions_to_unreachable", - Help: fmt.Sprintf("Total number of times node has transitioned to %s", NodeStateUnreachable), - }, []string{"evmChainID", "nodeName"}) - promEVMPoolRPCNodeTransitionsToInvalidChainID = promauto.NewCounterVec(prometheus.CounterOpts{ - Name: "evm_pool_rpc_node_num_transitions_to_invalid_chain_id", - Help: fmt.Sprintf("Total number of times node has transitioned to %s", NodeStateInvalidChainID), - }, []string{"evmChainID", "nodeName"}) - promEVMPoolRPCNodeTransitionsToUnusable = promauto.NewCounterVec(prometheus.CounterOpts{ - Name: "evm_pool_rpc_node_num_transitions_to_unusable", - Help: fmt.Sprintf("Total number of times node has transitioned to %s", NodeStateUnusable), - }, []string{"evmChainID", "nodeName"}) -) - -// NodeState represents the current state of the node -// Node is a FSM (finite state machine) -// -// Deprecated: to be removed. It is now internal in common/client -type NodeState int - -func (n NodeState) String() string { - switch n { - case NodeStateUndialed: - return "Undialed" - case NodeStateDialed: - return "Dialed" - case NodeStateInvalidChainID: - return "InvalidChainID" - case NodeStateAlive: - return "Alive" - case NodeStateUnreachable: - return "Unreachable" - case NodeStateUnusable: - return "Unusable" - case NodeStateOutOfSync: - return "OutOfSync" - case NodeStateClosed: - return "Closed" - default: - return fmt.Sprintf("NodeState(%d)", n) - } -} - -// GoString prints a prettier state -func (n NodeState) GoString() string { - return fmt.Sprintf("NodeState%s(%d)", n.String(), n) -} - -const ( - // NodeStateUndialed is the first state of a virgin node - NodeStateUndialed = NodeState(iota) - // NodeStateDialed is after a node has successfully dialed but before it has verified the correct chain ID - NodeStateDialed - // NodeStateInvalidChainID is after chain ID verification failed - NodeStateInvalidChainID - // NodeStateAlive is a healthy node after chain ID verification succeeded - NodeStateAlive - // NodeStateUnreachable is a node that cannot be dialed or has disconnected - NodeStateUnreachable - // NodeStateOutOfSync is a node that is accepting connections but exceeded - // the failure threshold without sending any new heads. It will be - // disconnected, then put into a revive loop and re-awakened after redial - // if a new head arrives - NodeStateOutOfSync - // NodeStateUnusable is a sendonly node that has an invalid URL that can never be reached - NodeStateUnusable - // NodeStateClosed is after the connection has been closed and the node is at the end of its lifecycle - NodeStateClosed - // nodeStateLen tracks the number of states - nodeStateLen -) - -// allNodeStates represents all possible states a node can be in -var allNodeStates []NodeState - -func init() { - for s := NodeState(0); s < nodeStateLen; s++ { - allNodeStates = append(allNodeStates, s) - } -} - -// FSM methods - -// State allows reading the current state of the node. -func (n *node) State() NodeState { - n.stateMu.RLock() - defer n.stateMu.RUnlock() - return n.state -} - -func (n *node) StateAndLatest() (NodeState, int64, *big.Int) { - n.stateMu.RLock() - defer n.stateMu.RUnlock() - return n.state, n.stateLatestBlockNumber, n.stateLatestTotalDifficulty -} - -// setState is only used by internal state management methods. -// This is low-level; care should be taken by the caller to ensure the new state is a valid transition. -// State changes should always be synchronous: only one goroutine at a time should change state. -// n.stateMu should not be locked for long periods of time because external clients expect a timely response from n.State() -func (n *node) setState(s NodeState) { - n.stateMu.Lock() - defer n.stateMu.Unlock() - n.state = s -} - -// declareXXX methods change the state and pass conrol off the new state -// management goroutine - -func (n *node) declareAlive() { - n.transitionToAlive(func() { - n.lfcLog.Infow("RPC Node is online", "nodeState", n.state) - n.wg.Add(1) - go n.aliveLoop() - }) -} - -func (n *node) transitionToAlive(fn func()) { - promEVMPoolRPCNodeTransitionsToAlive.WithLabelValues(n.chainID.String(), n.name).Inc() - n.stateMu.Lock() - defer n.stateMu.Unlock() - if n.state == NodeStateClosed { - return - } - switch n.state { - case NodeStateDialed, NodeStateInvalidChainID: - n.state = NodeStateAlive - default: - panic(fmt.Sprintf("cannot transition from %#v to %#v", n.state, NodeStateAlive)) - } - fn() -} - -// declareInSync puts a node back into Alive state, allowing it to be used by -// pool consumers again -func (n *node) declareInSync() { - n.transitionToInSync(func() { - n.lfcLog.Infow("RPC Node is back in sync", "nodeState", n.state) - n.wg.Add(1) - go n.aliveLoop() - }) -} - -func (n *node) transitionToInSync(fn func()) { - promEVMPoolRPCNodeTransitionsToAlive.WithLabelValues(n.chainID.String(), n.name).Inc() - promEVMPoolRPCNodeTransitionsToInSync.WithLabelValues(n.chainID.String(), n.name).Inc() - n.stateMu.Lock() - defer n.stateMu.Unlock() - if n.state == NodeStateClosed { - return - } - switch n.state { - case NodeStateOutOfSync: - n.state = NodeStateAlive - default: - panic(fmt.Sprintf("cannot transition from %#v to %#v", n.state, NodeStateAlive)) - } - fn() -} - -// declareOutOfSync puts a node into OutOfSync state, disconnecting all current -// clients and making it unavailable for use until back in-sync. -func (n *node) declareOutOfSync(isOutOfSync func(num int64, td *big.Int) bool) { - n.transitionToOutOfSync(func() { - n.lfcLog.Errorw("RPC Node is out of sync", "nodeState", n.state) - n.wg.Add(1) - go n.outOfSyncLoop(isOutOfSync) - }) -} - -func (n *node) transitionToOutOfSync(fn func()) { - promEVMPoolRPCNodeTransitionsToOutOfSync.WithLabelValues(n.chainID.String(), n.name).Inc() - n.stateMu.Lock() - defer n.stateMu.Unlock() - if n.state == NodeStateClosed { - return - } - switch n.state { - case NodeStateAlive: - n.disconnectAll() - n.state = NodeStateOutOfSync - default: - panic(fmt.Sprintf("cannot transition from %#v to %#v", n.state, NodeStateOutOfSync)) - } - fn() -} - -func (n *node) declareUnreachable() { - n.transitionToUnreachable(func() { - n.lfcLog.Errorw("RPC Node is unreachable", "nodeState", n.state) - n.wg.Add(1) - go n.unreachableLoop() - }) -} - -func (n *node) transitionToUnreachable(fn func()) { - promEVMPoolRPCNodeTransitionsToUnreachable.WithLabelValues(n.chainID.String(), n.name).Inc() - n.stateMu.Lock() - defer n.stateMu.Unlock() - if n.state == NodeStateClosed { - return - } - switch n.state { - case NodeStateUndialed, NodeStateDialed, NodeStateAlive, NodeStateOutOfSync, NodeStateInvalidChainID: - n.disconnectAll() - n.state = NodeStateUnreachable - default: - panic(fmt.Sprintf("cannot transition from %#v to %#v", n.state, NodeStateUnreachable)) - } - fn() -} - -func (n *node) declareInvalidChainID() { - n.transitionToInvalidChainID(func() { - n.lfcLog.Errorw("RPC Node has the wrong chain ID", "nodeState", n.state) - n.wg.Add(1) - go n.invalidChainIDLoop() - }) -} - -func (n *node) transitionToInvalidChainID(fn func()) { - promEVMPoolRPCNodeTransitionsToInvalidChainID.WithLabelValues(n.chainID.String(), n.name).Inc() - n.stateMu.Lock() - defer n.stateMu.Unlock() - if n.state == NodeStateClosed { - return - } - switch n.state { - case NodeStateDialed, NodeStateOutOfSync: - n.disconnectAll() - n.state = NodeStateInvalidChainID - default: - panic(fmt.Sprintf("cannot transition from %#v to %#v", n.state, NodeStateInvalidChainID)) - } - fn() -} diff --git a/core/chains/evm/client/node_fsm_test.go b/core/chains/evm/client/node_fsm_test.go deleted file mode 100644 index 321bbc7a309..00000000000 --- a/core/chains/evm/client/node_fsm_test.go +++ /dev/null @@ -1,173 +0,0 @@ -package client - -import ( - "testing" - "time" - - "github.com/ethereum/go-ethereum" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/smartcontractkit/chainlink-common/pkg/logger" - "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" -) - -type fnMock struct{ calls int } - -func (fm *fnMock) Fn() { - fm.calls++ -} - -func (fm *fnMock) AssertNotCalled(t *testing.T) { - assert.Equal(t, 0, fm.calls) -} - -func (fm *fnMock) AssertCalled(t *testing.T) { - assert.Greater(t, fm.calls, 0) -} - -func (fm *fnMock) AssertNumberOfCalls(t *testing.T, n int) { - assert.Equal(t, n, fm.calls) -} - -var _ ethereum.Subscription = (*subMock)(nil) - -type subMock struct{ unsubbed bool } - -func (s *subMock) Unsubscribe() { - s.unsubbed = true -} -func (s *subMock) Err() <-chan error { return nil } - -func TestUnit_Node_StateTransitions(t *testing.T) { - t.Parallel() - - s := testutils.NewWSServer(t, testutils.FixtureChainID, nil) - iN := NewNode(TestNodePoolConfig{}, time.Second*0, logger.Test(t), *s.WSURL(), nil, "test node", 42, nil, 1) - n := iN.(*node) - - assert.Equal(t, NodeStateUndialed, n.State()) - - t.Run("setState", func(t *testing.T) { - n.setState(NodeStateAlive) - assert.Equal(t, NodeStateAlive, n.State()) - n.setState(NodeStateUndialed) - assert.Equal(t, NodeStateUndialed, n.State()) - }) - - // must dial to set rpc client for use in state transitions - err := n.dial(testutils.Context(t)) - require.NoError(t, err) - - t.Run("transitionToAlive", func(t *testing.T) { - m := new(fnMock) - assert.Panics(t, func() { - n.transitionToAlive(m.Fn) - }) - m.AssertNotCalled(t) - n.setState(NodeStateDialed) - n.transitionToAlive(m.Fn) - m.AssertNumberOfCalls(t, 1) - n.setState(NodeStateInvalidChainID) - n.transitionToAlive(m.Fn) - m.AssertNumberOfCalls(t, 2) - }) - - t.Run("transitionToInSync", func(t *testing.T) { - m := new(fnMock) - n.setState(NodeStateAlive) - assert.Panics(t, func() { - n.transitionToInSync(m.Fn) - }) - m.AssertNotCalled(t) - n.setState(NodeStateOutOfSync) - n.transitionToInSync(m.Fn) - m.AssertCalled(t) - }) - t.Run("transitionToOutOfSync", func(t *testing.T) { - m := new(fnMock) - n.setState(NodeStateOutOfSync) - assert.Panics(t, func() { - n.transitionToOutOfSync(m.Fn) - }) - m.AssertNotCalled(t) - n.setState(NodeStateAlive) - n.transitionToOutOfSync(m.Fn) - m.AssertCalled(t) - }) - t.Run("transitionToOutOfSync unsubscribes everything", func(t *testing.T) { - m := new(fnMock) - n.setState(NodeStateAlive) - sub := &subMock{} - n.registerSub(sub) - n.transitionToOutOfSync(m.Fn) - m.AssertNumberOfCalls(t, 1) - assert.True(t, sub.unsubbed) - }) - t.Run("transitionToUnreachable", func(t *testing.T) { - m := new(fnMock) - n.setState(NodeStateUnreachable) - assert.Panics(t, func() { - n.transitionToUnreachable(m.Fn) - }) - m.AssertNotCalled(t) - n.setState(NodeStateDialed) - n.transitionToUnreachable(m.Fn) - m.AssertNumberOfCalls(t, 1) - n.setState(NodeStateAlive) - n.transitionToUnreachable(m.Fn) - m.AssertNumberOfCalls(t, 2) - n.setState(NodeStateOutOfSync) - n.transitionToUnreachable(m.Fn) - m.AssertNumberOfCalls(t, 3) - n.setState(NodeStateUndialed) - n.transitionToUnreachable(m.Fn) - m.AssertNumberOfCalls(t, 4) - n.setState(NodeStateInvalidChainID) - n.transitionToUnreachable(m.Fn) - m.AssertNumberOfCalls(t, 5) - }) - t.Run("transitionToUnreachable unsubscribes everything", func(t *testing.T) { - m := new(fnMock) - n.setState(NodeStateDialed) - sub := &subMock{} - n.registerSub(sub) - n.transitionToUnreachable(m.Fn) - m.AssertNumberOfCalls(t, 1) - assert.True(t, sub.unsubbed) - }) - t.Run("transitionToInvalidChainID", func(t *testing.T) { - m := new(fnMock) - n.setState(NodeStateUnreachable) - assert.Panics(t, func() { - n.transitionToInvalidChainID(m.Fn) - }) - m.AssertNotCalled(t) - n.setState(NodeStateDialed) - n.transitionToInvalidChainID(m.Fn) - n.setState(NodeStateOutOfSync) - n.transitionToInvalidChainID(m.Fn) - m.AssertNumberOfCalls(t, 2) - }) - t.Run("transitionToInvalidChainID unsubscribes everything", func(t *testing.T) { - m := new(fnMock) - n.setState(NodeStateDialed) - sub := &subMock{} - n.registerSub(sub) - n.transitionToInvalidChainID(m.Fn) - m.AssertNumberOfCalls(t, 1) - assert.True(t, sub.unsubbed) - }) - t.Run("Close", func(t *testing.T) { - // first attempt errors due to node being unstarted - assert.Error(t, n.Close()) - // must start to allow closing - err := n.StartOnce("test node", func() error { return nil }) - assert.NoError(t, err) - assert.NoError(t, n.Close()) - - assert.Equal(t, NodeStateClosed, n.State()) - // second attempt errors due to node being stopped twice - assert.Error(t, n.Close()) - }) -} diff --git a/core/chains/evm/client/node_lifecycle.go b/core/chains/evm/client/node_lifecycle.go deleted file mode 100644 index c18c8032009..00000000000 --- a/core/chains/evm/client/node_lifecycle.go +++ /dev/null @@ -1,451 +0,0 @@ -package client - -import ( - "context" - "fmt" - "math" - "math/big" - "time" - - pkgerrors "github.com/pkg/errors" - "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/client_golang/prometheus/promauto" - - "github.com/smartcontractkit/chainlink-common/pkg/logger" - cutils "github.com/smartcontractkit/chainlink-common/pkg/utils" - bigmath "github.com/smartcontractkit/chainlink-common/pkg/utils/big_math" - - evmtypes "github.com/smartcontractkit/chainlink/v2/core/chains/evm/types" - "github.com/smartcontractkit/chainlink/v2/core/chains/evm/utils" -) - -var ( - promEVMPoolRPCNodeHighestSeenBlock = promauto.NewGaugeVec(prometheus.GaugeOpts{ - Name: "evm_pool_rpc_node_highest_seen_block", - Help: "The highest seen block for the given RPC node", - }, []string{"evmChainID", "nodeName"}) - promEVMPoolRPCNodeNumSeenBlocks = promauto.NewCounterVec(prometheus.CounterOpts{ - Name: "evm_pool_rpc_node_num_seen_blocks", - Help: "The total number of new blocks seen by the given RPC node", - }, []string{"evmChainID", "nodeName"}) - promEVMPoolRPCNodePolls = promauto.NewCounterVec(prometheus.CounterOpts{ - Name: "evm_pool_rpc_node_polls_total", - Help: "The total number of poll checks for the given RPC node", - }, []string{"evmChainID", "nodeName"}) - promEVMPoolRPCNodePollsFailed = promauto.NewCounterVec(prometheus.CounterOpts{ - Name: "evm_pool_rpc_node_polls_failed", - Help: "The total number of failed poll checks for the given RPC node", - }, []string{"evmChainID", "nodeName"}) - promEVMPoolRPCNodePollsSuccess = promauto.NewCounterVec(prometheus.CounterOpts{ - Name: "evm_pool_rpc_node_polls_success", - Help: "The total number of successful poll checks for the given RPC node", - }, []string{"evmChainID", "nodeName"}) -) - -// zombieNodeCheckInterval controls how often to re-check to see if we need to -// state change in case we have to force a state transition due to no available -// nodes. -// NOTE: This only applies to out-of-sync nodes if they are the last available node -func zombieNodeCheckInterval(noNewHeadsThreshold time.Duration) time.Duration { - interval := noNewHeadsThreshold - if interval <= 0 || interval > queryTimeout { - interval = queryTimeout - } - return cutils.WithJitter(interval) -} - -func (n *node) setLatestReceived(blockNumber int64, totalDifficulty *big.Int) { - n.stateMu.Lock() - defer n.stateMu.Unlock() - n.stateLatestBlockNumber = blockNumber - n.stateLatestTotalDifficulty = totalDifficulty -} - -const ( - msgCannotDisable = "but cannot disable this connection because there are no other RPC endpoints, or all other RPC endpoints are dead." - msgDegradedState = "Chainlink is now operating in a degraded state and urgent action is required to resolve the issue" -) - -// Node is a FSM -// Each state has a loop that goes with it, which monitors the node and moves it into another state as necessary. -// Only one loop must run at a time. -// Each loop passes control onto the next loop as it exits, except when the node is Closed which terminates the loop permanently. - -// This handles node lifecycle for the ALIVE state -// Should only be run ONCE per node, after a successful Dial -func (n *node) aliveLoop() { - defer n.wg.Done() - ctx, cancel := n.stopCh.NewCtx() - defer cancel() - - { - // sanity check - state := n.State() - switch state { - case NodeStateAlive: - case NodeStateClosed: - return - default: - panic(fmt.Sprintf("aliveLoop can only run for node in Alive state, got: %s", state)) - } - } - - noNewHeadsTimeoutThreshold := n.noNewHeadsThreshold - pollFailureThreshold := n.nodePoolCfg.PollFailureThreshold() - pollInterval := n.nodePoolCfg.PollInterval() - - lggr := logger.Sugared(n.lfcLog).Named("Alive").With("noNewHeadsTimeoutThreshold", noNewHeadsTimeoutThreshold, "pollInterval", pollInterval, "pollFailureThreshold", pollFailureThreshold) - lggr.Tracew("Alive loop starting", "nodeState", n.State()) - - headsC := make(chan *evmtypes.Head) - sub, err := n.EthSubscribe(ctx, headsC, "newHeads") - if err != nil { - lggr.Errorw("Initial subscribe for heads failed", "nodeState", n.State()) - n.declareUnreachable() - return - } - n.aliveLoopSub = sub - defer sub.Unsubscribe() - - var outOfSyncT *time.Ticker - var outOfSyncTC <-chan time.Time - if noNewHeadsTimeoutThreshold > 0 { - lggr.Debugw("Head liveness checking enabled", "nodeState", n.State()) - outOfSyncT = time.NewTicker(noNewHeadsTimeoutThreshold) - defer outOfSyncT.Stop() - outOfSyncTC = outOfSyncT.C - } else { - lggr.Debug("Head liveness checking disabled") - } - - var pollCh <-chan time.Time - if pollInterval > 0 { - lggr.Debug("Polling enabled") - pollT := time.NewTicker(pollInterval) - defer pollT.Stop() - pollCh = pollT.C - if pollFailureThreshold > 0 { - // polling can be enabled with no threshold to enable polling but - // the node will not be marked offline regardless of the number of - // poll failures - lggr.Debug("Polling liveness checking enabled") - } - } else { - lggr.Debug("Polling disabled") - } - - _, highestReceivedBlockNumber, _ := n.StateAndLatest() - var pollFailures uint32 - - for { - select { - case <-ctx.Done(): - return - case <-pollCh: - promEVMPoolRPCNodePolls.WithLabelValues(n.chainID.String(), n.name).Inc() - lggr.Tracew("Polling for version", "nodeState", n.State(), "pollFailures", pollFailures) - var version string - if err := func(ctx context.Context) error { - ctx, cancel := context.WithTimeout(ctx, pollInterval) - defer cancel() - ctx, cancel2 := n.makeQueryCtx(ctx) - defer cancel2() - return n.CallContext(ctx, &version, "web3_clientVersion") - }(ctx); err != nil { - // prevent overflow - if pollFailures < math.MaxUint32 { - promEVMPoolRPCNodePollsFailed.WithLabelValues(n.chainID.String(), n.name).Inc() - pollFailures++ - } - lggr.Warnw(fmt.Sprintf("Poll failure, RPC endpoint %s failed to respond properly", n.String()), "err", err, "pollFailures", pollFailures, "nodeState", n.State()) - } else { - lggr.Debugw("Version poll successful", "nodeState", n.State(), "clientVersion", version) - promEVMPoolRPCNodePollsSuccess.WithLabelValues(n.chainID.String(), n.name).Inc() - pollFailures = 0 - } - if pollFailureThreshold > 0 && pollFailures >= pollFailureThreshold { - lggr.Errorw(fmt.Sprintf("RPC endpoint failed to respond to %d consecutive polls", pollFailures), "pollFailures", pollFailures, "nodeState", n.State()) - if n.nLiveNodes != nil { - if l, _, _ := n.nLiveNodes(); l < 2 { - lggr.Criticalf("RPC endpoint failed to respond to polls; %s %s", msgCannotDisable, msgDegradedState) - continue - } - } - n.declareUnreachable() - return - } - _, num, td := n.StateAndLatest() - if outOfSync, liveNodes := n.syncStatus(num, td); outOfSync { - // note: there must be another live node for us to be out of sync - lggr.Errorw("RPC endpoint has fallen behind", "blockNumber", num, "totalDifficulty", td, "nodeState", n.State()) - if liveNodes < 2 { - lggr.Criticalf("RPC endpoint has fallen behind; %s %s", msgCannotDisable, msgDegradedState) - continue - } - n.declareOutOfSync(n.isOutOfSync) - return - } - case bh, open := <-headsC: - if !open { - lggr.Errorw("Subscription channel unexpectedly closed", "nodeState", n.State()) - n.declareUnreachable() - return - } - promEVMPoolRPCNodeNumSeenBlocks.WithLabelValues(n.chainID.String(), n.name).Inc() - lggr.Tracew("Got head", "head", bh) - if bh.Number > highestReceivedBlockNumber { - promEVMPoolRPCNodeHighestSeenBlock.WithLabelValues(n.chainID.String(), n.name).Set(float64(bh.Number)) - lggr.Tracew("Got higher block number, resetting timer", "latestReceivedBlockNumber", highestReceivedBlockNumber, "blockNumber", bh.Number, "nodeState", n.State()) - highestReceivedBlockNumber = bh.Number - } else { - lggr.Tracew("Ignoring previously seen block number", "latestReceivedBlockNumber", highestReceivedBlockNumber, "blockNumber", bh.Number, "nodeState", n.State()) - } - if outOfSyncT != nil { - outOfSyncT.Reset(noNewHeadsTimeoutThreshold) - } - n.setLatestReceived(bh.Number, bh.TotalDifficulty) - case err := <-sub.Err(): - lggr.Errorw("Subscription was terminated", "err", err, "nodeState", n.State()) - n.declareUnreachable() - return - case <-outOfSyncTC: - // We haven't received a head on the channel for at least the - // threshold amount of time, mark it broken - lggr.Errorw(fmt.Sprintf("RPC endpoint detected out of sync; no new heads received for %s (last head received was %v)", noNewHeadsTimeoutThreshold, highestReceivedBlockNumber), "nodeState", n.State(), "latestReceivedBlockNumber", highestReceivedBlockNumber, "noNewHeadsTimeoutThreshold", noNewHeadsTimeoutThreshold) - if n.nLiveNodes != nil { - if l, _, _ := n.nLiveNodes(); l < 2 { - lggr.Criticalf("RPC endpoint detected out of sync; %s %s", msgCannotDisable, msgDegradedState) - // We don't necessarily want to wait the full timeout to check again, we should - // check regularly and log noisily in this state - outOfSyncT.Reset(zombieNodeCheckInterval(n.noNewHeadsThreshold)) - continue - } - } - n.declareOutOfSync(func(num int64, td *big.Int) bool { return num < highestReceivedBlockNumber }) - return - } - } -} - -func (n *node) isOutOfSync(num int64, td *big.Int) (outOfSync bool) { - outOfSync, _ = n.syncStatus(num, td) - return -} - -// syncStatus returns outOfSync true if num or td is more than SyncThresold behind the best node. -// Always returns outOfSync false for SyncThreshold 0. -// liveNodes is only included when outOfSync is true. -func (n *node) syncStatus(num int64, td *big.Int) (outOfSync bool, liveNodes int) { - if n.nLiveNodes == nil { - return // skip for tests - } - threshold := n.nodePoolCfg.SyncThreshold() - if threshold == 0 { - return // disabled - } - // Check against best node - ln, highest, greatest := n.nLiveNodes() - mode := n.nodePoolCfg.SelectionMode() - switch mode { - case NodeSelectionMode_HighestHead, NodeSelectionMode_RoundRobin, NodeSelectionMode_PriorityLevel: - return num < highest-int64(threshold), ln - case NodeSelectionMode_TotalDifficulty: - bigThreshold := big.NewInt(int64(threshold)) - return td.Cmp(bigmath.Sub(greatest, bigThreshold)) < 0, ln - default: - panic("unrecognized NodeSelectionMode: " + mode) - } -} - -const ( - msgReceivedBlock = "Received block for RPC node, waiting until back in-sync to mark as live again" - msgInSync = "RPC node back in sync" -) - -// outOfSyncLoop takes an OutOfSync node and waits until isOutOfSync returns false to go back to live status -func (n *node) outOfSyncLoop(isOutOfSync func(num int64, td *big.Int) bool) { - defer n.wg.Done() - ctx, cancel := n.stopCh.NewCtx() - defer cancel() - - { - // sanity check - state := n.State() - switch state { - case NodeStateOutOfSync: - case NodeStateClosed: - return - default: - panic(fmt.Sprintf("outOfSyncLoop can only run for node in OutOfSync state, got: %s", state)) - } - } - - outOfSyncAt := time.Now() - - lggr := logger.Sugared(logger.Named(n.lfcLog, "OutOfSync")) - lggr.Debugw("Trying to revive out-of-sync RPC node", "nodeState", n.State()) - - // Need to redial since out-of-sync nodes are automatically disconnected - if err := n.dial(ctx); err != nil { - lggr.Errorw("Failed to dial out-of-sync RPC node", "nodeState", n.State()) - n.declareUnreachable() - return - } - - // Manually re-verify since out-of-sync nodes are automatically disconnected - if err := n.verify(ctx); err != nil { - lggr.Errorw(fmt.Sprintf("Failed to verify out-of-sync RPC node: %v", err), "err", err) - n.declareInvalidChainID() - return - } - - lggr.Tracew("Successfully subscribed to heads feed on out-of-sync RPC node", "nodeState", n.State()) - - ch := make(chan *evmtypes.Head) - subCtx, cancel := n.makeQueryCtx(ctx) - // raw call here to bypass node state checking - sub, err := n.ws.rpc.EthSubscribe(subCtx, ch, "newHeads") - cancel() - if err != nil { - lggr.Errorw("Failed to subscribe heads on out-of-sync RPC node", "nodeState", n.State(), "err", err) - n.declareUnreachable() - return - } - defer sub.Unsubscribe() - - for { - select { - case <-ctx.Done(): - return - case head, open := <-ch: - if !open { - lggr.Error("Subscription channel unexpectedly closed", "nodeState", n.State()) - n.declareUnreachable() - return - } - n.setLatestReceived(head.Number, head.TotalDifficulty) - if !isOutOfSync(head.Number, head.TotalDifficulty) { - // back in-sync! flip back into alive loop - lggr.Infow(fmt.Sprintf("%s: %s. Node was out-of-sync for %s", msgInSync, n.String(), time.Since(outOfSyncAt)), "blockNumber", head.Number, "totalDifficulty", head.TotalDifficulty, "nodeState", n.State()) - n.declareInSync() - return - } - lggr.Debugw(msgReceivedBlock, "blockNumber", head.Number, "totalDifficulty", head.TotalDifficulty, "nodeState", n.State()) - case <-time.After(zombieNodeCheckInterval(n.noNewHeadsThreshold)): - if n.nLiveNodes != nil { - if l, _, _ := n.nLiveNodes(); l < 1 { - lggr.Critical("RPC endpoint is still out of sync, but there are no other available nodes. This RPC node will be forcibly moved back into the live pool in a degraded state") - n.declareInSync() - return - } - } - case err := <-sub.Err(): - lggr.Errorw("Subscription was terminated", "nodeState", n.State(), "err", err) - n.declareUnreachable() - return - } - } -} - -func (n *node) unreachableLoop() { - defer n.wg.Done() - ctx, cancel := n.stopCh.NewCtx() - defer cancel() - - { - // sanity check - state := n.State() - switch state { - case NodeStateUnreachable: - case NodeStateClosed: - return - default: - panic(fmt.Sprintf("unreachableLoop can only run for node in Unreachable state, got: %s", state)) - } - } - - unreachableAt := time.Now() - - lggr := logger.Sugared(logger.Named(n.lfcLog, "Unreachable")) - lggr.Debugw("Trying to revive unreachable RPC node", "nodeState", n.State()) - - dialRetryBackoff := utils.NewRedialBackoff() - - for { - select { - case <-ctx.Done(): - return - case <-time.After(dialRetryBackoff.Duration()): - lggr.Tracew("Trying to re-dial RPC node", "nodeState", n.State()) - - err := n.dial(ctx) - if err != nil { - lggr.Errorw(fmt.Sprintf("Failed to redial RPC node; still unreachable: %v", err), "err", err, "nodeState", n.State()) - continue - } - - n.setState(NodeStateDialed) - - err = n.verify(ctx) - - if pkgerrors.Is(err, errInvalidChainID) { - lggr.Errorw("Failed to redial RPC node; remote endpoint returned the wrong chain ID", "err", err) - n.declareInvalidChainID() - return - } else if err != nil { - lggr.Errorw(fmt.Sprintf("Failed to redial RPC node; verify failed: %v", err), "err", err) - n.declareUnreachable() - return - } - - lggr.Infow(fmt.Sprintf("Successfully redialled and verified RPC node %s. Node was offline for %s", n.String(), time.Since(unreachableAt)), "nodeState", n.State()) - n.declareAlive() - return - } - } -} - -func (n *node) invalidChainIDLoop() { - defer n.wg.Done() - ctx, cancel := n.stopCh.NewCtx() - defer cancel() - - { - // sanity check - state := n.State() - switch state { - case NodeStateInvalidChainID: - case NodeStateClosed: - return - default: - panic(fmt.Sprintf("invalidChainIDLoop can only run for node in InvalidChainID state, got: %s", state)) - } - } - - invalidAt := time.Now() - - lggr := logger.Named(n.lfcLog, "InvalidChainID") - lggr.Debugw(fmt.Sprintf("Periodically re-checking RPC node %s with invalid chain ID", n.String()), "nodeState", n.State()) - - chainIDRecheckBackoff := utils.NewRedialBackoff() - - for { - select { - case <-ctx.Done(): - return - case <-time.After(chainIDRecheckBackoff.Duration()): - err := n.verify(ctx) - if pkgerrors.Is(err, errInvalidChainID) { - lggr.Errorw("Failed to verify RPC node; remote endpoint returned the wrong chain ID", "err", err) - continue - } else if err != nil { - lggr.Errorw(fmt.Sprintf("Unexpected error while verifying RPC node chain ID; %v", err), "err", err) - n.declareUnreachable() - return - } - lggr.Infow(fmt.Sprintf("Successfully verified RPC node. Node was offline for %s", time.Since(invalidAt)), "nodeState", n.State()) - n.declareAlive() - return - } - } -} diff --git a/core/chains/evm/client/node_lifecycle_test.go b/core/chains/evm/client/node_lifecycle_test.go deleted file mode 100644 index 878ecabe600..00000000000 --- a/core/chains/evm/client/node_lifecycle_test.go +++ /dev/null @@ -1,857 +0,0 @@ -package client - -import ( - "fmt" - "math/big" - "sync/atomic" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/tidwall/gjson" - "go.uber.org/zap" - - "github.com/smartcontractkit/chainlink-common/pkg/logger" - "github.com/smartcontractkit/chainlink/v2/core/chains/evm/config" - evmtypes "github.com/smartcontractkit/chainlink/v2/core/chains/evm/types" - "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" -) - -func standardHandler(method string, _ gjson.Result) (resp testutils.JSONRPCResponse) { - if method == "eth_subscribe" { - resp.Result = `"0x00"` - resp.Notify = HeadResult - return - } - return -} - -func newTestNode(t *testing.T, cfg config.NodePool, noNewHeadsThresholds time.Duration) *node { - return newTestNodeWithCallback(t, cfg, noNewHeadsThresholds, standardHandler) -} - -func newTestNodeWithCallback(t *testing.T, cfg config.NodePool, noNewHeadsThreshold time.Duration, callback testutils.JSONRPCHandler) *node { - s := testutils.NewWSServer(t, testutils.FixtureChainID, callback) - iN := NewNode(cfg, noNewHeadsThreshold, logger.Test(t), *s.WSURL(), nil, "test node", 42, testutils.FixtureChainID, 1) - n := iN.(*node) - return n -} - -// dial sets up the node and puts it into the live state, bypassing the -// normal Start() method which would fire off unwanted goroutines -func dial(t *testing.T, n *node) { - ctx := testutils.Context(t) - require.NoError(t, n.dial(ctx)) - n.setState(NodeStateAlive) - start(t, n) -} - -func start(t *testing.T, n *node) { - // must start to allow closing - err := n.StartOnce("test node", func() error { return nil }) - assert.NoError(t, err) -} - -func makeHeadResult(n int) string { - return fmt.Sprintf( - `{"difficulty":"0xf3a00","extraData":"0xd883010503846765746887676f312e372e318664617277696e","gasLimit":"0xffc001","gasUsed":"0x0","hash":"0x41800b5c3f1717687d85fc9018faac0a6e90b39deaa0b99e7fe4fe796ddeb26a","logsBloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","miner":"0xd1aeb42885a43b72b518182ef893125814811048","mixHash":"0x0f98b15f1a4901a7e9204f3c500a7bd527b3fb2c3340e12176a44b83e414a69e","nonce":"0x0ece08ea8c49dfd9","number":"%s","parentHash":"0x41941023680923e0fe4d74a34bdac8141f2540e3ae90623718e47d66d1ca4a2d","receiptsRoot":"0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421","sha3Uncles":"0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347","size":"0x218","stateRoot":"0xc7b01007a10da045eacb90385887dd0c38fcb5db7393006bdde24b93873c334b","timestamp":"0x58318da2","totalDifficulty":"0x1f3a00","transactions":[],"transactionsRoot":"0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421","uncles":[]}`, - testutils.IntToHex(n), - ) -} - -func makeNewHeadWSMessage(n int) string { - return fmt.Sprintf(`{"jsonrpc":"2.0","method":"eth_subscription","params":{"subscription":"0x00","result":%s}}`, makeHeadResult(n)) -} - -func TestUnit_NodeLifecycle_aliveLoop(t *testing.T) { - t.Parallel() - - t.Run("with no poll and sync timeouts, exits on close", func(t *testing.T) { - pollAndSyncTimeoutsDisabledCfg := TestNodePoolConfig{} - n := newTestNode(t, pollAndSyncTimeoutsDisabledCfg, 0*time.Second) - dial(t, n) - - ch := make(chan struct{}) - n.wg.Add(1) - go func() { - defer close(ch) - n.aliveLoop() - }() - assert.NoError(t, n.Close()) - testutils.WaitWithTimeout(t, ch, "expected aliveLoop to exit") - }) - - t.Run("with no poll failures past threshold, stays alive", func(t *testing.T) { - threshold := 5 - cfg := TestNodePoolConfig{NodePollFailureThreshold: uint32(threshold), NodePollInterval: testutils.TestInterval} - var calls atomic.Int32 - n := newTestNodeWithCallback(t, cfg, time.Second*0, func(method string, params gjson.Result) (resp testutils.JSONRPCResponse) { - switch method { - case "eth_subscribe": - resp.Result = `"0x00"` - resp.Notify = makeHeadResult(0) - return - case "eth_unsubscribe": - resp.Result = "true" - return - case "web3_clientVersion": - defer calls.Add(1) - // It starts working right before it hits threshold - if int(calls.Load())+1 >= threshold { - resp.Result = `"test client version"` - return - } - resp.Result = "this will error" - return - default: - t.Errorf("unexpected RPC method: %s", method) - } - return - }) - dial(t, n) - defer func() { assert.NoError(t, n.Close()) }() - - n.wg.Add(1) - go n.aliveLoop() - - testutils.AssertEventually(t, func() bool { - // Need to wait for one complete cycle before checking state so add - // 1 to threshold - return int(calls.Load()) > threshold+1 - }) - - assert.Equal(t, NodeStateAlive, n.State()) - }) - - t.Run("with threshold poll failures, transitions to unreachable", func(t *testing.T) { - syncTimeoutsDisabledCfg := TestNodePoolConfig{NodePollFailureThreshold: 3, NodePollInterval: testutils.TestInterval} - n := newTestNode(t, syncTimeoutsDisabledCfg, time.Second*0) - dial(t, n) - defer func() { assert.NoError(t, n.Close()) }() - - n.wg.Add(1) - go n.aliveLoop() - - testutils.AssertEventually(t, func() bool { - return n.State() == NodeStateUnreachable - }) - }) - - t.Run("with threshold poll failures, but we are the last node alive, forcibly keeps it alive", func(t *testing.T) { - threshold := 3 - cfg := TestNodePoolConfig{NodePollFailureThreshold: uint32(threshold), NodePollInterval: testutils.TestInterval} - var calls atomic.Int32 - n := newTestNodeWithCallback(t, cfg, time.Second*0, func(method string, params gjson.Result) (resp testutils.JSONRPCResponse) { - switch method { - case "eth_subscribe": - resp.Result = `"0x00"` - resp.Notify = HeadResult - return - case "eth_unsubscribe": - resp.Result = "true" - return - case "web3_clientVersion": - defer calls.Add(1) - resp.Error.Message = "this will error" - return - default: - t.Errorf("unexpected RPC method: %s", method) - } - return - }) - n.nLiveNodes = func() (int, int64, *big.Int) { return 1, 0, nil } - dial(t, n) - defer func() { assert.NoError(t, n.Close()) }() - - n.wg.Add(1) - go n.aliveLoop() - - testutils.AssertEventually(t, func() bool { - // Need to wait for one complete cycle before checking state so add - // 1 to threshold - return int(calls.Load()) > threshold+1 - }) - - assert.Equal(t, NodeStateAlive, n.State()) - }) - - t.Run("if initial subscribe fails, transitions to unreachable", func(t *testing.T) { - pollDisabledCfg := TestNodePoolConfig{} - n := newTestNodeWithCallback(t, pollDisabledCfg, testutils.TestInterval, func(string, gjson.Result) (resp testutils.JSONRPCResponse) { return }) - dial(t, n) - defer func() { assert.NoError(t, n.Close()) }() - - _, err := n.EthSubscribe(testutils.Context(t), make(chan *evmtypes.Head)) - assert.Error(t, err) - - n.wg.Add(1) - n.aliveLoop() - - assert.Equal(t, NodeStateUnreachable, n.State()) - // sc-39341: ensure failed EthSubscribe didn't register a (*rpc.ClientSubscription)(nil) which would lead to a panic on Unsubscribe - assert.Len(t, n.subs, 0) - }) - - t.Run("if remote RPC connection is closed transitions to unreachable", func(t *testing.T) { - // NoNewHeadsThreshold needs to be positive but must be very large so - // we don't time out waiting for a new head before we have a chance to - // handle the server disconnect - cfg := TestNodePoolConfig{NodePollInterval: 1 * time.Second} - chSubbed := make(chan struct{}, 1) - chPolled := make(chan struct{}) - s := testutils.NewWSServer(t, testutils.FixtureChainID, - func(method string, params gjson.Result) (resp testutils.JSONRPCResponse) { - switch method { - case "eth_subscribe": - chSubbed <- struct{}{} - resp.Result = `"0x00"` - resp.Notify = makeHeadResult(0) - return - case "web3_clientVersion": - select { - case chPolled <- struct{}{}: - default: - } - resp.Result = `"test client version 2"` - return - default: - t.Errorf("unexpected RPC method: %s", method) - } - return - }) - - iN := NewNode(cfg, testutils.WaitTimeout(t), logger.Test(t), *s.WSURL(), nil, "test node", 42, testutils.FixtureChainID, 1) - n := iN.(*node) - - dial(t, n) - defer func() { assert.NoError(t, n.Close()) }() - - n.wg.Add(1) - go n.aliveLoop() - - testutils.WaitWithTimeout(t, chSubbed, "timed out waiting for initial subscription") - testutils.WaitWithTimeout(t, chPolled, "timed out waiting for initial poll") - - assert.Equal(t, NodeStateAlive, n.State()) - - // Simulate remote websocket disconnect - // This causes sub.Err() to close - s.Close() - - testutils.AssertEventually(t, func() bool { - return n.State() == NodeStateUnreachable - }) - }) - - t.Run("when no new heads received for threshold, transitions to out of sync", func(t *testing.T) { - cfg := TestNodePoolConfig{} - chSubbed := make(chan struct{}, 2) - s := testutils.NewWSServer(t, testutils.FixtureChainID, - func(method string, params gjson.Result) (resp testutils.JSONRPCResponse) { - switch method { - case "eth_subscribe": - chSubbed <- struct{}{} - resp.Result = `"0x00"` - resp.Notify = makeHeadResult(0) - return - case "eth_unsubscribe": - resp.Result = "true" - return - case "web3_clientVersion": - resp.Result = `"test client version 2"` - return - default: - t.Errorf("unexpected RPC method: %s", method) - } - return - }) - - iN := NewNode(cfg, 1*time.Second, logger.Test(t), *s.WSURL(), nil, "test node", 42, testutils.FixtureChainID, 1) - n := iN.(*node) - - dial(t, n) - defer func() { assert.NoError(t, n.Close()) }() - - n.wg.Add(1) - go n.aliveLoop() - - testutils.WaitWithTimeout(t, chSubbed, "timed out waiting for initial subscription for InSync") - - testutils.AssertEventually(t, func() bool { - return n.State() == NodeStateOutOfSync - }) - - // Otherwise, there may be data race on dial() vs Close() (accessing ws.rpc) - testutils.WaitWithTimeout(t, chSubbed, "timed out waiting for initial subscription for OutOfSync") - }) - - t.Run("when no new heads received for threshold but we are the last live node, forcibly stays alive", func(t *testing.T) { - lggr, observedLogs := logger.TestObserved(t, zap.ErrorLevel) - pollDisabledCfg := TestNodePoolConfig{} - s := testutils.NewWSServer(t, testutils.FixtureChainID, - func(method string, params gjson.Result) (resp testutils.JSONRPCResponse) { - switch method { - case "eth_subscribe": - resp.Result = `"0x00"` - resp.Notify = makeHeadResult(0) - return - case "eth_unsubscribe": - resp.Result = "true" - return - default: - t.Errorf("unexpected RPC method: %s", method) - } - return - }) - - iN := NewNode(pollDisabledCfg, testutils.TestInterval, lggr, *s.WSURL(), nil, "test node", 42, testutils.FixtureChainID, 1) - n := iN.(*node) - n.nLiveNodes = func() (int, int64, *big.Int) { return 1, 0, nil } - dial(t, n) - defer func() { assert.NoError(t, n.Close()) }() - - n.wg.Add(1) - go n.aliveLoop() - - // to avoid timing-dependent tests, simply wait for the log message instead - // wait for the log twice to be sure we have fully completed the code path and gone around the loop - testutils.WaitForLogMessageCount(t, observedLogs, msgCannotDisable, 2) - - assert.Equal(t, NodeStateAlive, n.State()) - }) - - t.Run("when behind more than SyncThreshold, transitions to out of sync", func(t *testing.T) { - cfg := TestNodePoolConfig{NodeSyncThreshold: 10, NodePollFailureThreshold: 2, NodePollInterval: 100 * time.Millisecond, NodeSelectionMode: NodeSelectionMode_HighestHead} - chSubbed := make(chan struct{}, 2) - var highestHead atomic.Int64 - const stall = 10 - s := testutils.NewWSServer(t, testutils.FixtureChainID, - func(method string, params gjson.Result) (resp testutils.JSONRPCResponse) { - switch method { - case "eth_subscribe": - chSubbed <- struct{}{} - resp.Result = `"0x00"` - resp.Notify = makeHeadResult(int(highestHead.Load())) - return - case "eth_unsubscribe": - resp.Result = "true" - return - case "web3_clientVersion": - resp.Result = `"test client version 2"` - // always tick each poll, but only signal back up to stall - if n := highestHead.Add(1); n <= stall { - resp.Notify = makeHeadResult(int(n)) - } - return - default: - t.Errorf("unexpected RPC method: %s", method) - } - return - }) - - iN := NewNode(cfg, 0*time.Second, logger.Test(t), *s.WSURL(), nil, "test node", 42, testutils.FixtureChainID, 1) - n := iN.(*node) - n.nLiveNodes = func() (count int, blockNumber int64, totalDifficulty *big.Int) { - return 2, highestHead.Load(), nil - } - - dial(t, n) - defer func() { assert.NoError(t, n.Close()) }() - - n.wg.Add(1) - go n.aliveLoop() - - testutils.WaitWithTimeout(t, chSubbed, "timed out waiting for initial subscription for InSync") - - // ensure alive up to stall - testutils.AssertEventually(t, func() bool { - state, num, _ := n.StateAndLatest() - if num < stall { - require.Equal(t, NodeStateAlive, state) - } - return num == stall - }) - - testutils.AssertEventually(t, func() bool { - state, num, _ := n.StateAndLatest() - return state == NodeStateOutOfSync && num == stall - }) - assert.GreaterOrEqual(t, highestHead.Load(), int64(stall+cfg.SyncThreshold())) - - // Otherwise, there may be data race on dial() vs Close() (accessing ws.rpc) - testutils.WaitWithTimeout(t, chSubbed, "timed out waiting for initial subscription for OutOfSync") - }) - - t.Run("when behind but SyncThreshold=0, stay alive", func(t *testing.T) { - cfg := TestNodePoolConfig{NodeSyncThreshold: 0, NodePollFailureThreshold: 2, NodePollInterval: 100 * time.Millisecond, NodeSelectionMode: NodeSelectionMode_HighestHead} - chSubbed := make(chan struct{}, 1) - var highestHead atomic.Int64 - const stall = 10 - s := testutils.NewWSServer(t, testutils.FixtureChainID, - func(method string, params gjson.Result) (resp testutils.JSONRPCResponse) { - switch method { - case "eth_subscribe": - chSubbed <- struct{}{} - resp.Result = `"0x00"` - resp.Notify = makeHeadResult(int(highestHead.Load())) - return - case "eth_unsubscribe": - resp.Result = "true" - return - case "web3_clientVersion": - resp.Result = `"test client version 2"` - // always tick each poll, but only signal back up to stall - if n := highestHead.Add(1); n <= stall { - resp.Notify = makeHeadResult(int(n)) - } - return - default: - t.Errorf("unexpected RPC method: %s", method) - } - return - }) - - iN := NewNode(cfg, 0*time.Second, logger.Test(t), *s.WSURL(), nil, "test node", 42, testutils.FixtureChainID, 1) - n := iN.(*node) - n.nLiveNodes = func() (count int, blockNumber int64, totalDifficulty *big.Int) { - return 2, highestHead.Load(), nil - } - - dial(t, n) - defer func() { assert.NoError(t, n.Close()) }() - - n.wg.Add(1) - go n.aliveLoop() - - testutils.WaitWithTimeout(t, chSubbed, "timed out waiting for initial subscription for InSync") - - // ensure alive up to stall - testutils.AssertEventually(t, func() bool { - state, num, _ := n.StateAndLatest() - require.Equal(t, NodeStateAlive, state) - return num == stall - }) - - assert.Equal(t, NodeStateAlive, n.state) - assert.GreaterOrEqual(t, highestHead.Load(), int64(stall+cfg.SyncThreshold())) - }) - - t.Run("when behind more than SyncThreshold but we are the last live node, forcibly stays alive", func(t *testing.T) { - lggr, observedLogs := logger.TestObserved(t, zap.ErrorLevel) - cfg := TestNodePoolConfig{NodeSyncThreshold: 5, NodePollFailureThreshold: 2, NodePollInterval: 100 * time.Millisecond, NodeSelectionMode: NodeSelectionMode_HighestHead} - chSubbed := make(chan struct{}, 1) - var highestHead atomic.Int64 - const stall = 10 - s := testutils.NewWSServer(t, testutils.FixtureChainID, - func(method string, params gjson.Result) (resp testutils.JSONRPCResponse) { - switch method { - case "eth_subscribe": - chSubbed <- struct{}{} - resp.Result = `"0x00"` - n := highestHead.Load() - if n > stall { - n = stall - } - resp.Notify = makeHeadResult(int(n)) - return - case "eth_unsubscribe": - resp.Result = "true" - return - case "web3_clientVersion": - resp.Result = `"test client version 2"` - // always tick each poll, but only signal back up to stall - if n := highestHead.Add(1); n <= stall { - resp.Notify = makeHeadResult(int(n)) - } - return - default: - t.Errorf("unexpected RPC method: %s", method) - } - return - }) - - iN := NewNode(cfg, 0*time.Second, lggr, *s.WSURL(), nil, "test node", 42, testutils.FixtureChainID, 1) - n := iN.(*node) - n.nLiveNodes = func() (count int, blockNumber int64, totalDifficulty *big.Int) { - return 1, highestHead.Load(), nil - } - - dial(t, n) - defer func() { assert.NoError(t, n.Close()) }() - - n.wg.Add(1) - go n.aliveLoop() - - testutils.WaitWithTimeout(t, chSubbed, "timed out waiting for initial subscription for InSync") - - // ensure alive up to stall - testutils.AssertEventually(t, func() bool { - state, num, _ := n.StateAndLatest() - require.Equal(t, NodeStateAlive, state) - return num == stall - }) - - assert.Equal(t, NodeStateAlive, n.state) - testutils.AssertEventually(t, func() bool { - return highestHead.Load() >= int64(stall+cfg.SyncThreshold()) - }) - - testutils.WaitForLogMessageCount(t, observedLogs, msgCannotDisable, 1) - - state, num, _ := n.StateAndLatest() - assert.Equal(t, NodeStateAlive, state) - assert.Equal(t, int64(stall), num) - }) -} - -func TestUnit_NodeLifecycle_outOfSyncLoop(t *testing.T) { - t.Parallel() - - t.Run("exits on close", func(t *testing.T) { - cfg := TestNodePoolConfig{} - n := newTestNode(t, cfg, time.Second*0) - dial(t, n) - n.setState(NodeStateOutOfSync) - - ch := make(chan struct{}) - - n.wg.Add(1) - go func() { - defer close(ch) - n.outOfSyncLoop(func(num int64, td *big.Int) bool { return false }) - }() - assert.NoError(t, n.Close()) - testutils.WaitWithTimeout(t, ch, "expected outOfSyncLoop to exit") - }) - - t.Run("if initial subscribe fails, transitions to unreachable", func(t *testing.T) { - cfg := TestNodePoolConfig{} - n := newTestNodeWithCallback(t, cfg, time.Second*0, func(string, gjson.Result) (resp testutils.JSONRPCResponse) { return }) - dial(t, n) - n.setState(NodeStateOutOfSync) - defer func() { assert.NoError(t, n.Close()) }() - - n.wg.Add(1) - - n.outOfSyncLoop(func(num int64, td *big.Int) bool { return num == 0 }) - assert.Equal(t, NodeStateUnreachable, n.State()) - }) - - t.Run("transitions to unreachable if remote RPC subscription channel closed", func(t *testing.T) { - cfg := TestNodePoolConfig{} - chSubbed := make(chan struct{}, 1) - s := testutils.NewWSServer(t, testutils.FixtureChainID, - func(method string, params gjson.Result) (resp testutils.JSONRPCResponse) { - switch method { - case "eth_subscribe": - chSubbed <- struct{}{} - resp.Result = `"0x00"` - resp.Notify = makeHeadResult(0) - return - default: - t.Errorf("unexpected RPC method: %s", method) - } - return - }) - - iN := NewNode(cfg, time.Second, logger.Test(t), *s.WSURL(), nil, "test node", 42, testutils.FixtureChainID, 1) - n := iN.(*node) - - dial(t, n) - n.setState(NodeStateOutOfSync) - defer func() { assert.NoError(t, n.Close()) }() - - n.wg.Add(1) - go n.outOfSyncLoop(func(num int64, td *big.Int) bool { return num == 0 }) - - testutils.WaitWithTimeout(t, chSubbed, "timed out waiting for initial subscription") - - assert.Equal(t, NodeStateOutOfSync, n.State()) - - // Simulate remote websocket disconnect - // This causes sub.Err() to close - s.Close() - - testutils.AssertEventually(t, func() bool { - return n.State() == NodeStateUnreachable - }) - }) - - t.Run("transitions to alive if it receives a newer head", func(t *testing.T) { - // NoNewHeadsThreshold needs to be positive but must be very large so - // we don't time out waiting for a new head before we have a chance to - // handle the server disconnect - lggr, observedLogs := logger.TestObserved(t, zap.DebugLevel) - cfg := TestNodePoolConfig{} - chSubbed := make(chan struct{}, 1) - s := testutils.NewWSServer(t, testutils.FixtureChainID, - func(method string, params gjson.Result) (resp testutils.JSONRPCResponse) { - switch method { - case "eth_subscribe": - chSubbed <- struct{}{} - resp.Result = `"0x00"` - resp.Notify = makeNewHeadWSMessage(42) - return - case "eth_unsubscribe": - resp.Result = "true" - return - default: - t.Errorf("unexpected RPC method: %s", method) - } - return - }) - - iN := NewNode(cfg, time.Second*0, lggr, *s.WSURL(), nil, "test node", 0, testutils.FixtureChainID, 1) - n := iN.(*node) - - start(t, n) - n.setState(NodeStateOutOfSync) - defer func() { assert.NoError(t, n.Close()) }() - - n.wg.Add(1) - go n.outOfSyncLoop(func(num int64, td *big.Int) bool { return num < 43 }) - - testutils.WaitWithTimeout(t, chSubbed, "timed out waiting for initial subscription") - - assert.Equal(t, NodeStateOutOfSync, n.State()) - - // heads less than latest seen head are ignored; they do not make the node live - for i := 0; i < 43; i++ { - msg := makeNewHeadWSMessage(i) - s.MustWriteBinaryMessageSync(t, msg) - testutils.WaitForLogMessageCount(t, observedLogs, msgReceivedBlock, i+1) - assert.Equal(t, NodeStateOutOfSync, n.State()) - } - - msg := makeNewHeadWSMessage(43) - s.MustWriteBinaryMessageSync(t, msg) - - testutils.AssertEventually(t, func() bool { - s, n, td := n.StateAndLatest() - return s == NodeStateAlive && n != -1 && td != nil - }) - - testutils.WaitForLogMessage(t, observedLogs, msgInSync) - }) - - t.Run("transitions to alive if back in-sync", func(t *testing.T) { - lggr, observedLogs := logger.TestObserved(t, zap.DebugLevel) - cfg := TestNodePoolConfig{NodeSyncThreshold: 5, NodeSelectionMode: NodeSelectionMode_HighestHead} - chSubbed := make(chan struct{}, 1) - const stall = 42 - s := testutils.NewWSServer(t, testutils.FixtureChainID, - func(method string, params gjson.Result) (resp testutils.JSONRPCResponse) { - switch method { - case "eth_subscribe": - chSubbed <- struct{}{} - resp.Result = `"0x00"` - resp.Notify = makeNewHeadWSMessage(stall) - return - case "eth_unsubscribe": - resp.Result = "true" - return - default: - t.Errorf("unexpected RPC method: %s", method) - } - return - }) - - iN := NewNode(cfg, time.Second*0, lggr, *s.WSURL(), nil, "test node", 0, testutils.FixtureChainID, 1) - n := iN.(*node) - n.nLiveNodes = func() (count int, blockNumber int64, totalDifficulty *big.Int) { - return 2, stall + int64(cfg.SyncThreshold()), nil - } - - start(t, n) - n.setState(NodeStateOutOfSync) - defer func() { assert.NoError(t, n.Close()) }() - - n.wg.Add(1) - go n.outOfSyncLoop(n.isOutOfSync) - - testutils.WaitWithTimeout(t, chSubbed, "timed out waiting for initial subscription") - - assert.Equal(t, NodeStateOutOfSync, n.State()) - - // heads less than stall (latest seen head - SyncThreshold) are ignored; they do not make the node live - for i := 0; i < stall; i++ { - msg := makeNewHeadWSMessage(i) - s.MustWriteBinaryMessageSync(t, msg) - testutils.WaitForLogMessageCount(t, observedLogs, msgReceivedBlock, i+1) - assert.Equal(t, NodeStateOutOfSync, n.State()) - } - - msg := makeNewHeadWSMessage(stall) - s.MustWriteBinaryMessageSync(t, msg) - - testutils.AssertEventually(t, func() bool { - s, n, td := n.StateAndLatest() - return s == NodeStateAlive && n != -1 && td != nil - }) - - testutils.WaitForLogMessage(t, observedLogs, msgInSync) - }) - - t.Run("if no live nodes are available, forcibly marks this one alive again", func(t *testing.T) { - cfg := TestNodePoolConfig{} - chSubbed := make(chan struct{}, 1) - s := testutils.NewWSServer(t, testutils.FixtureChainID, - func(method string, params gjson.Result) (resp testutils.JSONRPCResponse) { - switch method { - case "eth_subscribe": - chSubbed <- struct{}{} - resp.Result = `"0x00"` - resp.Notify = makeHeadResult(0) - return - case "eth_unsubscribe": - resp.Result = "true" - return - default: - t.Errorf("unexpected RPC method: %s", method) - } - return - }) - - iN := NewNode(cfg, testutils.TestInterval, logger.Test(t), *s.WSURL(), nil, "test node", 42, testutils.FixtureChainID, 1) - n := iN.(*node) - n.nLiveNodes = func() (int, int64, *big.Int) { return 0, 0, nil } - - dial(t, n) - n.setState(NodeStateOutOfSync) - defer func() { assert.NoError(t, n.Close()) }() - - n.wg.Add(1) - go n.outOfSyncLoop(func(num int64, td *big.Int) bool { return num == 0 }) - - testutils.WaitWithTimeout(t, chSubbed, "timed out waiting for initial subscription") - - testutils.AssertEventually(t, func() bool { - return n.State() == NodeStateAlive - }) - }) -} - -func TestUnit_NodeLifecycle_unreachableLoop(t *testing.T) { - t.Parallel() - - t.Run("exits on close", func(t *testing.T) { - cfg := TestNodePoolConfig{} - n := newTestNode(t, cfg, time.Second*0) - start(t, n) - n.setState(NodeStateUnreachable) - - ch := make(chan struct{}) - n.wg.Add(1) - go func() { - n.unreachableLoop() - close(ch) - }() - assert.NoError(t, n.Close()) - testutils.WaitWithTimeout(t, ch, "expected unreachableLoop to exit") - }) - - t.Run("on successful redial and verify, transitions to alive", func(t *testing.T) { - cfg := TestNodePoolConfig{} - n := newTestNode(t, cfg, time.Second*0) - start(t, n) - defer func() { assert.NoError(t, n.Close()) }() - n.setState(NodeStateUnreachable) - n.wg.Add(1) - - go n.unreachableLoop() - - testutils.AssertEventually(t, func() bool { - return n.State() == NodeStateAlive - }) - }) - - t.Run("on successful redial but failed verify, transitions to invalid chain ID", func(t *testing.T) { - cfg := TestNodePoolConfig{} - s := testutils.NewWSServer(t, testutils.FixtureChainID, standardHandler) - lggr, observedLogs := logger.TestObserved(t, zap.ErrorLevel) - iN := NewNode(cfg, time.Second*0, lggr, *s.WSURL(), nil, "test node", 0, big.NewInt(42), 1) - n := iN.(*node) - defer func() { assert.NoError(t, n.Close()) }() - start(t, n) - n.setState(NodeStateUnreachable) - n.wg.Add(1) - - go n.unreachableLoop() - - testutils.WaitForLogMessage(t, observedLogs, "Failed to redial RPC node; remote endpoint returned the wrong chain ID") - - testutils.AssertEventually(t, func() bool { - return n.State() == NodeStateInvalidChainID - }) - }) - - t.Run("on failed redial, keeps trying to redial", func(t *testing.T) { - cfg := TestNodePoolConfig{} - lggr, observedLogs := logger.TestObserved(t, zap.DebugLevel) - iN := NewNode(cfg, time.Second*0, lggr, *testutils.MustParseURL(t, "ws://test.invalid"), nil, "test node", 0, big.NewInt(42), 1) - n := iN.(*node) - defer func() { assert.NoError(t, n.Close()) }() - start(t, n) - n.setState(NodeStateUnreachable) - n.wg.Add(1) - - go n.unreachableLoop() - - testutils.WaitForLogMessageCount(t, observedLogs, "Failed to redial RPC node", 3) - - assert.Equal(t, NodeStateUnreachable, n.State()) - }) -} -func TestUnit_NodeLifecycle_invalidChainIDLoop(t *testing.T) { - t.Parallel() - - t.Run("exits on close", func(t *testing.T) { - cfg := TestNodePoolConfig{} - n := newTestNode(t, cfg, time.Second*0) - start(t, n) - n.setState(NodeStateInvalidChainID) - - ch := make(chan struct{}) - n.wg.Add(1) - go func() { - n.invalidChainIDLoop() - close(ch) - }() - assert.NoError(t, n.Close()) - testutils.WaitWithTimeout(t, ch, "expected invalidChainIDLoop to exit") - }) - - t.Run("on successful verify, transitions to alive", func(t *testing.T) { - cfg := TestNodePoolConfig{} - n := newTestNode(t, cfg, time.Second*0) - dial(t, n) - defer func() { assert.NoError(t, n.Close()) }() - n.setState(NodeStateInvalidChainID) - n.wg.Add(1) - - go n.invalidChainIDLoop() - - testutils.AssertEventually(t, func() bool { - return n.State() == NodeStateAlive - }) - }) - - t.Run("on failed verify, keeps checking", func(t *testing.T) { - cfg := TestNodePoolConfig{} - s := testutils.NewWSServer(t, testutils.FixtureChainID, standardHandler) - lggr, observedLogs := logger.TestObserved(t, zap.ErrorLevel) - iN := NewNode(cfg, time.Second*0, lggr, *s.WSURL(), nil, "test node", 0, big.NewInt(42), 1) - n := iN.(*node) - defer func() { assert.NoError(t, n.Close()) }() - dial(t, n) - n.setState(NodeStateUnreachable) - n.wg.Add(1) - - go n.unreachableLoop() - - testutils.WaitForLogMessageCount(t, observedLogs, "Failed to redial RPC node; remote endpoint returned the wrong chain ID", 3) - - assert.Equal(t, NodeStateInvalidChainID, n.State()) - }) -} diff --git a/core/chains/evm/client/node_selector_highest_head.go b/core/chains/evm/client/node_selector_highest_head.go deleted file mode 100644 index 2ed41486cff..00000000000 --- a/core/chains/evm/client/node_selector_highest_head.go +++ /dev/null @@ -1,32 +0,0 @@ -package client - -import ( - "math" -) - -type highestHeadNodeSelector []Node - -// Deprecated: use [pkg/github.com/smartcontractkit/chainlink/v2/common/client.NewHighestHeadNodeSelector] -func NewHighestHeadNodeSelector(nodes []Node) NodeSelector { - return highestHeadNodeSelector(nodes) -} - -func (s highestHeadNodeSelector) Select() Node { - var highestHeadNumber int64 = math.MinInt64 - var highestHeadNodes []Node - for _, n := range s { - state, currentHeadNumber, _ := n.StateAndLatest() - if state == NodeStateAlive && currentHeadNumber >= highestHeadNumber { - if highestHeadNumber < currentHeadNumber { - highestHeadNumber = currentHeadNumber - highestHeadNodes = nil - } - highestHeadNodes = append(highestHeadNodes, n) - } - } - return firstOrHighestPriority(highestHeadNodes) -} - -func (s highestHeadNodeSelector) Name() string { - return NodeSelectionMode_HighestHead -} diff --git a/core/chains/evm/client/node_selector_highest_head_test.go b/core/chains/evm/client/node_selector_highest_head_test.go deleted file mode 100644 index 29b39b7fe73..00000000000 --- a/core/chains/evm/client/node_selector_highest_head_test.go +++ /dev/null @@ -1,175 +0,0 @@ -package client_test - -import ( - "testing" - - evmclient "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client" - evmmocks "github.com/smartcontractkit/chainlink/v2/core/chains/evm/mocks" - - "github.com/stretchr/testify/assert" -) - -func TestHighestHeadNodeSelectorName(t *testing.T) { - selector := evmclient.NewHighestHeadNodeSelector(nil) - assert.Equal(t, selector.Name(), evmclient.NodeSelectionMode_HighestHead) -} - -func TestHighestHeadNodeSelector(t *testing.T) { - t.Parallel() - - var nodes []evmclient.Node - - for i := 0; i < 3; i++ { - node := evmmocks.NewNode(t) - if i == 0 { - // first node is out of sync - node.On("StateAndLatest").Return(evmclient.NodeStateOutOfSync, int64(-1), nil) - } else if i == 1 { - // second node is alive, LatestReceivedBlockNumber = 1 - node.On("StateAndLatest").Return(evmclient.NodeStateAlive, int64(1), nil) - } else { - // third node is alive, LatestReceivedBlockNumber = 2 (best node) - node.On("StateAndLatest").Return(evmclient.NodeStateAlive, int64(2), nil) - } - node.On("Order").Maybe().Return(int32(1)) - nodes = append(nodes, node) - } - - selector := evmclient.NewHighestHeadNodeSelector(nodes) - assert.Same(t, nodes[2], selector.Select()) - - t.Run("stick to the same node", func(t *testing.T) { - node := evmmocks.NewNode(t) - // fourth node is alive, LatestReceivedBlockNumber = 2 (same as 3rd) - node.On("StateAndLatest").Return(evmclient.NodeStateAlive, int64(2), nil) - node.On("Order").Return(int32(1)) - nodes = append(nodes, node) - - selector := evmclient.NewHighestHeadNodeSelector(nodes) - assert.Same(t, nodes[2], selector.Select()) - }) - - t.Run("another best node", func(t *testing.T) { - node := evmmocks.NewNode(t) - // fifth node is alive, LatestReceivedBlockNumber = 3 (better than 3rd and 4th) - node.On("StateAndLatest").Return(evmclient.NodeStateAlive, int64(3), nil) - node.On("Order").Return(int32(1)) - nodes = append(nodes, node) - - selector := evmclient.NewHighestHeadNodeSelector(nodes) - assert.Same(t, nodes[4], selector.Select()) - }) - - t.Run("nodes never update latest block number", func(t *testing.T) { - node1 := evmmocks.NewNode(t) - node1.On("StateAndLatest").Return(evmclient.NodeStateAlive, int64(-1), nil) - node1.On("Order").Return(int32(1)) - node2 := evmmocks.NewNode(t) - node2.On("StateAndLatest").Return(evmclient.NodeStateAlive, int64(-1), nil) - node2.On("Order").Return(int32(1)) - nodes := []evmclient.Node{node1, node2} - - selector := evmclient.NewHighestHeadNodeSelector(nodes) - assert.Same(t, node1, selector.Select()) - }) -} - -func TestHighestHeadNodeSelector_None(t *testing.T) { - t.Parallel() - - var nodes []evmclient.Node - - for i := 0; i < 3; i++ { - node := evmmocks.NewNode(t) - if i == 0 { - // first node is out of sync - node.On("StateAndLatest").Return(evmclient.NodeStateOutOfSync, int64(-1), nil) - } else { - // others are unreachable - node.On("StateAndLatest").Return(evmclient.NodeStateUnreachable, int64(1), nil) - } - nodes = append(nodes, node) - } - - selector := evmclient.NewHighestHeadNodeSelector(nodes) - assert.Nil(t, selector.Select()) -} - -func TestHighestHeadNodeSelectorWithOrder(t *testing.T) { - t.Parallel() - - var nodes []evmclient.Node - - t.Run("same head and order", func(t *testing.T) { - for i := 0; i < 3; i++ { - node := evmmocks.NewNode(t) - node.On("StateAndLatest").Return(evmclient.NodeStateAlive, int64(1), nil) - node.On("Order").Return(int32(2)) - nodes = append(nodes, node) - } - selector := evmclient.NewHighestHeadNodeSelector(nodes) - //Should select the first node because all things are equal - assert.Same(t, nodes[0], selector.Select()) - }) - - t.Run("same head but different order", func(t *testing.T) { - node1 := evmmocks.NewNode(t) - node1.On("StateAndLatest").Return(evmclient.NodeStateAlive, int64(3), nil) - node1.On("Order").Return(int32(3)) - - node2 := evmmocks.NewNode(t) - node2.On("StateAndLatest").Return(evmclient.NodeStateAlive, int64(3), nil) - node2.On("Order").Return(int32(1)) - - node3 := evmmocks.NewNode(t) - node3.On("StateAndLatest").Return(evmclient.NodeStateAlive, int64(3), nil) - node3.On("Order").Return(int32(2)) - - nodes := []evmclient.Node{node1, node2, node3} - selector := evmclient.NewHighestHeadNodeSelector(nodes) - //Should select the second node as it has the highest priority - assert.Same(t, nodes[1], selector.Select()) - }) - - t.Run("different head but same order", func(t *testing.T) { - node1 := evmmocks.NewNode(t) - node1.On("StateAndLatest").Return(evmclient.NodeStateAlive, int64(1), nil) - node1.On("Order").Maybe().Return(int32(3)) - - node2 := evmmocks.NewNode(t) - node2.On("StateAndLatest").Return(evmclient.NodeStateAlive, int64(2), nil) - node2.On("Order").Maybe().Return(int32(3)) - - node3 := evmmocks.NewNode(t) - node3.On("StateAndLatest").Return(evmclient.NodeStateAlive, int64(3), nil) - node3.On("Order").Return(int32(3)) - - nodes := []evmclient.Node{node1, node2, node3} - selector := evmclient.NewHighestHeadNodeSelector(nodes) - //Should select the third node as it has the highest head - assert.Same(t, nodes[2], selector.Select()) - }) - - t.Run("different head and different order", func(t *testing.T) { - node1 := evmmocks.NewNode(t) - node1.On("StateAndLatest").Return(evmclient.NodeStateAlive, int64(10), nil) - node1.On("Order").Maybe().Return(int32(3)) - - node2 := evmmocks.NewNode(t) - node2.On("StateAndLatest").Return(evmclient.NodeStateAlive, int64(11), nil) - node2.On("Order").Maybe().Return(int32(4)) - - node3 := evmmocks.NewNode(t) - node3.On("StateAndLatest").Return(evmclient.NodeStateAlive, int64(11), nil) - node3.On("Order").Maybe().Return(int32(3)) - - node4 := evmmocks.NewNode(t) - node4.On("StateAndLatest").Return(evmclient.NodeStateAlive, int64(10), nil) - node4.On("Order").Maybe().Return(int32(1)) - - nodes := []evmclient.Node{node1, node2, node3, node4} - selector := evmclient.NewHighestHeadNodeSelector(nodes) - //Should select the third node as it has the highest head and will win the priority tie-breaker - assert.Same(t, nodes[2], selector.Select()) - }) -} diff --git a/core/chains/evm/client/node_selector_priority_level.go b/core/chains/evm/client/node_selector_priority_level.go deleted file mode 100644 index fba6d403327..00000000000 --- a/core/chains/evm/client/node_selector_priority_level.go +++ /dev/null @@ -1,104 +0,0 @@ -package client - -import ( - "math" - "sort" - "sync/atomic" -) - -type priorityLevelNodeSelector struct { - nodes []Node - roundRobinCount []atomic.Uint32 -} - -type nodeWithPriority struct { - node Node - priority int32 -} - -// Deprecated: use [pkg/github.com/smartcontractkit/chainlink/v2/common/client.NewPriorityLevelNodeSelector] -func NewPriorityLevelNodeSelector(nodes []Node) NodeSelector { - return &priorityLevelNodeSelector{ - nodes: nodes, - roundRobinCount: make([]atomic.Uint32, nrOfPriorityTiers(nodes)), - } -} - -func (s priorityLevelNodeSelector) Select() Node { - nodes := s.getHighestPriorityAliveTier() - - if len(nodes) == 0 { - return nil - } - priorityLevel := nodes[len(nodes)-1].priority - - // NOTE: Inc returns the number after addition, so we must -1 to get the "current" counter - count := s.roundRobinCount[priorityLevel].Add(1) - 1 - idx := int(count % uint32(len(nodes))) - - return nodes[idx].node -} - -func (s priorityLevelNodeSelector) Name() string { - return NodeSelectionMode_PriorityLevel -} - -// getHighestPriorityAliveTier filters nodes that are not in state NodeStateAlive and -// returns only the highest tier of alive nodes -func (s priorityLevelNodeSelector) getHighestPriorityAliveTier() []nodeWithPriority { - var nodes []nodeWithPriority - for _, n := range s.nodes { - if n.State() == NodeStateAlive { - nodes = append(nodes, nodeWithPriority{n, n.Order()}) - } - } - - if len(nodes) == 0 { - return nil - } - - return removeLowerTiers(nodes) -} - -// removeLowerTiers take a slice of nodeWithPriority and keeps only the highest tier -func removeLowerTiers(nodes []nodeWithPriority) []nodeWithPriority { - sort.SliceStable(nodes, func(i, j int) bool { - return nodes[i].priority > nodes[j].priority - }) - - var nodes2 []nodeWithPriority - currentPriority := nodes[len(nodes)-1].priority - - for _, n := range nodes { - if n.priority == currentPriority { - nodes2 = append(nodes2, n) - } - } - - return nodes2 -} - -// nrOfPriorityTiers calculates the total number of priority tiers -func nrOfPriorityTiers(nodes []Node) int32 { - highestPriority := int32(0) - for _, n := range nodes { - priority := n.Order() - if highestPriority < priority { - highestPriority = priority - } - } - return highestPriority + 1 -} - -// firstOrHighestPriority takes a list of nodes and returns the first one with the highest priority -func firstOrHighestPriority(nodes []Node) Node { - hp := int32(math.MaxInt32) - var node Node - for _, n := range nodes { - if n.Order() < hp { - hp = n.Order() - node = n - } - } - return node -} diff --git a/core/chains/evm/client/node_selector_priority_level_test.go b/core/chains/evm/client/node_selector_priority_level_test.go deleted file mode 100644 index a7c68c3f282..00000000000 --- a/core/chains/evm/client/node_selector_priority_level_test.go +++ /dev/null @@ -1,86 +0,0 @@ -package client_test - -import ( - "testing" - - evmclient "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client" - evmmocks "github.com/smartcontractkit/chainlink/v2/core/chains/evm/mocks" - - "github.com/stretchr/testify/assert" -) - -func TestPriorityLevelNodeSelectorName(t *testing.T) { - selector := evmclient.NewPriorityLevelNodeSelector(nil) - assert.Equal(t, selector.Name(), evmclient.NodeSelectionMode_PriorityLevel) -} - -func TestPriorityLevelNodeSelector(t *testing.T) { - t.Parallel() - - var nodes []evmclient.Node - n1 := evmmocks.NewNode(t) - n1.On("State").Return(evmclient.NodeStateAlive) - n1.On("Order").Return(int32(1)) - - n2 := evmmocks.NewNode(t) - n2.On("State").Return(evmclient.NodeStateAlive) - n2.On("Order").Return(int32(1)) - - n3 := evmmocks.NewNode(t) - n3.On("State").Return(evmclient.NodeStateAlive) - n3.On("Order").Return(int32(1)) - - nodes = append(nodes, n1, n2, n3) - selector := evmclient.NewPriorityLevelNodeSelector(nodes) - assert.Same(t, nodes[0], selector.Select()) - assert.Same(t, nodes[1], selector.Select()) - assert.Same(t, nodes[2], selector.Select()) - assert.Same(t, nodes[0], selector.Select()) - assert.Same(t, nodes[1], selector.Select()) - assert.Same(t, nodes[2], selector.Select()) -} - -func TestPriorityLevelNodeSelector_None(t *testing.T) { - t.Parallel() - - var nodes []evmclient.Node - - for i := 0; i < 3; i++ { - node := evmmocks.NewNode(t) - if i == 0 { - // first node is out of sync - node.On("State").Return(evmclient.NodeStateOutOfSync) - node.On("Order").Return(int32(1)) - } else { - // others are unreachable - node.On("State").Return(evmclient.NodeStateUnreachable) - node.On("Order").Return(int32(1)) - } - nodes = append(nodes, node) - } - - selector := evmclient.NewPriorityLevelNodeSelector(nodes) - assert.Nil(t, selector.Select()) -} - -func TestPriorityLevelNodeSelector_DifferentOrder(t *testing.T) { - t.Parallel() - - var nodes []evmclient.Node - n1 := evmmocks.NewNode(t) - n1.On("State").Return(evmclient.NodeStateAlive) - n1.On("Order").Return(int32(1)) - - n2 := evmmocks.NewNode(t) - n2.On("State").Return(evmclient.NodeStateAlive) - n2.On("Order").Return(int32(2)) - - n3 := evmmocks.NewNode(t) - n3.On("State").Return(evmclient.NodeStateAlive) - n3.On("Order").Return(int32(3)) - - nodes = append(nodes, n1, n2, n3) - selector := evmclient.NewPriorityLevelNodeSelector(nodes) - assert.Same(t, nodes[0], selector.Select()) - assert.Same(t, nodes[0], selector.Select()) -} diff --git a/core/chains/evm/client/node_selector_round_robin.go b/core/chains/evm/client/node_selector_round_robin.go deleted file mode 100644 index 3bd19f0ede4..00000000000 --- a/core/chains/evm/client/node_selector_round_robin.go +++ /dev/null @@ -1,39 +0,0 @@ -package client - -import "sync/atomic" - -type roundRobinSelector struct { - nodes []Node - roundRobinCount atomic.Uint32 -} - -// Deprecated: use [pkg/github.com/smartcontractkit/chainlink/v2/common/client.NewRoundRobinSelector] -func NewRoundRobinSelector(nodes []Node) NodeSelector { - return &roundRobinSelector{ - nodes: nodes, - } -} - -func (s *roundRobinSelector) Select() Node { - var liveNodes []Node - for _, n := range s.nodes { - if n.State() == NodeStateAlive { - liveNodes = append(liveNodes, n) - } - } - - nNodes := len(liveNodes) - if nNodes == 0 { - return nil - } - - // NOTE: Inc returns the number after addition, so we must -1 to get the "current" counter - count := s.roundRobinCount.Add(1) - 1 - idx := int(count % uint32(nNodes)) - - return liveNodes[idx] -} - -func (s *roundRobinSelector) Name() string { - return NodeSelectionMode_RoundRobin -} diff --git a/core/chains/evm/client/node_selector_round_robin_test.go b/core/chains/evm/client/node_selector_round_robin_test.go deleted file mode 100644 index 8308d779895..00000000000 --- a/core/chains/evm/client/node_selector_round_robin_test.go +++ /dev/null @@ -1,55 +0,0 @@ -package client_test - -import ( - "testing" - - evmclient "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client" - evmmocks "github.com/smartcontractkit/chainlink/v2/core/chains/evm/mocks" - - "github.com/stretchr/testify/assert" -) - -func TestRoundRobinNodeSelector(t *testing.T) { - t.Parallel() - - var nodes []evmclient.Node - - for i := 0; i < 3; i++ { - node := evmmocks.NewNode(t) - if i == 0 { - // first node is out of sync - node.On("State").Return(evmclient.NodeStateOutOfSync) - } else { - // second & third nodes are alive - node.On("State").Return(evmclient.NodeStateAlive) - } - nodes = append(nodes, node) - } - - selector := evmclient.NewRoundRobinSelector(nodes) - assert.Same(t, nodes[1], selector.Select()) - assert.Same(t, nodes[2], selector.Select()) - assert.Same(t, nodes[1], selector.Select()) - assert.Same(t, nodes[2], selector.Select()) -} - -func TestRoundRobinNodeSelector_None(t *testing.T) { - t.Parallel() - - var nodes []evmclient.Node - - for i := 0; i < 3; i++ { - node := evmmocks.NewNode(t) - if i == 0 { - // first node is out of sync - node.On("State").Return(evmclient.NodeStateOutOfSync) - } else { - // others are unreachable - node.On("State").Return(evmclient.NodeStateUnreachable) - } - nodes = append(nodes, node) - } - - selector := evmclient.NewRoundRobinSelector(nodes) - assert.Nil(t, selector.Select()) -} diff --git a/core/chains/evm/client/node_selector_total_difficulty.go b/core/chains/evm/client/node_selector_total_difficulty.go deleted file mode 100644 index 27d888947d9..00000000000 --- a/core/chains/evm/client/node_selector_total_difficulty.go +++ /dev/null @@ -1,43 +0,0 @@ -package client - -import "math/big" - -type totalDifficultyNodeSelector []Node - -// Deprecated: use [pkg/github.com/smartcontractkit/chainlink/v2/common/client.NewTotalDifficultyNodeSelector] -func NewTotalDifficultyNodeSelector(nodes []Node) NodeSelector { - return totalDifficultyNodeSelector(nodes) -} - -func (s totalDifficultyNodeSelector) Select() Node { - // NodeNoNewHeadsThreshold may not be enabled, in this case all nodes have td == nil - var highestTD *big.Int - var nodes []Node - var aliveNodes []Node - - for _, n := range s { - state, _, currentTD := n.StateAndLatest() - if state != NodeStateAlive { - continue - } - - aliveNodes = append(aliveNodes, n) - if currentTD != nil && (highestTD == nil || currentTD.Cmp(highestTD) >= 0) { - if highestTD == nil || currentTD.Cmp(highestTD) > 0 { - highestTD = currentTD - nodes = nil - } - nodes = append(nodes, n) - } - } - - //If all nodes have td == nil pick one from the nodes that are alive - if len(nodes) == 0 { - return firstOrHighestPriority(aliveNodes) - } - return firstOrHighestPriority(nodes) -} - -func (s totalDifficultyNodeSelector) Name() string { - return NodeSelectionMode_TotalDifficulty -} diff --git a/core/chains/evm/client/node_selector_total_difficulty_test.go b/core/chains/evm/client/node_selector_total_difficulty_test.go deleted file mode 100644 index 486a421477e..00000000000 --- a/core/chains/evm/client/node_selector_total_difficulty_test.go +++ /dev/null @@ -1,176 +0,0 @@ -package client_test - -import ( - "math/big" - "testing" - - evmclient "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client" - evmmocks "github.com/smartcontractkit/chainlink/v2/core/chains/evm/mocks" - - "github.com/stretchr/testify/assert" -) - -func TestTotalDifficultyNodeSelectorName(t *testing.T) { - selector := evmclient.NewTotalDifficultyNodeSelector(nil) - assert.Equal(t, selector.Name(), evmclient.NodeSelectionMode_TotalDifficulty) -} - -func TestTotalDifficultyNodeSelector(t *testing.T) { - t.Parallel() - - var nodes []evmclient.Node - - for i := 0; i < 3; i++ { - node := evmmocks.NewNode(t) - if i == 0 { - // first node is out of sync - node.On("StateAndLatest").Return(evmclient.NodeStateOutOfSync, int64(-1), nil) - } else if i == 1 { - // second node is alive - node.On("StateAndLatest").Return(evmclient.NodeStateAlive, int64(1), big.NewInt(7)) - } else { - // third node is alive and best - node.On("StateAndLatest").Return(evmclient.NodeStateAlive, int64(2), big.NewInt(8)) - } - node.On("Order").Maybe().Return(int32(1)) - nodes = append(nodes, node) - } - - selector := evmclient.NewTotalDifficultyNodeSelector(nodes) - assert.Same(t, nodes[2], selector.Select()) - - t.Run("stick to the same node", func(t *testing.T) { - node := evmmocks.NewNode(t) - // fourth node is alive (same as 3rd) - node.On("StateAndLatest").Return(evmclient.NodeStateAlive, int64(2), big.NewInt(8)) - node.On("Order").Maybe().Return(int32(1)) - nodes = append(nodes, node) - - selector := evmclient.NewTotalDifficultyNodeSelector(nodes) - assert.Same(t, nodes[2], selector.Select()) - }) - - t.Run("another best node", func(t *testing.T) { - node := evmmocks.NewNode(t) - // fifth node is alive (better than 3rd and 4th) - node.On("StateAndLatest").Return(evmclient.NodeStateAlive, int64(3), big.NewInt(11)) - node.On("Order").Maybe().Return(int32(1)) - nodes = append(nodes, node) - - selector := evmclient.NewTotalDifficultyNodeSelector(nodes) - assert.Same(t, nodes[4], selector.Select()) - }) - - t.Run("nodes never update latest block number", func(t *testing.T) { - node1 := evmmocks.NewNode(t) - node1.On("StateAndLatest").Return(evmclient.NodeStateAlive, int64(-1), nil) - node1.On("Order").Maybe().Return(int32(1)) - node2 := evmmocks.NewNode(t) - node2.On("StateAndLatest").Return(evmclient.NodeStateAlive, int64(-1), nil) - node2.On("Order").Maybe().Return(int32(1)) - nodes := []evmclient.Node{node1, node2} - - selector := evmclient.NewTotalDifficultyNodeSelector(nodes) - assert.Same(t, node1, selector.Select()) - }) -} - -func TestTotalDifficultyNodeSelector_None(t *testing.T) { - t.Parallel() - - var nodes []evmclient.Node - - for i := 0; i < 3; i++ { - node := evmmocks.NewNode(t) - if i == 0 { - // first node is out of sync - node.On("StateAndLatest").Return(evmclient.NodeStateOutOfSync, int64(-1), nil) - } else { - // others are unreachable - node.On("StateAndLatest").Return(evmclient.NodeStateUnreachable, int64(1), big.NewInt(7)) - } - nodes = append(nodes, node) - } - - selector := evmclient.NewTotalDifficultyNodeSelector(nodes) - assert.Nil(t, selector.Select()) -} - -func TestTotalDifficultyNodeSelectorWithOrder(t *testing.T) { - t.Parallel() - - var nodes []evmclient.Node - - t.Run("same td and order", func(t *testing.T) { - for i := 0; i < 3; i++ { - node := evmmocks.NewNode(t) - node.On("StateAndLatest").Return(evmclient.NodeStateAlive, int64(1), big.NewInt(10)) - node.On("Order").Return(int32(2)) - nodes = append(nodes, node) - } - selector := evmclient.NewTotalDifficultyNodeSelector(nodes) - //Should select the first node because all things are equal - assert.Same(t, nodes[0], selector.Select()) - }) - - t.Run("same td but different order", func(t *testing.T) { - node1 := evmmocks.NewNode(t) - node1.On("StateAndLatest").Return(evmclient.NodeStateAlive, int64(3), big.NewInt(10)) - node1.On("Order").Return(int32(3)) - - node2 := evmmocks.NewNode(t) - node2.On("StateAndLatest").Return(evmclient.NodeStateAlive, int64(3), big.NewInt(10)) - node2.On("Order").Return(int32(1)) - - node3 := evmmocks.NewNode(t) - node3.On("StateAndLatest").Return(evmclient.NodeStateAlive, int64(3), big.NewInt(10)) - node3.On("Order").Return(int32(2)) - - nodes := []evmclient.Node{node1, node2, node3} - selector := evmclient.NewTotalDifficultyNodeSelector(nodes) - //Should select the second node as it has the highest priority - assert.Same(t, nodes[1], selector.Select()) - }) - - t.Run("different td but same order", func(t *testing.T) { - node1 := evmmocks.NewNode(t) - node1.On("StateAndLatest").Return(evmclient.NodeStateAlive, int64(1), big.NewInt(10)) - node1.On("Order").Maybe().Return(int32(3)) - - node2 := evmmocks.NewNode(t) - node2.On("StateAndLatest").Return(evmclient.NodeStateAlive, int64(1), big.NewInt(11)) - node2.On("Order").Maybe().Return(int32(3)) - - node3 := evmmocks.NewNode(t) - node3.On("StateAndLatest").Return(evmclient.NodeStateAlive, int64(1), big.NewInt(12)) - node3.On("Order").Return(int32(3)) - - nodes := []evmclient.Node{node1, node2, node3} - selector := evmclient.NewTotalDifficultyNodeSelector(nodes) - //Should select the third node as it has the highest td - assert.Same(t, nodes[2], selector.Select()) - }) - - t.Run("different head and different order", func(t *testing.T) { - node1 := evmmocks.NewNode(t) - node1.On("StateAndLatest").Return(evmclient.NodeStateAlive, int64(1), big.NewInt(100)) - node1.On("Order").Maybe().Return(int32(4)) - - node2 := evmmocks.NewNode(t) - node2.On("StateAndLatest").Return(evmclient.NodeStateAlive, int64(1), big.NewInt(110)) - node2.On("Order").Maybe().Return(int32(5)) - - node3 := evmmocks.NewNode(t) - node3.On("StateAndLatest").Return(evmclient.NodeStateAlive, int64(1), big.NewInt(110)) - node3.On("Order").Maybe().Return(int32(1)) - - node4 := evmmocks.NewNode(t) - node4.On("StateAndLatest").Return(evmclient.NodeStateAlive, int64(1), big.NewInt(105)) - node4.On("Order").Maybe().Return(int32(2)) - - nodes := []evmclient.Node{node1, node2, node3, node4} - selector := evmclient.NewTotalDifficultyNodeSelector(nodes) - //Should select the third node as it has the highest td and will win the priority tie-breaker - assert.Same(t, nodes[2], selector.Select()) - }) -} diff --git a/core/chains/evm/client/node_test.go b/core/chains/evm/client/node_test.go deleted file mode 100644 index a544fc0a3a2..00000000000 --- a/core/chains/evm/client/node_test.go +++ /dev/null @@ -1,31 +0,0 @@ -package client_test - -import ( - "context" - "testing" - - "github.com/stretchr/testify/assert" - - evmclient "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client" - "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" -) - -func Test_NodeWrapError(t *testing.T) { - t.Parallel() - - t.Run("handles nil errors", func(t *testing.T) { - err := evmclient.Wrap(nil, "foo") - assert.NoError(t, err) - }) - - t.Run("adds extra info to context deadline exceeded errors", func(t *testing.T) { - ctx, cancel := context.WithTimeout(testutils.Context(t), 0) - defer cancel() - - err := ctx.Err() - - err = evmclient.Wrap(err, "foo") - - assert.EqualError(t, err, "foo call failed: remote eth node timed out: context deadline exceeded") - }) -} diff --git a/core/chains/evm/client/pool.go b/core/chains/evm/client/pool.go deleted file mode 100644 index dcaf2a6b543..00000000000 --- a/core/chains/evm/client/pool.go +++ /dev/null @@ -1,503 +0,0 @@ -package client - -import ( - "context" - "fmt" - "math/big" - "sync" - "time" - - "github.com/ethereum/go-ethereum" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/rpc" - pkgerrors "github.com/pkg/errors" - "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/client_golang/prometheus/promauto" - - "github.com/smartcontractkit/chainlink-common/pkg/logger" - "github.com/smartcontractkit/chainlink-common/pkg/services" - "github.com/smartcontractkit/chainlink-common/pkg/utils" - - "github.com/smartcontractkit/chainlink/v2/common/config" - evmtypes "github.com/smartcontractkit/chainlink/v2/core/chains/evm/types" -) - -var ( - // PromEVMPoolRPCNodeStates reports current RPC node state - PromEVMPoolRPCNodeStates = promauto.NewGaugeVec(prometheus.GaugeOpts{ - Name: "evm_pool_rpc_node_states", - Help: "The number of RPC nodes currently in the given state for the given chain", - }, []string{"evmChainID", "state"}) -) - -const ( - NodeSelectionMode_HighestHead = "HighestHead" - NodeSelectionMode_RoundRobin = "RoundRobin" - NodeSelectionMode_TotalDifficulty = "TotalDifficulty" - NodeSelectionMode_PriorityLevel = "PriorityLevel" -) - -// NodeSelector represents a strategy to select the next node from the pool. -// -// Deprecated: use [pkg/github.com/smartcontractkit/chainlink/v2/common/client.NodeSelector] -type NodeSelector interface { - // Select returns a Node, or nil if none can be selected. - // Implementation must be thread-safe. - Select() Node - // Name returns the strategy name, e.g. "HighestHead" or "RoundRobin" - Name() string -} - -// PoolConfig represents settings for the Pool -// -// Deprecated: to be removed -type PoolConfig interface { - NodeSelectionMode() string - NodeNoNewHeadsThreshold() time.Duration - LeaseDuration() time.Duration -} - -// Pool represents an abstraction over one or more primary nodes -// It is responsible for liveness checking and balancing queries across live nodes -// -// Deprecated: use [pkg/github.com/smartcontractkit/chainlink/v2/common/client.MultiNode] -type Pool struct { - services.StateMachine - nodes []Node - sendonlys []SendOnlyNode - chainID *big.Int - chainType config.ChainType - logger logger.SugaredLogger - selectionMode string - noNewHeadsThreshold time.Duration - nodeSelector NodeSelector - leaseDuration time.Duration - leaseTicker *time.Ticker - - activeMu sync.RWMutex - activeNode Node - - chStop services.StopChan - wg sync.WaitGroup -} - -// NewPool - creates new instance of [Pool] -// -// Deprecated: use [pkg/github.com/smartcontractkit/chainlink/v2/common/client.NewMultiNode] -func NewPool(lggr logger.Logger, selectionMode string, leaseDuration time.Duration, noNewHeadsTreshold time.Duration, nodes []Node, sendonlys []SendOnlyNode, chainID *big.Int, chainType config.ChainType) *Pool { - if chainID == nil { - panic("chainID is required") - } - - nodeSelector := func() NodeSelector { - switch selectionMode { - case NodeSelectionMode_HighestHead: - return NewHighestHeadNodeSelector(nodes) - case NodeSelectionMode_RoundRobin: - return NewRoundRobinSelector(nodes) - case NodeSelectionMode_TotalDifficulty: - return NewTotalDifficultyNodeSelector(nodes) - case NodeSelectionMode_PriorityLevel: - return NewPriorityLevelNodeSelector(nodes) - default: - panic(fmt.Sprintf("unsupported NodeSelectionMode: %s", selectionMode)) - } - }() - - lggr = logger.Named(lggr, "Pool") - lggr = logger.With(lggr, "evmChainID", chainID.String()) - - p := &Pool{ - nodes: nodes, - sendonlys: sendonlys, - chainID: chainID, - chainType: chainType, - logger: logger.Sugared(lggr), - selectionMode: selectionMode, - noNewHeadsThreshold: noNewHeadsTreshold, - nodeSelector: nodeSelector, - chStop: make(chan struct{}), - leaseDuration: leaseDuration, - } - - p.logger.Debugf("The pool is configured to use NodeSelectionMode: %s", selectionMode) - - return p -} - -// Dial starts every node in the pool -// -// Nodes handle their own redialing and runloops, so this function does not -// return any error if the nodes aren't available -func (p *Pool) Dial(ctx context.Context) error { - return p.StartOnce("Pool", func() (merr error) { - if len(p.nodes) == 0 { - return pkgerrors.Errorf("no available nodes for chain %s", p.chainID.String()) - } - var ms services.MultiStart - for _, n := range p.nodes { - if n.ChainID().Cmp(p.chainID) != 0 { - return ms.CloseBecause(pkgerrors.Errorf("node %s has chain ID %s which does not match pool chain ID of %s", n.String(), n.ChainID().String(), p.chainID.String())) - } - rawNode, ok := n.(*node) - if ok { - // This is a bit hacky but it allows the node to be aware of - // pool state and prevent certain state transitions that might - // otherwise leave no nodes available. It is better to have one - // node in a degraded state than no nodes at all. - rawNode.nLiveNodes = p.nLiveNodes - } - // node will handle its own redialing and automatic recovery - if err := ms.Start(ctx, n); err != nil { - return err - } - } - for _, s := range p.sendonlys { - if s.ChainID().Cmp(p.chainID) != 0 { - return ms.CloseBecause(pkgerrors.Errorf("sendonly node %s has chain ID %s which does not match pool chain ID of %s", s.String(), s.ChainID().String(), p.chainID.String())) - } - if err := ms.Start(ctx, s); err != nil { - return err - } - } - p.wg.Add(1) - go p.runLoop() - - if p.leaseDuration.Seconds() > 0 && p.selectionMode != NodeSelectionMode_RoundRobin { - p.logger.Infof("The pool will switch to best node every %s", p.leaseDuration.String()) - p.wg.Add(1) - go p.checkLeaseLoop() - } else { - p.logger.Info("Best node switching is disabled") - } - - return nil - }) -} - -// nLiveNodes returns the number of currently alive nodes, as well as the highest block number and greatest total difficulty. -// totalDifficulty will be 0 if all nodes return nil. -func (p *Pool) nLiveNodes() (nLiveNodes int, blockNumber int64, totalDifficulty *big.Int) { - totalDifficulty = big.NewInt(0) - for _, n := range p.nodes { - if s, num, td := n.StateAndLatest(); s == NodeStateAlive { - nLiveNodes++ - if num > blockNumber { - blockNumber = num - } - if td != nil && td.Cmp(totalDifficulty) > 0 { - totalDifficulty = td - } - } - } - return -} - -func (p *Pool) checkLease() { - bestNode := p.nodeSelector.Select() - for _, n := range p.nodes { - // Terminate client subscriptions. Services are responsible for reconnecting, which will be routed to the new - // best node. Only terminate connections with more than 1 subscription to account for the aliveLoop subscription - if n.State() == NodeStateAlive && n != bestNode && n.SubscribersCount() > 1 { - p.logger.Infof("Switching to best node from %q to %q", n.String(), bestNode.String()) - n.UnsubscribeAllExceptAliveLoop() - } - } - - if bestNode != p.activeNode { - p.activeMu.Lock() - p.activeNode = bestNode - p.activeMu.Unlock() - } -} - -func (p *Pool) checkLeaseLoop() { - defer p.wg.Done() - p.leaseTicker = time.NewTicker(p.leaseDuration) - defer p.leaseTicker.Stop() - - for { - select { - case <-p.leaseTicker.C: - p.checkLease() - case <-p.chStop: - return - } - } -} - -func (p *Pool) runLoop() { - defer p.wg.Done() - - p.report() - - // Prometheus' default interval is 15s, set this to under 7.5s to avoid - // aliasing (see: https://en.wikipedia.org/wiki/Nyquist_frequency) - reportInterval := 6500 * time.Millisecond - monitor := time.NewTicker(utils.WithJitter(reportInterval)) - defer monitor.Stop() - - for { - select { - case <-monitor.C: - p.report() - case <-p.chStop: - return - } - } -} - -func (p *Pool) report() { - type nodeWithState struct { - Node string - State string - } - - var total, dead int - counts := make(map[NodeState]int) - nodeStates := make([]nodeWithState, len(p.nodes)) - for i, n := range p.nodes { - state := n.State() - nodeStates[i] = nodeWithState{n.String(), state.String()} - total++ - if state != NodeStateAlive { - dead++ - } - counts[state]++ - } - for _, state := range allNodeStates { - count := counts[state] - PromEVMPoolRPCNodeStates.WithLabelValues(p.chainID.String(), state.String()).Set(float64(count)) - } - - live := total - dead - p.logger.Tracew(fmt.Sprintf("Pool state: %d/%d nodes are alive", live, total), "nodeStates", nodeStates) - if total == dead { - rerr := fmt.Errorf("no EVM primary nodes available: 0/%d nodes are alive", total) - p.logger.Criticalw(rerr.Error(), "nodeStates", nodeStates) - p.SvcErrBuffer.Append(rerr) - } else if dead > 0 { - p.logger.Errorw(fmt.Sprintf("At least one EVM primary node is dead: %d/%d nodes are alive", live, total), "nodeStates", nodeStates) - } -} - -// Close tears down the pool and closes all nodes -func (p *Pool) Close() error { - return p.StopOnce("Pool", func() error { - close(p.chStop) - p.wg.Wait() - - return services.CloseAll(services.MultiCloser(p.nodes), services.MultiCloser(p.sendonlys)) - }) -} - -func (p *Pool) ChainID() *big.Int { - return p.selectNode().ChainID() -} - -func (p *Pool) ChainType() config.ChainType { - return p.chainType -} - -// selectNode returns the active Node, if it is still NodeStateAlive, otherwise it selects a new one from the NodeSelector. -func (p *Pool) selectNode() (node Node) { - p.activeMu.RLock() - node = p.activeNode - p.activeMu.RUnlock() - if node != nil && node.State() == NodeStateAlive { - return // still alive - } - - // select a new one - p.activeMu.Lock() - defer p.activeMu.Unlock() - node = p.activeNode - if node != nil && node.State() == NodeStateAlive { - return // another goroutine beat us here - } - - p.activeNode = p.nodeSelector.Select() - - if p.activeNode == nil { - p.logger.Criticalw("No live RPC nodes available", "NodeSelectionMode", p.nodeSelector.Name()) - errmsg := fmt.Errorf("no live nodes available for chain %s", p.chainID.String()) - p.SvcErrBuffer.Append(errmsg) - return &erroringNode{errMsg: errmsg.Error()} - } - - if p.leaseTicker != nil { - p.leaseTicker.Reset(p.leaseDuration) - } - return p.activeNode -} - -func (p *Pool) CallContext(ctx context.Context, result interface{}, method string, args ...interface{}) error { - return p.selectNode().CallContext(ctx, result, method, args...) -} - -func (p *Pool) BatchCallContext(ctx context.Context, b []rpc.BatchElem) error { - return p.selectNode().BatchCallContext(ctx, b) -} - -// BatchCallContextAll calls BatchCallContext for every single node including -// sendonlys. -// CAUTION: This should only be used for mass re-transmitting transactions, it -// might have unexpected effects to use it for anything else. -func (p *Pool) BatchCallContextAll(ctx context.Context, b []rpc.BatchElem) error { - var wg sync.WaitGroup - defer wg.Wait() - - main := p.selectNode() - var all []SendOnlyNode - for _, n := range p.nodes { - all = append(all, n) - } - all = append(all, p.sendonlys...) - for _, n := range all { - if n == main { - // main node is used at the end for the return value - continue - } - // Parallel call made to all other nodes with ignored return value - wg.Add(1) - go func(n SendOnlyNode) { - defer wg.Done() - err := n.BatchCallContext(ctx, b) - if err != nil { - p.logger.Debugw("Secondary node BatchCallContext failed", "err", err) - } else { - p.logger.Trace("Secondary node BatchCallContext success") - } - }(n) - } - - return main.BatchCallContext(ctx, b) -} - -// SendTransaction wrapped Geth client methods -func (p *Pool) SendTransaction(ctx context.Context, tx *types.Transaction) error { - main := p.selectNode() - var all []SendOnlyNode - for _, n := range p.nodes { - all = append(all, n) - } - all = append(all, p.sendonlys...) - for _, n := range all { - if n == main { - // main node is used at the end for the return value - continue - } - // Parallel send to all other nodes with ignored return value - // Async - we do not want to block the main thread with secondary nodes - // in case they are unreliable/slow. - // It is purely a "best effort" send. - // Resource is not unbounded because the default context has a timeout. - ok := p.IfNotStopped(func() { - // Must wrap inside IfNotStopped to avoid waitgroup racing with Close - p.wg.Add(1) - go func(n SendOnlyNode) { - defer p.wg.Done() - - sendCtx, cancel := p.chStop.CtxCancel(ContextWithDefaultTimeout()) - defer cancel() - - err := NewSendError(n.SendTransaction(sendCtx, tx)) - p.logger.Debugw("Sendonly node sent transaction", "name", n.String(), "tx", tx, "err", err) - if err == nil || err.IsNonceTooLowError(nil) || err.IsTransactionAlreadyMined(nil) || err.IsTransactionAlreadyInMempool(nil) { - // Nonce too low or transaction known errors are expected since - // the primary SendTransaction may well have succeeded already - return - } - - p.logger.Warnw("Eth client returned error", "name", n.String(), "err", err, "tx", tx) - }(n) - }) - if !ok { - p.logger.Debug("Cannot send transaction on sendonly node; pool is stopped", "node", n.String()) - } - } - - return main.SendTransaction(ctx, tx) -} - -func (p *Pool) PendingCodeAt(ctx context.Context, account common.Address) ([]byte, error) { - return p.selectNode().PendingCodeAt(ctx, account) -} - -func (p *Pool) PendingNonceAt(ctx context.Context, account common.Address) (uint64, error) { - return p.selectNode().PendingNonceAt(ctx, account) -} - -func (p *Pool) NonceAt(ctx context.Context, account common.Address, blockNumber *big.Int) (uint64, error) { - return p.selectNode().NonceAt(ctx, account, blockNumber) -} - -func (p *Pool) TransactionReceipt(ctx context.Context, txHash common.Hash) (*types.Receipt, error) { - return p.selectNode().TransactionReceipt(ctx, txHash) -} - -func (p *Pool) TransactionByHash(ctx context.Context, txHash common.Hash) (*types.Transaction, error) { - return p.selectNode().TransactionByHash(ctx, txHash) -} - -func (p *Pool) BlockByNumber(ctx context.Context, number *big.Int) (*types.Block, error) { - return p.selectNode().BlockByNumber(ctx, number) -} - -func (p *Pool) BlockByHash(ctx context.Context, hash common.Hash) (*types.Block, error) { - return p.selectNode().BlockByHash(ctx, hash) -} - -func (p *Pool) BlockNumber(ctx context.Context) (uint64, error) { - return p.selectNode().BlockNumber(ctx) -} - -func (p *Pool) BalanceAt(ctx context.Context, account common.Address, blockNumber *big.Int) (*big.Int, error) { - return p.selectNode().BalanceAt(ctx, account, blockNumber) -} - -func (p *Pool) FilterLogs(ctx context.Context, q ethereum.FilterQuery) ([]types.Log, error) { - return p.selectNode().FilterLogs(ctx, q) -} - -func (p *Pool) SubscribeFilterLogs(ctx context.Context, q ethereum.FilterQuery, ch chan<- types.Log) (ethereum.Subscription, error) { - return p.selectNode().SubscribeFilterLogs(ctx, q, ch) -} - -func (p *Pool) EstimateGas(ctx context.Context, call ethereum.CallMsg) (uint64, error) { - return p.selectNode().EstimateGas(ctx, call) -} - -func (p *Pool) SuggestGasPrice(ctx context.Context) (*big.Int, error) { - return p.selectNode().SuggestGasPrice(ctx) -} - -func (p *Pool) CallContract(ctx context.Context, msg ethereum.CallMsg, blockNumber *big.Int) ([]byte, error) { - return p.selectNode().CallContract(ctx, msg, blockNumber) -} - -func (p *Pool) PendingCallContract(ctx context.Context, msg ethereum.CallMsg) ([]byte, error) { - return p.selectNode().PendingCallContract(ctx, msg) -} - -func (p *Pool) CodeAt(ctx context.Context, account common.Address, blockNumber *big.Int) ([]byte, error) { - return p.selectNode().CodeAt(ctx, account, blockNumber) -} - -// bind.ContractBackend methods -func (p *Pool) HeaderByNumber(ctx context.Context, n *big.Int) (*types.Header, error) { - return p.selectNode().HeaderByNumber(ctx, n) -} -func (p *Pool) HeaderByHash(ctx context.Context, h common.Hash) (*types.Header, error) { - return p.selectNode().HeaderByHash(ctx, h) -} - -func (p *Pool) SuggestGasTipCap(ctx context.Context) (*big.Int, error) { - return p.selectNode().SuggestGasTipCap(ctx) -} - -// EthSubscribe implements evmclient.Client -func (p *Pool) EthSubscribe(ctx context.Context, channel chan<- *evmtypes.Head, args ...interface{}) (ethereum.Subscription, error) { - return p.selectNode().EthSubscribe(ctx, channel, args...) -} diff --git a/core/chains/evm/client/pool_test.go b/core/chains/evm/client/pool_test.go deleted file mode 100644 index 5a2c13130d3..00000000000 --- a/core/chains/evm/client/pool_test.go +++ /dev/null @@ -1,393 +0,0 @@ -package client_test - -import ( - "context" - "math/big" - "net/http/httptest" - "net/url" - "sync" - "testing" - "time" - - "github.com/ethereum/go-ethereum/common/hexutil" - "github.com/ethereum/go-ethereum/rpc" - promtestutil "github.com/prometheus/client_golang/prometheus/testutil" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" - "github.com/tidwall/gjson" - "go.uber.org/zap" - - "github.com/smartcontractkit/chainlink-common/pkg/logger" - evmclient "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client" - evmmocks "github.com/smartcontractkit/chainlink/v2/core/chains/evm/mocks" - "github.com/smartcontractkit/chainlink/v2/core/internal/cltest" - "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" -) - -type poolConfig struct { - selectionMode string - noNewHeadsThreshold time.Duration - leaseDuration time.Duration -} - -func (c poolConfig) NodeSelectionMode() string { - return c.selectionMode -} - -func (c poolConfig) NodeNoNewHeadsThreshold() time.Duration { - return c.noNewHeadsThreshold -} - -func (c poolConfig) LeaseDuration() time.Duration { - return c.leaseDuration -} - -var defaultConfig evmclient.PoolConfig = &poolConfig{ - selectionMode: evmclient.NodeSelectionMode_RoundRobin, - noNewHeadsThreshold: 0, - leaseDuration: time.Second * 0, -} - -func TestPool_Dial(t *testing.T) { - t.Parallel() - - tests := []struct { - name string - poolChainID *big.Int - nodeChainID int64 - sendNodeChainID int64 - nodes []chainIDResps - sendNodes []chainIDResp - errStr string - }{ - { - name: "no nodes", - poolChainID: testutils.FixtureChainID, - nodeChainID: testutils.FixtureChainID.Int64(), - sendNodeChainID: testutils.FixtureChainID.Int64(), - nodes: []chainIDResps{}, - sendNodes: []chainIDResp{}, - errStr: "no available nodes for chain 0", - }, - { - name: "normal", - poolChainID: testutils.FixtureChainID, - nodeChainID: testutils.FixtureChainID.Int64(), - sendNodeChainID: testutils.FixtureChainID.Int64(), - nodes: []chainIDResps{ - {ws: chainIDResp{testutils.FixtureChainID.Int64(), nil}}, - }, - sendNodes: []chainIDResp{ - {testutils.FixtureChainID.Int64(), nil}, - }, - }, - { - name: "node has wrong chain ID compared to pool", - poolChainID: testutils.FixtureChainID, - nodeChainID: 42, - sendNodeChainID: testutils.FixtureChainID.Int64(), - nodes: []chainIDResps{ - {ws: chainIDResp{1, nil}}, - }, - sendNodes: []chainIDResp{ - {1, nil}, - }, - errStr: "has chain ID 42 which does not match pool chain ID of 0", - }, - { - name: "sendonly node has wrong chain ID compared to pool", - poolChainID: testutils.FixtureChainID, - nodeChainID: testutils.FixtureChainID.Int64(), - sendNodeChainID: 42, - nodes: []chainIDResps{ - {ws: chainIDResp{testutils.FixtureChainID.Int64(), nil}}, - }, - sendNodes: []chainIDResp{ - {testutils.FixtureChainID.Int64(), nil}, - }, - errStr: "has chain ID 42 which does not match pool chain ID of 0", - }, - { - name: "remote RPC has wrong chain ID for primary node (ws) - no error, it will go into retry loop", - poolChainID: testutils.FixtureChainID, - nodeChainID: testutils.FixtureChainID.Int64(), - sendNodeChainID: testutils.FixtureChainID.Int64(), - nodes: []chainIDResps{ - { - ws: chainIDResp{42, nil}, - http: &chainIDResp{testutils.FixtureChainID.Int64(), nil}, - }, - }, - sendNodes: []chainIDResp{ - {testutils.FixtureChainID.Int64(), nil}, - }, - }, - { - name: "remote RPC has wrong chain ID for primary node (http) - no error, it will go into retry loop", - poolChainID: testutils.FixtureChainID, - nodeChainID: testutils.FixtureChainID.Int64(), - sendNodeChainID: testutils.FixtureChainID.Int64(), - nodes: []chainIDResps{ - { - ws: chainIDResp{testutils.FixtureChainID.Int64(), nil}, - http: &chainIDResp{42, nil}, - }, - }, - sendNodes: []chainIDResp{ - {testutils.FixtureChainID.Int64(), nil}, - }, - }, - { - name: "remote RPC has wrong chain ID for sendonly node - no error, it will go into retry loop", - poolChainID: testutils.FixtureChainID, - nodeChainID: testutils.FixtureChainID.Int64(), - sendNodeChainID: testutils.FixtureChainID.Int64(), - nodes: []chainIDResps{ - {ws: chainIDResp{testutils.FixtureChainID.Int64(), nil}}, - }, - sendNodes: []chainIDResp{ - {42, nil}, - }, - }, - } - for _, test := range tests { - test := test - t.Run(test.name, func(t *testing.T) { - ctx := testutils.Context(t) - - nodes := make([]evmclient.Node, len(test.nodes)) - for i, n := range test.nodes { - nodes[i] = n.newNode(t, test.nodeChainID) - } - sendNodes := make([]evmclient.SendOnlyNode, len(test.sendNodes)) - for i, n := range test.sendNodes { - sendNodes[i] = n.newSendOnlyNode(t, test.sendNodeChainID) - } - p := evmclient.NewPool(logger.Test(t), defaultConfig.NodeSelectionMode(), defaultConfig.LeaseDuration(), time.Second*0, nodes, sendNodes, test.poolChainID, "") - err := p.Dial(ctx) - if err == nil { - t.Cleanup(func() { assert.NoError(t, p.Close()) }) - } - assert.False(t, p.ChainType().IsL2()) - if test.errStr != "" { - require.Error(t, err) - assert.Contains(t, err.Error(), test.errStr) - } else { - require.NoError(t, err) - } - }) - } -} - -type chainIDResp struct { - chainID int64 - err error -} - -func (r *chainIDResp) newSendOnlyNode(t *testing.T, nodeChainID int64) evmclient.SendOnlyNode { - httpURL := r.newHTTPServer(t) - return evmclient.NewSendOnlyNode(logger.Test(t), *httpURL, t.Name(), big.NewInt(nodeChainID)) -} - -func (r *chainIDResp) newHTTPServer(t *testing.T) *url.URL { - rpcSrv := rpc.NewServer() - t.Cleanup(rpcSrv.Stop) - err := rpcSrv.RegisterName("eth", &chainIDService{*r}) - require.NoError(t, err) - ts := httptest.NewServer(rpcSrv) - t.Cleanup(ts.Close) - - httpURL, err := url.Parse(ts.URL) - require.NoError(t, err) - return httpURL -} - -type chainIDResps struct { - ws chainIDResp - http *chainIDResp - id int32 -} - -func (r *chainIDResps) newNode(t *testing.T, nodeChainID int64) evmclient.Node { - ws := testutils.NewWSServer(t, big.NewInt(r.ws.chainID), func(method string, params gjson.Result) (resp testutils.JSONRPCResponse) { - switch method { - case "eth_subscribe": - resp.Result = `"0x00"` - resp.Notify = headResult - return - case "eth_unsubscribe": - resp.Result = "true" - return - } - t.Errorf("Unexpected method call: %s(%s)", method, params) - return - }).WSURL().String() - - wsURL, err := url.Parse(ws) - require.NoError(t, err) - - var httpURL *url.URL - if r.http != nil { - httpURL = r.http.newHTTPServer(t) - } - - defer func() { r.id++ }() - return evmclient.NewNode(evmclient.TestNodePoolConfig{}, time.Second*0, logger.Test(t), *wsURL, httpURL, t.Name(), r.id, big.NewInt(nodeChainID), 0) -} - -type chainIDService struct { - chainIDResp -} - -func (x *chainIDService) ChainId(ctx context.Context) (*hexutil.Big, error) { - if x.err != nil { - return nil, x.err - } - return (*hexutil.Big)(big.NewInt(x.chainID)), nil -} - -func TestUnit_Pool_RunLoop(t *testing.T) { - t.Parallel() - - n1 := evmmocks.NewNode(t) - n2 := evmmocks.NewNode(t) - n3 := evmmocks.NewNode(t) - nodes := []evmclient.Node{n1, n2, n3} - - lggr, observedLogs := logger.TestObserved(t, zap.ErrorLevel) - p := evmclient.NewPool(lggr, defaultConfig.NodeSelectionMode(), defaultConfig.LeaseDuration(), time.Second*0, nodes, []evmclient.SendOnlyNode{}, &cltest.FixtureChainID, "") - - n1.On("String").Maybe().Return("n1") - n2.On("String").Maybe().Return("n2") - n3.On("String").Maybe().Return("n3") - - n1.On("Close").Maybe().Return(nil) - n2.On("Close").Maybe().Return(nil) - n3.On("Close").Maybe().Return(nil) - - // n1 is alive - n1.On("Start", mock.Anything).Return(nil).Once() - n1.On("State").Return(evmclient.NodeStateAlive) - n1.On("ChainID").Return(testutils.FixtureChainID).Once() - // n2 is unreachable - n2.On("Start", mock.Anything).Return(nil).Once() - n2.On("State").Return(evmclient.NodeStateUnreachable) - n2.On("ChainID").Return(testutils.FixtureChainID).Once() - // n3 is out of sync - n3.On("Start", mock.Anything).Return(nil).Once() - n3.On("State").Return(evmclient.NodeStateOutOfSync) - n3.On("ChainID").Return(testutils.FixtureChainID).Once() - - require.NoError(t, p.Dial(testutils.Context(t))) - t.Cleanup(func() { assert.NoError(t, p.Close()) }) - - testutils.WaitForLogMessage(t, observedLogs, "At least one EVM primary node is dead") - - testutils.AssertEventually(t, func() bool { - totalReported := promtestutil.CollectAndCount(evmclient.PromEVMPoolRPCNodeStates) - if totalReported < 3 { - return false - } - if promtestutil.ToFloat64(evmclient.PromEVMPoolRPCNodeStates.WithLabelValues("0", "Alive")) < 1.0 { - return false - } - if promtestutil.ToFloat64(evmclient.PromEVMPoolRPCNodeStates.WithLabelValues("0", "Unreachable")) < 1.0 { - return false - } - if promtestutil.ToFloat64(evmclient.PromEVMPoolRPCNodeStates.WithLabelValues("0", "OutOfSync")) < 1.0 { - return false - } - return true - }) -} - -func TestUnit_Pool_BatchCallContextAll(t *testing.T) { - t.Parallel() - - var nodes []evmclient.Node - var sendonlys []evmclient.SendOnlyNode - - nodeCount := 2 - sendOnlyCount := 3 - - b := []rpc.BatchElem{ - {Method: "method", Args: []interface{}{1, false}}, - {Method: "method2"}, - } - - ctx := testutils.Context(t) - - for i := 0; i < nodeCount; i++ { - node := evmmocks.NewNode(t) - node.On("State").Return(evmclient.NodeStateAlive).Maybe() - node.On("BatchCallContext", ctx, b).Return(nil).Once() - nodes = append(nodes, node) - } - for i := 0; i < sendOnlyCount; i++ { - s := evmmocks.NewSendOnlyNode(t) - s.On("BatchCallContext", ctx, b).Return(nil).Once() - sendonlys = append(sendonlys, s) - } - - p := evmclient.NewPool(logger.Test(t), defaultConfig.NodeSelectionMode(), defaultConfig.LeaseDuration(), time.Second*0, nodes, sendonlys, &cltest.FixtureChainID, "") - - assert.False(t, p.ChainType().IsL2()) - require.NoError(t, p.BatchCallContextAll(ctx, b)) -} - -func TestUnit_Pool_LeaseDuration(t *testing.T) { - t.Parallel() - - n1 := evmmocks.NewNode(t) - n2 := evmmocks.NewNode(t) - nodes := []evmclient.Node{n1, n2} - type nodeStateSwitch struct { - isAlive bool - mu sync.RWMutex - } - - nodeSwitch := nodeStateSwitch{ - isAlive: true, - mu: sync.RWMutex{}, - } - - n1.On("String").Maybe().Return("n1") - n2.On("String").Maybe().Return("n2") - n1.On("Close").Maybe().Return(nil) - n2.On("Close").Maybe().Return(nil) - n2.On("UnsubscribeAllExceptAliveLoop").Return() - n2.On("SubscribersCount").Return(int32(2)) - - n1.On("Start", mock.Anything).Return(nil).Once() - n1.On("State").Return(func() evmclient.NodeState { - nodeSwitch.mu.RLock() - defer nodeSwitch.mu.RUnlock() - if nodeSwitch.isAlive { - return evmclient.NodeStateAlive - } - return evmclient.NodeStateOutOfSync - }) - n1.On("Order").Return(int32(1)) - n1.On("ChainID").Return(testutils.FixtureChainID).Once() - - n2.On("Start", mock.Anything).Return(nil).Once() - n2.On("State").Return(evmclient.NodeStateAlive) - n2.On("Order").Return(int32(2)) - n2.On("ChainID").Return(testutils.FixtureChainID).Once() - - lggr, observedLogs := logger.TestObserved(t, zap.InfoLevel) - p := evmclient.NewPool(lggr, "PriorityLevel", time.Second*2, time.Second*0, nodes, []evmclient.SendOnlyNode{}, &cltest.FixtureChainID, "") - require.NoError(t, p.Dial(testutils.Context(t))) - t.Cleanup(func() { assert.NoError(t, p.Close()) }) - - testutils.WaitForLogMessage(t, observedLogs, "The pool will switch to best node every 2s") - nodeSwitch.mu.Lock() - nodeSwitch.isAlive = false - nodeSwitch.mu.Unlock() - testutils.WaitForLogMessage(t, observedLogs, "At least one EVM primary node is dead") - nodeSwitch.mu.Lock() - nodeSwitch.isAlive = true - nodeSwitch.mu.Unlock() - testutils.WaitForLogMessage(t, observedLogs, `Switching to best node from "n2" to "n1"`) -} diff --git a/core/chains/evm/client/rpc_client.go b/core/chains/evm/client/rpc_client.go index 548acf3206c..5b64900a0cb 100644 --- a/core/chains/evm/client/rpc_client.go +++ b/core/chains/evm/client/rpc_client.go @@ -17,9 +17,12 @@ import ( "github.com/ethereum/go-ethereum/rpc" "github.com/google/uuid" pkgerrors "github.com/pkg/errors" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" commonassets "github.com/smartcontractkit/chainlink-common/pkg/assets" "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-common/pkg/services" commonclient "github.com/smartcontractkit/chainlink/v2/common/client" commontypes "github.com/smartcontractkit/chainlink/v2/common/types" @@ -29,6 +32,48 @@ import ( ubig "github.com/smartcontractkit/chainlink/v2/core/chains/evm/utils/big" ) +var ( + promEVMPoolRPCNodeDials = promauto.NewCounterVec(prometheus.CounterOpts{ + Name: "evm_pool_rpc_node_dials_total", + Help: "The total number of dials for the given RPC node", + }, []string{"evmChainID", "nodeName"}) + promEVMPoolRPCNodeDialsFailed = promauto.NewCounterVec(prometheus.CounterOpts{ + Name: "evm_pool_rpc_node_dials_failed", + Help: "The total number of failed dials for the given RPC node", + }, []string{"evmChainID", "nodeName"}) + promEVMPoolRPCNodeDialsSuccess = promauto.NewCounterVec(prometheus.CounterOpts{ + Name: "evm_pool_rpc_node_dials_success", + Help: "The total number of successful dials for the given RPC node", + }, []string{"evmChainID", "nodeName"}) + + promEVMPoolRPCNodeCalls = promauto.NewCounterVec(prometheus.CounterOpts{ + Name: "evm_pool_rpc_node_calls_total", + Help: "The approximate total number of RPC calls for the given RPC node", + }, []string{"evmChainID", "nodeName"}) + promEVMPoolRPCNodeCallsFailed = promauto.NewCounterVec(prometheus.CounterOpts{ + Name: "evm_pool_rpc_node_calls_failed", + Help: "The approximate total number of failed RPC calls for the given RPC node", + }, []string{"evmChainID", "nodeName"}) + promEVMPoolRPCNodeCallsSuccess = promauto.NewCounterVec(prometheus.CounterOpts{ + Name: "evm_pool_rpc_node_calls_success", + Help: "The approximate total number of successful RPC calls for the given RPC node", + }, []string{"evmChainID", "nodeName"}) + promEVMPoolRPCCallTiming = promauto.NewHistogramVec(prometheus.HistogramOpts{ + Name: "evm_pool_rpc_node_rpc_call_time", + Help: "The duration of an RPC call in nanoseconds", + Buckets: []float64{ + float64(50 * time.Millisecond), + float64(100 * time.Millisecond), + float64(200 * time.Millisecond), + float64(500 * time.Millisecond), + float64(1 * time.Second), + float64(2 * time.Second), + float64(4 * time.Second), + float64(8 * time.Second), + }, + }, []string{"evmChainID", "nodeName", "rpcHost", "isSendOnly", "success", "rpcCallName"}) +) + // RPCClient includes all the necessary generalized RPC methods along with any additional chain-specific methods. // //go:generate mockery --quiet --name RPCClient --output ./mocks --case=underscore @@ -58,6 +103,12 @@ type RPCClient interface { TransactionReceiptGeth(ctx context.Context, txHash common.Hash) (r *types.Receipt, err error) } +type rawclient struct { + rpc *rpc.Client + geth *ethclient.Client + uri url.URL +} + type rpcClient struct { rpcLog logger.SugaredLogger name string @@ -837,6 +888,15 @@ func (r *rpcClient) BalanceAt(ctx context.Context, account common.Address, block return } +// CallArgs represents the data used to call the balance method of a contract. +// "To" is the address of the ERC contract. "Data" is the message sent +// to the contract. "From" is the sender address. +type CallArgs struct { + From common.Address `json:"from"` + To common.Address `json:"to"` + Data hexutil.Bytes `json:"data"` +} + // TokenBalance returns the balance of the given address for the token contract address. func (r *rpcClient) TokenBalance(ctx context.Context, address common.Address, contractAddress common.Address) (*big.Int, error) { result := "" @@ -1014,6 +1074,21 @@ func (r *rpcClient) makeLiveQueryCtxAndSafeGetClients(parentCtx context.Context) return } +// makeQueryCtx returns a context that cancels if: +// 1. Passed in ctx cancels +// 2. Passed in channel is closed +// 3. Default timeout is reached (queryTimeout) +func makeQueryCtx(ctx context.Context, ch services.StopChan) (context.Context, context.CancelFunc) { + var chCancel, timeoutCancel context.CancelFunc + ctx, chCancel = ch.Ctx(ctx) + ctx, timeoutCancel = context.WithTimeout(ctx, queryTimeout) + cancel := func() { + chCancel() + timeoutCancel() + } + return ctx, cancel +} + func (r *rpcClient) makeQueryCtx(ctx context.Context) (context.Context, context.CancelFunc) { return makeQueryCtx(ctx, r.getChStopInflight()) } @@ -1058,3 +1133,10 @@ func (r *rpcClient) Name() string { func Name(r *rpcClient) string { return r.name } + +func ToBlockNumArg(number *big.Int) string { + if number == nil { + return "latest" + } + return hexutil.EncodeBig(number) +} diff --git a/core/chains/evm/client/send_only_node.go b/core/chains/evm/client/send_only_node.go deleted file mode 100644 index b6ad26696fc..00000000000 --- a/core/chains/evm/client/send_only_node.go +++ /dev/null @@ -1,267 +0,0 @@ -package client - -import ( - "context" - "fmt" - "log" - "math/big" - "net/url" - "strconv" - "sync" - "time" - - "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/ethclient" - "github.com/ethereum/go-ethereum/rpc" - - "github.com/smartcontractkit/chainlink-common/pkg/logger" - "github.com/smartcontractkit/chainlink-common/pkg/services" -) - -//go:generate mockery --quiet --name SendOnlyNode --output ../mocks/ --case=underscore - -// SendOnlyNode represents one ethereum node used as a sendonly -// -// Deprecated: use [pkg/github.com/smartcontractkit/chainlink/v2/common/client.SendOnlyNode] -type SendOnlyNode interface { - // Start may attempt to connect to the node, but should only return error for misconfiguration - never for temporary errors. - Start(context.Context) error - Close() error - - ChainID() (chainID *big.Int) - - SendTransaction(ctx context.Context, tx *types.Transaction) error - BatchCallContext(ctx context.Context, b []rpc.BatchElem) error - - String() string - // State returns NodeState - State() NodeState - // Name is a unique identifier for this node. - Name() string -} - -//go:generate mockery --quiet --name TxSender --output ./mocks/ --case=underscore - -type TxSender interface { - SendTransaction(ctx context.Context, tx *types.Transaction) error - ChainID(context.Context) (*big.Int, error) -} - -//go:generate mockery --quiet --name BatchSender --output ./mocks/ --case=underscore - -type BatchSender interface { - BatchCallContext(ctx context.Context, b []rpc.BatchElem) error -} - -var _ SendOnlyNode = &sendOnlyNode{} - -// It only supports sending transactions -// It must a http(s) url -type sendOnlyNode struct { - services.StateMachine - - stateMu sync.RWMutex // protects state* fields - state NodeState - - uri url.URL - batchSender BatchSender - sender TxSender - log logger.Logger - dialed bool - name string - chainID *big.Int - chStop services.StopChan - wg sync.WaitGroup -} - -// NewSendOnlyNode returns a new sendonly node -// -// Deprecated: use [pkg/github.com/smartcontractkit/chainlink/v2/common/client.NewSendOnlyNode] -func NewSendOnlyNode(lggr logger.Logger, httpuri url.URL, name string, chainID *big.Int) SendOnlyNode { - s := new(sendOnlyNode) - s.name = name - s.log = logger.Named(logger.Named(lggr, "SendOnlyNode"), name) - s.log = logger.With(s.log, - "nodeTier", "sendonly", - ) - s.uri = httpuri - s.chainID = chainID - s.chStop = make(chan struct{}) - return s -} - -func (s *sendOnlyNode) Start(ctx context.Context) error { - return s.StartOnce(s.name, func() error { - s.start(ctx) - return nil - }) -} - -// Start setups up and verifies the sendonly node -// Should only be called once in a node's lifecycle -func (s *sendOnlyNode) start(startCtx context.Context) { - if s.state != NodeStateUndialed { - panic(fmt.Sprintf("cannot dial node with state %v", s.state)) - } - - s.log.Debugw("evmclient.Client#Dial(...)") - if s.dialed { - panic("evmclient.Client.Dial(...) should only be called once during the node's lifetime.") - } - - // DialHTTP doesn't actually make any external HTTP calls - // It can only return error if the URL is malformed. No amount of retries - // will change this result. - rpc, err := rpc.DialHTTP(s.uri.String()) - if err != nil { - promEVMPoolRPCNodeTransitionsToUnusable.WithLabelValues(s.chainID.String(), s.name).Inc() - s.log.Errorw("Dial failed: EVM SendOnly Node is unusable", "err", err) - s.setState(NodeStateUnusable) - return - } - s.dialed = true - geth := ethclient.NewClient(rpc) - s.SetEthClient(rpc, geth) - - if s.chainID.Cmp(big.NewInt(0)) == 0 { - // Skip verification if chainID is zero - s.log.Warn("sendonly rpc ChainID verification skipped") - } else { - verifyCtx, verifyCancel := s.makeQueryCtx(startCtx) - defer verifyCancel() - - chainID, err := s.sender.ChainID(verifyCtx) - if err != nil || chainID.Cmp(s.chainID) != 0 { - promEVMPoolRPCNodeTransitionsToUnreachable.WithLabelValues(s.chainID.String(), s.name).Inc() - if err != nil { - promEVMPoolRPCNodeTransitionsToUnreachable.WithLabelValues(s.chainID.String(), s.name).Inc() - s.log.Errorw(fmt.Sprintf("Verify failed: %v", err), "err", err) - s.setState(NodeStateUnreachable) - } else { - promEVMPoolRPCNodeTransitionsToInvalidChainID.WithLabelValues(s.chainID.String(), s.name).Inc() - s.log.Errorf( - "sendonly rpc ChainID doesn't match local chain ID: RPC ID=%s, local ID=%s, node name=%s", - chainID.String(), - s.chainID.String(), - s.name, - ) - s.setState(NodeStateInvalidChainID) - } - // Since it has failed, spin up the verifyLoop that will keep - // retrying until success - s.wg.Add(1) - go s.verifyLoop() - return - } - } - - promEVMPoolRPCNodeTransitionsToAlive.WithLabelValues(s.chainID.String(), s.name).Inc() - s.setState(NodeStateAlive) - s.log.Infow("Sendonly RPC Node is online", "nodeState", s.state) -} - -func (s *sendOnlyNode) SetEthClient(newBatchSender BatchSender, newSender TxSender) { - if s.sender != nil { - log.Panicf("sendOnlyNode.SetEthClient should only be called once!") - return - } - s.batchSender = newBatchSender - s.sender = newSender -} - -func (s *sendOnlyNode) Close() error { - return s.StopOnce(s.name, func() error { - close(s.chStop) - s.wg.Wait() - s.setState(NodeStateClosed) - return nil - }) -} - -func (s *sendOnlyNode) logTiming(lggr logger.Logger, duration time.Duration, err error, callName string) { - promEVMPoolRPCCallTiming. - WithLabelValues( - s.chainID.String(), // chain id - s.name, // node name - s.uri.Host, // rpc domain - "true", // is send only - strconv.FormatBool(err == nil), // is successful - callName, // rpc call name - ). - Observe(float64(duration)) - lggr.Debugw(fmt.Sprintf("SendOnly RPC call: evmclient.#%s", callName), - "duration", duration, - "rpcDomain", s.uri.Host, - "name", s.name, - "chainID", s.chainID, - "sendOnly", true, - "err", err, - ) -} - -func (s *sendOnlyNode) SendTransaction(parentCtx context.Context, tx *types.Transaction) (err error) { - defer func(start time.Time) { - s.logTiming(s.log, time.Since(start), err, "SendTransaction") - }(time.Now()) - - ctx, cancel := s.makeQueryCtx(parentCtx) - defer cancel() - return s.wrap(s.sender.SendTransaction(ctx, tx)) -} - -func (s *sendOnlyNode) BatchCallContext(parentCtx context.Context, b []rpc.BatchElem) (err error) { - defer func(start time.Time) { - s.logTiming(logger.With(s.log, "nBatchElems", len(b)), time.Since(start), err, "BatchCallContext") - }(time.Now()) - - ctx, cancel := s.makeQueryCtx(parentCtx) - defer cancel() - return s.wrap(s.batchSender.BatchCallContext(ctx, b)) -} - -func (s *sendOnlyNode) ChainID() (chainID *big.Int) { - return s.chainID -} - -func (s *sendOnlyNode) wrap(err error) error { - return wrap(err, fmt.Sprintf("sendonly http (%s)", s.uri.Redacted())) -} - -func (s *sendOnlyNode) String() string { - return fmt.Sprintf("(secondary)%s:%s", s.name, s.uri.Redacted()) -} - -// makeQueryCtx returns a context that cancels if: -// 1. Passed in ctx cancels -// 2. chStop is closed -// 3. Default timeout is reached (queryTimeout) -func (s *sendOnlyNode) makeQueryCtx(ctx context.Context) (context.Context, context.CancelFunc) { - var chCancel, timeoutCancel context.CancelFunc - ctx, chCancel = s.chStop.Ctx(ctx) - ctx, timeoutCancel = context.WithTimeout(ctx, queryTimeout) - cancel := func() { - chCancel() - timeoutCancel() - } - return ctx, cancel -} - -func (s *sendOnlyNode) setState(state NodeState) (changed bool) { - s.stateMu.Lock() - defer s.stateMu.Unlock() - if s.state == state { - return false - } - s.state = state - return true -} - -func (s *sendOnlyNode) State() NodeState { - s.stateMu.RLock() - defer s.stateMu.RUnlock() - return s.state -} - -func (s *sendOnlyNode) Name() string { - return s.name -} diff --git a/core/chains/evm/client/send_only_node_lifecycle.go b/core/chains/evm/client/send_only_node_lifecycle.go deleted file mode 100644 index 127a5c6678c..00000000000 --- a/core/chains/evm/client/send_only_node_lifecycle.go +++ /dev/null @@ -1,68 +0,0 @@ -package client - -import ( - "fmt" - "time" - - "github.com/smartcontractkit/chainlink/v2/core/chains/evm/utils" -) - -// verifyLoop may only be triggered once, on Start, if initial chain ID check -// fails. -// -// It will continue checking until success and then exit permanently. -func (s *sendOnlyNode) verifyLoop() { - defer s.wg.Done() - ctx, cancel := s.chStop.NewCtx() - defer cancel() - - backoff := utils.NewRedialBackoff() - for { - select { - case <-time.After(backoff.Duration()): - chainID, err := s.sender.ChainID(ctx) - if err != nil { - ok := s.IfStarted(func() { - if changed := s.setState(NodeStateUnreachable); changed { - promEVMPoolRPCNodeTransitionsToUnreachable.WithLabelValues(s.chainID.String(), s.name).Inc() - } - }) - if !ok { - return - } - s.log.Errorw(fmt.Sprintf("Verify failed: %v", err), "err", err) - continue - } else if chainID.Cmp(s.chainID) != 0 { - ok := s.IfStarted(func() { - if changed := s.setState(NodeStateInvalidChainID); changed { - promEVMPoolRPCNodeTransitionsToInvalidChainID.WithLabelValues(s.chainID.String(), s.name).Inc() - } - }) - if !ok { - return - } - s.log.Errorf( - "sendonly rpc ChainID doesn't match local chain ID: RPC ID=%s, local ID=%s, node name=%s", - chainID.String(), - s.chainID.String(), - s.name, - ) - - continue - } else { - ok := s.IfStarted(func() { - if changed := s.setState(NodeStateAlive); changed { - promEVMPoolRPCNodeTransitionsToAlive.WithLabelValues(s.chainID.String(), s.name).Inc() - } - }) - if !ok { - return - } - s.log.Infow("Sendonly RPC Node is online", "nodeState", s.state) - return - } - case <-ctx.Done(): - return - } - } -} diff --git a/core/chains/evm/client/send_only_node_test.go b/core/chains/evm/client/send_only_node_test.go deleted file mode 100644 index 61db09a448c..00000000000 --- a/core/chains/evm/client/send_only_node_test.go +++ /dev/null @@ -1,170 +0,0 @@ -package client_test - -import ( - "fmt" - "math/big" - "net/url" - "testing" - - "github.com/ethereum/go-ethereum/accounts/abi/bind" - "github.com/ethereum/go-ethereum/common/hexutil" - "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/crypto" - "github.com/ethereum/go-ethereum/rpc" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" - "go.uber.org/zap" - - "github.com/smartcontractkit/chainlink-common/pkg/logger" - - "github.com/smartcontractkit/chainlink/v2/core/chains/evm/assets" - "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client" - "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client/mocks" - "github.com/smartcontractkit/chainlink/v2/core/internal/cltest" - "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" -) - -func TestNewSendOnlyNode(t *testing.T) { - t.Parallel() - - urlFormat := "http://user:%s@testurl.com" - password := "pass" - url := testutils.MustParseURL(t, fmt.Sprintf(urlFormat, password)) - redacted := fmt.Sprintf(urlFormat, "xxxxx") - lggr := logger.Test(t) - name := "TestNewSendOnlyNode" - chainID := testutils.NewRandomEVMChainID() - - node := client.NewSendOnlyNode(lggr, *url, name, chainID) - assert.NotNil(t, node) - - // Must contain name & url with redacted password - assert.Contains(t, node.String(), fmt.Sprintf("%s:%s", name, redacted)) - assert.Equal(t, node.ChainID(), chainID) -} - -func TestStartSendOnlyNode(t *testing.T) { - t.Parallel() - - t.Run("Start with Random ChainID", func(t *testing.T) { - t.Parallel() - chainID := testutils.NewRandomEVMChainID() - r := chainIDResp{chainID.Int64(), nil} - url := r.newHTTPServer(t) - lggr, observedLogs := logger.TestObserved(t, zap.WarnLevel) - s := client.NewSendOnlyNode(lggr, *url, t.Name(), chainID) - defer func() { assert.NoError(t, s.Close()) }() - err := s.Start(testutils.Context(t)) - assert.NoError(t, err) // No errors expected - assert.Equal(t, 0, observedLogs.Len()) // No warnings expected - }) - - t.Run("Start with ChainID=0", func(t *testing.T) { - t.Parallel() - lggr, observedLogs := logger.TestObserved(t, zap.WarnLevel) - chainID := testutils.FixtureChainID - r := chainIDResp{chainID.Int64(), nil} - url := r.newHTTPServer(t) - s := client.NewSendOnlyNode(lggr, *url, t.Name(), testutils.FixtureChainID) - - defer func() { assert.NoError(t, s.Close()) }() - err := s.Start(testutils.Context(t)) - assert.NoError(t, err) - // If ChainID = 0, this should get converted into a warning from Start() - testutils.WaitForLogMessage(t, observedLogs, "ChainID verification skipped") - }) - - t.Run("becomes unusable (and remains undialed) if initial dial fails", func(t *testing.T) { - t.Parallel() - lggr, observedLogs := logger.TestObserved(t, zap.WarnLevel) - invalidURL := url.URL{Scheme: "some rubbish", Host: "not a valid host"} - s := client.NewSendOnlyNode(lggr, invalidURL, t.Name(), testutils.FixtureChainID) - - defer func() { assert.NoError(t, s.Close()) }() - err := s.Start(testutils.Context(t)) - require.NoError(t, err) - - assert.False(t, client.IsDialed(s)) - testutils.RequireLogMessage(t, observedLogs, "Dial failed: EVM SendOnly Node is unusable") - }) -} - -func createSignedTx(t *testing.T, chainID *big.Int, nonce uint64, data []byte) *types.Transaction { - key, err := crypto.GenerateKey() - require.NoError(t, err) - sender, err := bind.NewKeyedTransactorWithChainID(key, chainID) - require.NoError(t, err) - tx := cltest.NewLegacyTransaction( - nonce, sender.From, - assets.Ether(100).ToInt(), - 21000, big.NewInt(1000000000), data, - ) - signedTx, err := sender.Signer(sender.From, tx) - require.NoError(t, err) - return signedTx -} - -func TestSendTransaction(t *testing.T) { - t.Parallel() - - chainID := testutils.FixtureChainID - lggr, observedLogs := logger.TestObserved(t, zap.DebugLevel) - url := testutils.MustParseURL(t, "http://place.holder") - s := client.NewSendOnlyNode(lggr, - *url, - t.Name(), - testutils.FixtureChainID).(client.TestableSendOnlyNode) - require.NotNil(t, s) - - signedTx := createSignedTx(t, chainID, 1, []byte{1, 2, 3}) - - mockTxSender := mocks.NewTxSender(t) - mockTxSender.On("SendTransaction", mock.Anything, mock.MatchedBy( - func(tx *types.Transaction) bool { - return tx.Nonce() == uint64(1) - }, - )).Once().Return(nil) - s.SetEthClient(nil, mockTxSender) - - err := s.SendTransaction(testutils.Context(t), signedTx) - assert.NoError(t, err) - testutils.WaitForLogMessage(t, observedLogs, "SendOnly RPC call") -} - -func TestBatchCallContext(t *testing.T) { - t.Parallel() - - lggr := logger.Test(t) - chainID := testutils.FixtureChainID - url := testutils.MustParseURL(t, "http://place.holder") - s := client.NewSendOnlyNode( - lggr, - *url, "TestBatchCallContext", - chainID).(client.TestableSendOnlyNode) - - blockNum := hexutil.EncodeBig(big.NewInt(42)) - req := []rpc.BatchElem{ - { - Method: "eth_getBlockByNumber", - Args: []interface{}{blockNum, true}, - Result: &types.Block{}, - }, - { - Method: "method", - Args: []interface{}{1, false}}, - } - - mockBatchSender := mocks.NewBatchSender(t) - mockBatchSender.On("BatchCallContext", mock.Anything, - mock.MatchedBy( - func(b []rpc.BatchElem) bool { - return len(b) == 2 && - b[0].Method == "eth_getBlockByNumber" && b[0].Args[0] == blockNum && b[0].Args[1].(bool) - })).Return(nil).Once().Return(nil) - - s.SetEthClient(mockBatchSender, nil) - - err := s.BatchCallContext(testutils.Context(t), req) - assert.NoError(t, err) -} diff --git a/core/chains/evm/mocks/node.go b/core/chains/evm/mocks/node.go deleted file mode 100644 index 6a939d5e844..00000000000 --- a/core/chains/evm/mocks/node.go +++ /dev/null @@ -1,881 +0,0 @@ -// Code generated by mockery v2.42.2. DO NOT EDIT. - -package mocks - -import ( - big "math/big" - - common "github.com/ethereum/go-ethereum/common" - client "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client" - - context "context" - - ethereum "github.com/ethereum/go-ethereum" - - evmtypes "github.com/smartcontractkit/chainlink/v2/core/chains/evm/types" - - mock "github.com/stretchr/testify/mock" - - rpc "github.com/ethereum/go-ethereum/rpc" - - types "github.com/ethereum/go-ethereum/core/types" -) - -// Node is an autogenerated mock type for the Node type -type Node struct { - mock.Mock -} - -// BalanceAt provides a mock function with given fields: ctx, account, blockNumber -func (_m *Node) BalanceAt(ctx context.Context, account common.Address, blockNumber *big.Int) (*big.Int, error) { - ret := _m.Called(ctx, account, blockNumber) - - if len(ret) == 0 { - panic("no return value specified for BalanceAt") - } - - var r0 *big.Int - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, common.Address, *big.Int) (*big.Int, error)); ok { - return rf(ctx, account, blockNumber) - } - if rf, ok := ret.Get(0).(func(context.Context, common.Address, *big.Int) *big.Int); ok { - r0 = rf(ctx, account, blockNumber) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*big.Int) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, common.Address, *big.Int) error); ok { - r1 = rf(ctx, account, blockNumber) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// BatchCallContext provides a mock function with given fields: ctx, b -func (_m *Node) BatchCallContext(ctx context.Context, b []rpc.BatchElem) error { - ret := _m.Called(ctx, b) - - if len(ret) == 0 { - panic("no return value specified for BatchCallContext") - } - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, []rpc.BatchElem) error); ok { - r0 = rf(ctx, b) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// BlockByHash provides a mock function with given fields: ctx, hash -func (_m *Node) BlockByHash(ctx context.Context, hash common.Hash) (*types.Block, error) { - ret := _m.Called(ctx, hash) - - if len(ret) == 0 { - panic("no return value specified for BlockByHash") - } - - var r0 *types.Block - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, common.Hash) (*types.Block, error)); ok { - return rf(ctx, hash) - } - if rf, ok := ret.Get(0).(func(context.Context, common.Hash) *types.Block); ok { - r0 = rf(ctx, hash) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*types.Block) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, common.Hash) error); ok { - r1 = rf(ctx, hash) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// BlockByNumber provides a mock function with given fields: ctx, number -func (_m *Node) BlockByNumber(ctx context.Context, number *big.Int) (*types.Block, error) { - ret := _m.Called(ctx, number) - - if len(ret) == 0 { - panic("no return value specified for BlockByNumber") - } - - var r0 *types.Block - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, *big.Int) (*types.Block, error)); ok { - return rf(ctx, number) - } - if rf, ok := ret.Get(0).(func(context.Context, *big.Int) *types.Block); ok { - r0 = rf(ctx, number) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*types.Block) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, *big.Int) error); ok { - r1 = rf(ctx, number) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// BlockNumber provides a mock function with given fields: ctx -func (_m *Node) BlockNumber(ctx context.Context) (uint64, error) { - ret := _m.Called(ctx) - - if len(ret) == 0 { - panic("no return value specified for BlockNumber") - } - - var r0 uint64 - var r1 error - if rf, ok := ret.Get(0).(func(context.Context) (uint64, error)); ok { - return rf(ctx) - } - if rf, ok := ret.Get(0).(func(context.Context) uint64); ok { - r0 = rf(ctx) - } else { - r0 = ret.Get(0).(uint64) - } - - if rf, ok := ret.Get(1).(func(context.Context) error); ok { - r1 = rf(ctx) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// CallContext provides a mock function with given fields: ctx, result, method, args -func (_m *Node) CallContext(ctx context.Context, result interface{}, method string, args ...interface{}) error { - var _ca []interface{} - _ca = append(_ca, ctx, result, method) - _ca = append(_ca, args...) - ret := _m.Called(_ca...) - - if len(ret) == 0 { - panic("no return value specified for CallContext") - } - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, interface{}, string, ...interface{}) error); ok { - r0 = rf(ctx, result, method, args...) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// CallContract provides a mock function with given fields: ctx, msg, blockNumber -func (_m *Node) CallContract(ctx context.Context, msg ethereum.CallMsg, blockNumber *big.Int) ([]byte, error) { - ret := _m.Called(ctx, msg, blockNumber) - - if len(ret) == 0 { - panic("no return value specified for CallContract") - } - - var r0 []byte - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, ethereum.CallMsg, *big.Int) ([]byte, error)); ok { - return rf(ctx, msg, blockNumber) - } - if rf, ok := ret.Get(0).(func(context.Context, ethereum.CallMsg, *big.Int) []byte); ok { - r0 = rf(ctx, msg, blockNumber) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]byte) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, ethereum.CallMsg, *big.Int) error); ok { - r1 = rf(ctx, msg, blockNumber) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// ChainID provides a mock function with given fields: -func (_m *Node) ChainID() *big.Int { - ret := _m.Called() - - if len(ret) == 0 { - panic("no return value specified for ChainID") - } - - var r0 *big.Int - if rf, ok := ret.Get(0).(func() *big.Int); ok { - r0 = rf() - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*big.Int) - } - } - - return r0 -} - -// Close provides a mock function with given fields: -func (_m *Node) Close() error { - ret := _m.Called() - - if len(ret) == 0 { - panic("no return value specified for Close") - } - - var r0 error - if rf, ok := ret.Get(0).(func() error); ok { - r0 = rf() - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// CodeAt provides a mock function with given fields: ctx, account, blockNumber -func (_m *Node) CodeAt(ctx context.Context, account common.Address, blockNumber *big.Int) ([]byte, error) { - ret := _m.Called(ctx, account, blockNumber) - - if len(ret) == 0 { - panic("no return value specified for CodeAt") - } - - var r0 []byte - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, common.Address, *big.Int) ([]byte, error)); ok { - return rf(ctx, account, blockNumber) - } - if rf, ok := ret.Get(0).(func(context.Context, common.Address, *big.Int) []byte); ok { - r0 = rf(ctx, account, blockNumber) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]byte) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, common.Address, *big.Int) error); ok { - r1 = rf(ctx, account, blockNumber) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// EstimateGas provides a mock function with given fields: ctx, call -func (_m *Node) EstimateGas(ctx context.Context, call ethereum.CallMsg) (uint64, error) { - ret := _m.Called(ctx, call) - - if len(ret) == 0 { - panic("no return value specified for EstimateGas") - } - - var r0 uint64 - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, ethereum.CallMsg) (uint64, error)); ok { - return rf(ctx, call) - } - if rf, ok := ret.Get(0).(func(context.Context, ethereum.CallMsg) uint64); ok { - r0 = rf(ctx, call) - } else { - r0 = ret.Get(0).(uint64) - } - - if rf, ok := ret.Get(1).(func(context.Context, ethereum.CallMsg) error); ok { - r1 = rf(ctx, call) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// EthSubscribe provides a mock function with given fields: ctx, channel, args -func (_m *Node) EthSubscribe(ctx context.Context, channel chan<- *evmtypes.Head, args ...interface{}) (ethereum.Subscription, error) { - var _ca []interface{} - _ca = append(_ca, ctx, channel) - _ca = append(_ca, args...) - ret := _m.Called(_ca...) - - if len(ret) == 0 { - panic("no return value specified for EthSubscribe") - } - - var r0 ethereum.Subscription - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, chan<- *evmtypes.Head, ...interface{}) (ethereum.Subscription, error)); ok { - return rf(ctx, channel, args...) - } - if rf, ok := ret.Get(0).(func(context.Context, chan<- *evmtypes.Head, ...interface{}) ethereum.Subscription); ok { - r0 = rf(ctx, channel, args...) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(ethereum.Subscription) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, chan<- *evmtypes.Head, ...interface{}) error); ok { - r1 = rf(ctx, channel, args...) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// FilterLogs provides a mock function with given fields: ctx, q -func (_m *Node) FilterLogs(ctx context.Context, q ethereum.FilterQuery) ([]types.Log, error) { - ret := _m.Called(ctx, q) - - if len(ret) == 0 { - panic("no return value specified for FilterLogs") - } - - var r0 []types.Log - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, ethereum.FilterQuery) ([]types.Log, error)); ok { - return rf(ctx, q) - } - if rf, ok := ret.Get(0).(func(context.Context, ethereum.FilterQuery) []types.Log); ok { - r0 = rf(ctx, q) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]types.Log) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, ethereum.FilterQuery) error); ok { - r1 = rf(ctx, q) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// HeaderByHash provides a mock function with given fields: _a0, _a1 -func (_m *Node) HeaderByHash(_a0 context.Context, _a1 common.Hash) (*types.Header, error) { - ret := _m.Called(_a0, _a1) - - if len(ret) == 0 { - panic("no return value specified for HeaderByHash") - } - - var r0 *types.Header - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, common.Hash) (*types.Header, error)); ok { - return rf(_a0, _a1) - } - if rf, ok := ret.Get(0).(func(context.Context, common.Hash) *types.Header); ok { - r0 = rf(_a0, _a1) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*types.Header) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, common.Hash) error); ok { - r1 = rf(_a0, _a1) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// HeaderByNumber provides a mock function with given fields: _a0, _a1 -func (_m *Node) HeaderByNumber(_a0 context.Context, _a1 *big.Int) (*types.Header, error) { - ret := _m.Called(_a0, _a1) - - if len(ret) == 0 { - panic("no return value specified for HeaderByNumber") - } - - var r0 *types.Header - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, *big.Int) (*types.Header, error)); ok { - return rf(_a0, _a1) - } - if rf, ok := ret.Get(0).(func(context.Context, *big.Int) *types.Header); ok { - r0 = rf(_a0, _a1) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*types.Header) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, *big.Int) error); ok { - r1 = rf(_a0, _a1) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// Name provides a mock function with given fields: -func (_m *Node) Name() string { - ret := _m.Called() - - if len(ret) == 0 { - panic("no return value specified for Name") - } - - var r0 string - if rf, ok := ret.Get(0).(func() string); ok { - r0 = rf() - } else { - r0 = ret.Get(0).(string) - } - - return r0 -} - -// NonceAt provides a mock function with given fields: ctx, account, blockNumber -func (_m *Node) NonceAt(ctx context.Context, account common.Address, blockNumber *big.Int) (uint64, error) { - ret := _m.Called(ctx, account, blockNumber) - - if len(ret) == 0 { - panic("no return value specified for NonceAt") - } - - var r0 uint64 - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, common.Address, *big.Int) (uint64, error)); ok { - return rf(ctx, account, blockNumber) - } - if rf, ok := ret.Get(0).(func(context.Context, common.Address, *big.Int) uint64); ok { - r0 = rf(ctx, account, blockNumber) - } else { - r0 = ret.Get(0).(uint64) - } - - if rf, ok := ret.Get(1).(func(context.Context, common.Address, *big.Int) error); ok { - r1 = rf(ctx, account, blockNumber) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// Order provides a mock function with given fields: -func (_m *Node) Order() int32 { - ret := _m.Called() - - if len(ret) == 0 { - panic("no return value specified for Order") - } - - var r0 int32 - if rf, ok := ret.Get(0).(func() int32); ok { - r0 = rf() - } else { - r0 = ret.Get(0).(int32) - } - - return r0 -} - -// PendingCallContract provides a mock function with given fields: ctx, msg -func (_m *Node) PendingCallContract(ctx context.Context, msg ethereum.CallMsg) ([]byte, error) { - ret := _m.Called(ctx, msg) - - if len(ret) == 0 { - panic("no return value specified for PendingCallContract") - } - - var r0 []byte - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, ethereum.CallMsg) ([]byte, error)); ok { - return rf(ctx, msg) - } - if rf, ok := ret.Get(0).(func(context.Context, ethereum.CallMsg) []byte); ok { - r0 = rf(ctx, msg) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]byte) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, ethereum.CallMsg) error); ok { - r1 = rf(ctx, msg) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// PendingCodeAt provides a mock function with given fields: ctx, account -func (_m *Node) PendingCodeAt(ctx context.Context, account common.Address) ([]byte, error) { - ret := _m.Called(ctx, account) - - if len(ret) == 0 { - panic("no return value specified for PendingCodeAt") - } - - var r0 []byte - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, common.Address) ([]byte, error)); ok { - return rf(ctx, account) - } - if rf, ok := ret.Get(0).(func(context.Context, common.Address) []byte); ok { - r0 = rf(ctx, account) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]byte) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, common.Address) error); ok { - r1 = rf(ctx, account) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// PendingNonceAt provides a mock function with given fields: ctx, account -func (_m *Node) PendingNonceAt(ctx context.Context, account common.Address) (uint64, error) { - ret := _m.Called(ctx, account) - - if len(ret) == 0 { - panic("no return value specified for PendingNonceAt") - } - - var r0 uint64 - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, common.Address) (uint64, error)); ok { - return rf(ctx, account) - } - if rf, ok := ret.Get(0).(func(context.Context, common.Address) uint64); ok { - r0 = rf(ctx, account) - } else { - r0 = ret.Get(0).(uint64) - } - - if rf, ok := ret.Get(1).(func(context.Context, common.Address) error); ok { - r1 = rf(ctx, account) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// SendTransaction provides a mock function with given fields: ctx, tx -func (_m *Node) SendTransaction(ctx context.Context, tx *types.Transaction) error { - ret := _m.Called(ctx, tx) - - if len(ret) == 0 { - panic("no return value specified for SendTransaction") - } - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, *types.Transaction) error); ok { - r0 = rf(ctx, tx) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// Start provides a mock function with given fields: ctx -func (_m *Node) Start(ctx context.Context) error { - ret := _m.Called(ctx) - - if len(ret) == 0 { - panic("no return value specified for Start") - } - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context) error); ok { - r0 = rf(ctx) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// State provides a mock function with given fields: -func (_m *Node) State() client.NodeState { - ret := _m.Called() - - if len(ret) == 0 { - panic("no return value specified for State") - } - - var r0 client.NodeState - if rf, ok := ret.Get(0).(func() client.NodeState); ok { - r0 = rf() - } else { - r0 = ret.Get(0).(client.NodeState) - } - - return r0 -} - -// StateAndLatest provides a mock function with given fields: -func (_m *Node) StateAndLatest() (client.NodeState, int64, *big.Int) { - ret := _m.Called() - - if len(ret) == 0 { - panic("no return value specified for StateAndLatest") - } - - var r0 client.NodeState - var r1 int64 - var r2 *big.Int - if rf, ok := ret.Get(0).(func() (client.NodeState, int64, *big.Int)); ok { - return rf() - } - if rf, ok := ret.Get(0).(func() client.NodeState); ok { - r0 = rf() - } else { - r0 = ret.Get(0).(client.NodeState) - } - - if rf, ok := ret.Get(1).(func() int64); ok { - r1 = rf() - } else { - r1 = ret.Get(1).(int64) - } - - if rf, ok := ret.Get(2).(func() *big.Int); ok { - r2 = rf() - } else { - if ret.Get(2) != nil { - r2 = ret.Get(2).(*big.Int) - } - } - - return r0, r1, r2 -} - -// String provides a mock function with given fields: -func (_m *Node) String() string { - ret := _m.Called() - - if len(ret) == 0 { - panic("no return value specified for String") - } - - var r0 string - if rf, ok := ret.Get(0).(func() string); ok { - r0 = rf() - } else { - r0 = ret.Get(0).(string) - } - - return r0 -} - -// SubscribeFilterLogs provides a mock function with given fields: ctx, q, ch -func (_m *Node) SubscribeFilterLogs(ctx context.Context, q ethereum.FilterQuery, ch chan<- types.Log) (ethereum.Subscription, error) { - ret := _m.Called(ctx, q, ch) - - if len(ret) == 0 { - panic("no return value specified for SubscribeFilterLogs") - } - - var r0 ethereum.Subscription - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, ethereum.FilterQuery, chan<- types.Log) (ethereum.Subscription, error)); ok { - return rf(ctx, q, ch) - } - if rf, ok := ret.Get(0).(func(context.Context, ethereum.FilterQuery, chan<- types.Log) ethereum.Subscription); ok { - r0 = rf(ctx, q, ch) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(ethereum.Subscription) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, ethereum.FilterQuery, chan<- types.Log) error); ok { - r1 = rf(ctx, q, ch) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// SubscribersCount provides a mock function with given fields: -func (_m *Node) SubscribersCount() int32 { - ret := _m.Called() - - if len(ret) == 0 { - panic("no return value specified for SubscribersCount") - } - - var r0 int32 - if rf, ok := ret.Get(0).(func() int32); ok { - r0 = rf() - } else { - r0 = ret.Get(0).(int32) - } - - return r0 -} - -// SuggestGasPrice provides a mock function with given fields: ctx -func (_m *Node) SuggestGasPrice(ctx context.Context) (*big.Int, error) { - ret := _m.Called(ctx) - - if len(ret) == 0 { - panic("no return value specified for SuggestGasPrice") - } - - var r0 *big.Int - var r1 error - if rf, ok := ret.Get(0).(func(context.Context) (*big.Int, error)); ok { - return rf(ctx) - } - if rf, ok := ret.Get(0).(func(context.Context) *big.Int); ok { - r0 = rf(ctx) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*big.Int) - } - } - - if rf, ok := ret.Get(1).(func(context.Context) error); ok { - r1 = rf(ctx) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// SuggestGasTipCap provides a mock function with given fields: ctx -func (_m *Node) SuggestGasTipCap(ctx context.Context) (*big.Int, error) { - ret := _m.Called(ctx) - - if len(ret) == 0 { - panic("no return value specified for SuggestGasTipCap") - } - - var r0 *big.Int - var r1 error - if rf, ok := ret.Get(0).(func(context.Context) (*big.Int, error)); ok { - return rf(ctx) - } - if rf, ok := ret.Get(0).(func(context.Context) *big.Int); ok { - r0 = rf(ctx) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*big.Int) - } - } - - if rf, ok := ret.Get(1).(func(context.Context) error); ok { - r1 = rf(ctx) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// TransactionByHash provides a mock function with given fields: ctx, txHash -func (_m *Node) TransactionByHash(ctx context.Context, txHash common.Hash) (*types.Transaction, error) { - ret := _m.Called(ctx, txHash) - - if len(ret) == 0 { - panic("no return value specified for TransactionByHash") - } - - var r0 *types.Transaction - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, common.Hash) (*types.Transaction, error)); ok { - return rf(ctx, txHash) - } - if rf, ok := ret.Get(0).(func(context.Context, common.Hash) *types.Transaction); ok { - r0 = rf(ctx, txHash) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*types.Transaction) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, common.Hash) error); ok { - r1 = rf(ctx, txHash) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// TransactionReceipt provides a mock function with given fields: ctx, txHash -func (_m *Node) TransactionReceipt(ctx context.Context, txHash common.Hash) (*types.Receipt, error) { - ret := _m.Called(ctx, txHash) - - if len(ret) == 0 { - panic("no return value specified for TransactionReceipt") - } - - var r0 *types.Receipt - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, common.Hash) (*types.Receipt, error)); ok { - return rf(ctx, txHash) - } - if rf, ok := ret.Get(0).(func(context.Context, common.Hash) *types.Receipt); ok { - r0 = rf(ctx, txHash) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*types.Receipt) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, common.Hash) error); ok { - r1 = rf(ctx, txHash) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// UnsubscribeAllExceptAliveLoop provides a mock function with given fields: -func (_m *Node) UnsubscribeAllExceptAliveLoop() { - _m.Called() -} - -// NewNode creates a new instance of Node. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -// The first argument is typically a *testing.T value. -func NewNode(t interface { - mock.TestingT - Cleanup(func()) -}) *Node { - mock := &Node{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/core/chains/evm/mocks/send_only_node.go b/core/chains/evm/mocks/send_only_node.go deleted file mode 100644 index e0ab9775be9..00000000000 --- a/core/chains/evm/mocks/send_only_node.go +++ /dev/null @@ -1,181 +0,0 @@ -// Code generated by mockery v2.42.2. DO NOT EDIT. - -package mocks - -import ( - context "context" - big "math/big" - - client "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client" - - mock "github.com/stretchr/testify/mock" - - rpc "github.com/ethereum/go-ethereum/rpc" - - types "github.com/ethereum/go-ethereum/core/types" -) - -// SendOnlyNode is an autogenerated mock type for the SendOnlyNode type -type SendOnlyNode struct { - mock.Mock -} - -// BatchCallContext provides a mock function with given fields: ctx, b -func (_m *SendOnlyNode) BatchCallContext(ctx context.Context, b []rpc.BatchElem) error { - ret := _m.Called(ctx, b) - - if len(ret) == 0 { - panic("no return value specified for BatchCallContext") - } - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, []rpc.BatchElem) error); ok { - r0 = rf(ctx, b) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// ChainID provides a mock function with given fields: -func (_m *SendOnlyNode) ChainID() *big.Int { - ret := _m.Called() - - if len(ret) == 0 { - panic("no return value specified for ChainID") - } - - var r0 *big.Int - if rf, ok := ret.Get(0).(func() *big.Int); ok { - r0 = rf() - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*big.Int) - } - } - - return r0 -} - -// Close provides a mock function with given fields: -func (_m *SendOnlyNode) Close() error { - ret := _m.Called() - - if len(ret) == 0 { - panic("no return value specified for Close") - } - - var r0 error - if rf, ok := ret.Get(0).(func() error); ok { - r0 = rf() - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// Name provides a mock function with given fields: -func (_m *SendOnlyNode) Name() string { - ret := _m.Called() - - if len(ret) == 0 { - panic("no return value specified for Name") - } - - var r0 string - if rf, ok := ret.Get(0).(func() string); ok { - r0 = rf() - } else { - r0 = ret.Get(0).(string) - } - - return r0 -} - -// SendTransaction provides a mock function with given fields: ctx, tx -func (_m *SendOnlyNode) SendTransaction(ctx context.Context, tx *types.Transaction) error { - ret := _m.Called(ctx, tx) - - if len(ret) == 0 { - panic("no return value specified for SendTransaction") - } - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, *types.Transaction) error); ok { - r0 = rf(ctx, tx) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// Start provides a mock function with given fields: _a0 -func (_m *SendOnlyNode) Start(_a0 context.Context) error { - ret := _m.Called(_a0) - - if len(ret) == 0 { - panic("no return value specified for Start") - } - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context) error); ok { - r0 = rf(_a0) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// State provides a mock function with given fields: -func (_m *SendOnlyNode) State() client.NodeState { - ret := _m.Called() - - if len(ret) == 0 { - panic("no return value specified for State") - } - - var r0 client.NodeState - if rf, ok := ret.Get(0).(func() client.NodeState); ok { - r0 = rf() - } else { - r0 = ret.Get(0).(client.NodeState) - } - - return r0 -} - -// String provides a mock function with given fields: -func (_m *SendOnlyNode) String() string { - ret := _m.Called() - - if len(ret) == 0 { - panic("no return value specified for String") - } - - var r0 string - if rf, ok := ret.Get(0).(func() string); ok { - r0 = rf() - } else { - r0 = ret.Get(0).(string) - } - - return r0 -} - -// NewSendOnlyNode creates a new instance of SendOnlyNode. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -// The first argument is typically a *testing.T value. -func NewSendOnlyNode(t interface { - mock.TestingT - Cleanup(func()) -}) *SendOnlyNode { - mock := &SendOnlyNode{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/core/services/chainlink/config_test.go b/core/services/chainlink/config_test.go index e819993d42b..c2b2e288039 100644 --- a/core/services/chainlink/config_test.go +++ b/core/services/chainlink/config_test.go @@ -25,10 +25,10 @@ import ( solcfg "github.com/smartcontractkit/chainlink-solana/pkg/solana/config" stkcfg "github.com/smartcontractkit/chainlink-starknet/relayer/pkg/chainlink/config" + "github.com/smartcontractkit/chainlink/v2/common/client" commonconfig "github.com/smartcontractkit/chainlink/v2/common/config" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/assets" - "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client" evmcfg "github.com/smartcontractkit/chainlink/v2/core/chains/evm/config/toml" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/types" ubig "github.com/smartcontractkit/chainlink/v2/core/chains/evm/utils/big" @@ -215,7 +215,7 @@ func TestConfig_Marshal(t *testing.T) { require.NoError(t, err) return &a } - selectionMode := client.NodeSelectionMode_HighestHead + selectionMode := client.NodeSelectionModeHighestHead global := Config{ Core: toml.Core{