diff --git a/CONFIG.md b/CONFIG.md index b0950b2931..e3eba07b14 100644 --- a/CONFIG.md +++ b/CONFIG.md @@ -934,6 +934,7 @@ GasEstimator.PriceMax overrides the maximum gas price for this key. See EVM.GasE ```toml [NodePool] PollFailureThreshold = 5 # Default +PollSuccessThreshold = 0 # Default PollInterval = '10s' # Default SelectionMode = 'HighestHead' # Default SyncThreshold = 5 # Default @@ -958,6 +959,14 @@ PollFailureThreshold indicates how many consecutive polls must fail in order to Set to zero to disable poll checking. +### PollSuccessThreshold +```toml +PollSuccessThreshold = 0 # Default +``` +PollSuccessThreshold indicates how many consecutive polls must succeed in order to mark a node as alive once it has been marked as unreachable. + +Set to zero to require no successful polls (previous behavior). + ### PollInterval ```toml PollInterval = '10s' # Default diff --git a/go.mod b/go.mod index 0c40542b44..e388fe6637 100644 --- a/go.mod +++ b/go.mod @@ -31,10 +31,10 @@ require ( github.com/smartcontractkit/chainlink-common/keystore v1.0.2 github.com/smartcontractkit/chainlink-data-streams v0.1.12-0.20260227110503-42b236799872 github.com/smartcontractkit/chainlink-evm/gethwrappers v0.0.0-20251022073203-7d8ae8cf67c1 - github.com/smartcontractkit/chainlink-framework/capabilities v0.0.0-20250818175541-3389ac08a563 - github.com/smartcontractkit/chainlink-framework/chains v0.0.0-20260326122810-b657beadfb57 - github.com/smartcontractkit/chainlink-framework/metrics v0.0.0-20260401162955-be2bc6b5264b - github.com/smartcontractkit/chainlink-framework/multinode v0.0.0-20260410144512-ca02ad6ed16a + github.com/smartcontractkit/chainlink-framework/capabilities v0.0.0-20260423135514-5b1a7565a99c + github.com/smartcontractkit/chainlink-framework/chains v0.0.0-20260423135514-5b1a7565a99c + github.com/smartcontractkit/chainlink-framework/metrics v0.0.0-20260423135514-5b1a7565a99c + github.com/smartcontractkit/chainlink-framework/multinode v0.0.0-20260423135514-5b1a7565a99c github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260326111235-8c09d1a4491f github.com/smartcontractkit/chainlink-protos/svr v1.1.1-0.20260203131522-bb8bc5c423b3 github.com/smartcontractkit/chainlink-tron/relayer v0.0.11-0.20250815105909-75499abc4335 diff --git a/go.sum b/go.sum index f41b6faeb7..62cb4936ad 100644 --- a/go.sum +++ b/go.sum @@ -662,15 +662,14 @@ github.com/smartcontractkit/chainlink-data-streams v0.1.12-0.20260227110503-42b2 github.com/smartcontractkit/chainlink-data-streams v0.1.12-0.20260227110503-42b236799872/go.mod h1:5jROIH/4cgHBQn875A+E2DCqvkBtrkHs+ciedcGTjNI= github.com/smartcontractkit/chainlink-evm/gethwrappers v0.0.0-20251022073203-7d8ae8cf67c1 h1:NTODgwAil7BLoijS7y6KnEuNbQ9v60VUhIR9FcAzIhg= github.com/smartcontractkit/chainlink-evm/gethwrappers v0.0.0-20251022073203-7d8ae8cf67c1/go.mod h1:oyfOm4k0uqmgZIfxk1elI/59B02shbbJQiiUdPdbMgI= -github.com/smartcontractkit/chainlink-framework/capabilities v0.0.0-20250818175541-3389ac08a563 h1:ACpDbAxG4fa4sA83dbtYcrnlpE/y7thNIZfHxTv2ZLs= -github.com/smartcontractkit/chainlink-framework/capabilities v0.0.0-20250818175541-3389ac08a563/go.mod h1:jP5mrOLFEYZZkl7EiCHRRIMSSHCQsYypm1OZSus//iI= -github.com/smartcontractkit/chainlink-framework/chains v0.0.0-20260326122810-b657beadfb57 h1:sCrr1Oy/JZstf/Oi2cRuU4mDN1BRUKfXP2CKByCMADg= -github.com/smartcontractkit/chainlink-framework/chains v0.0.0-20260326122810-b657beadfb57/go.mod h1:kGprqyjsz6qFNVszOQoHc24wfvCjyipNZFste/3zcbs= -github.com/smartcontractkit/chainlink-framework/metrics v0.0.0-20260401162955-be2bc6b5264b h1:L1So1EDBDRET3j/TdV1Gjv3qWARoa/NPRaU7k4r30yA= -github.com/smartcontractkit/chainlink-framework/metrics v0.0.0-20260401162955-be2bc6b5264b/go.mod h1:HG/aei0MgBOpsyRLexdKGtOUO8yjSJO3iUu0Uu8KBm4= -github.com/smartcontractkit/chainlink-framework/multinode v0.0.0-20260410144512-ca02ad6ed16a h1:PsFckZp3Dhb5pVc0Xccj1lvnOEg0H3eQdjtZgnCKd+4= -github.com/smartcontractkit/chainlink-framework/multinode v0.0.0-20260410144512-ca02ad6ed16a/go.mod h1:7ketk4ischPQW/JQgmyHz6zdzLUJv1VC29SiSgosydQ= - +github.com/smartcontractkit/chainlink-framework/capabilities v0.0.0-20260423135514-5b1a7565a99c h1:AYRSQarVw1EJXUrGvHSwmRTtNHHww/i3xwLat5CshUE= +github.com/smartcontractkit/chainlink-framework/capabilities v0.0.0-20260423135514-5b1a7565a99c/go.mod h1:HcwehCao5k5C2NGuKJUVoX/AYtoH6njGFiV44dBOcY4= +github.com/smartcontractkit/chainlink-framework/chains v0.0.0-20260423135514-5b1a7565a99c h1:0c+bCKo47vy/ItRtGa3S/vCpE5LRlgXpGnVKQX8TgjE= +github.com/smartcontractkit/chainlink-framework/chains v0.0.0-20260423135514-5b1a7565a99c/go.mod h1:kGprqyjsz6qFNVszOQoHc24wfvCjyipNZFste/3zcbs= +github.com/smartcontractkit/chainlink-framework/metrics v0.0.0-20260423135514-5b1a7565a99c h1:1VVreRcffo3N3zF1JVtgS+YC4puuj3y0FU0Fta7L3U0= +github.com/smartcontractkit/chainlink-framework/metrics v0.0.0-20260423135514-5b1a7565a99c/go.mod h1:HG/aei0MgBOpsyRLexdKGtOUO8yjSJO3iUu0Uu8KBm4= +github.com/smartcontractkit/chainlink-framework/multinode v0.0.0-20260423135514-5b1a7565a99c h1:N5cZI93lPH0oYnVSP9dq72QwKnrW0gBpEmx2MI1ermg= +github.com/smartcontractkit/chainlink-framework/multinode v0.0.0-20260423135514-5b1a7565a99c/go.mod h1:7ketk4ischPQW/JQgmyHz6zdzLUJv1VC29SiSgosydQ= github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260326111235-8c09d1a4491f h1:8p3vE987AHM3Of1JvnNJXNE/AtWtfNvJhk3TeeAG3Qw= github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260326111235-8c09d1a4491f/go.mod h1:Jqt53s27Tr0jDl8mdBXg1xhu6F8Fci8JOuq43tgHOM8= github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20251002192024-d2ad9222409b h1:QuI6SmQFK/zyUlVWEf0GMkiUYBPY4lssn26nKSd/bOM= diff --git a/pkg/client/config_builder.go b/pkg/client/config_builder.go index e12af0d580..05bc216830 100644 --- a/pkg/client/config_builder.go +++ b/pkg/client/config_builder.go @@ -32,6 +32,7 @@ func NewClientConfigs( chainType string, nodeCfgs []NodeConfig, pollFailureThreshold *uint32, + pollSuccessThreshold *uint32, pollInterval time.Duration, syncThreshold *uint32, nodeIsSyncingEnabled *bool, @@ -56,6 +57,7 @@ func NewClientConfigs( SelectionMode: selectionMode, LeaseDuration: commonconfig.MustNewDuration(leaseDuration), PollFailureThreshold: pollFailureThreshold, + PollSuccessThreshold: pollSuccessThreshold, PollInterval: commonconfig.MustNewDuration(pollInterval), SyncThreshold: syncThreshold, NodeIsSyncingEnabled: nodeIsSyncingEnabled, diff --git a/pkg/client/config_builder_test.go b/pkg/client/config_builder_test.go index 8f04472ae2..7f5f602e00 100644 --- a/pkg/client/config_builder_test.go +++ b/pkg/client/config_builder_test.go @@ -19,6 +19,7 @@ func TestClientConfigBuilder(t *testing.T) { selectionMode := ptr("HighestHead") leaseDuration := 0 * time.Second pollFailureThreshold := ptr(uint32(5)) + pollSuccessThreshold := ptr(uint32(3)) pollInterval := 10 * time.Second syncThreshold := ptr(uint32(5)) nodeIsSyncingEnabled := ptr(false) @@ -42,7 +43,7 @@ func TestClientConfigBuilder(t *testing.T) { noNewHeadsThreshold := time.Second newHeadsPollInterval := 0 * time.Second chainCfg, nodePool, nodes, err := client.NewClientConfigs(selectionMode, leaseDuration, chainTypeStr, nodeConfigs, - pollFailureThreshold, pollInterval, syncThreshold, nodeIsSyncingEnabled, noNewHeadsThreshold, finalityDepth, + pollFailureThreshold, pollSuccessThreshold, pollInterval, syncThreshold, nodeIsSyncingEnabled, noNewHeadsThreshold, finalityDepth, finalityTagEnabled, SafeTagSupported, finalizedBlockOffset, enforceRepeatableRead, deathDeclarationDelay, noNewFinalizedBlocksThreshold, pollInterval, newHeadsPollInterval, confirmationTimeout, safeDepth) require.NoError(t, err) @@ -51,6 +52,7 @@ func TestClientConfigBuilder(t *testing.T) { require.Equal(t, *selectionMode, nodePool.SelectionMode()) require.Equal(t, leaseDuration, nodePool.LeaseDuration()) require.Equal(t, *pollFailureThreshold, nodePool.PollFailureThreshold()) + require.Equal(t, *pollSuccessThreshold, nodePool.PollSuccessThreshold()) require.Equal(t, pollInterval, nodePool.PollInterval()) require.Equal(t, *syncThreshold, nodePool.SyncThreshold()) require.Equal(t, *nodeIsSyncingEnabled, nodePool.NodeIsSyncingEnabled()) diff --git a/pkg/client/evm_client_test.go b/pkg/client/evm_client_test.go index f6e8deebfe..1ebfc71929 100644 --- a/pkg/client/evm_client_test.go +++ b/pkg/client/evm_client_test.go @@ -29,6 +29,7 @@ func TestNewEvmClient(t *testing.T) { selectionMode := ptr("HighestHead") leaseDuration := 0 * time.Second pollFailureThreshold := ptr(uint32(5)) + pollSuccessThreshold := ptr(uint32(0)) pollInterval := 10 * time.Second syncThreshold := ptr(uint32(5)) nodeIsSyncingEnabled := ptr(false) @@ -52,7 +53,7 @@ func TestNewEvmClient(t *testing.T) { finalityTagEnabled := ptr(true) SafeTagSupported := ptr(true) chainCfg, nodePool, nodes, err := client.NewClientConfigs(selectionMode, leaseDuration, chainTypeStr, nodeConfigs, - pollFailureThreshold, pollInterval, syncThreshold, nodeIsSyncingEnabled, noNewHeadsThreshold, finalityDepth, + pollFailureThreshold, pollSuccessThreshold, pollInterval, syncThreshold, nodeIsSyncingEnabled, noNewHeadsThreshold, finalityDepth, finalityTagEnabled, SafeTagSupported, finalizedBlockOffset, enforceRepeatableRead, deathDeclarationDelay, noNewFinalizedBlocksThreshold, finalizedBlockPollInterval, newHeadsPollInterval, confirmationTimeout, safeDepth) require.NoError(t, err) @@ -79,7 +80,7 @@ func TestChainClientMetrics(t *testing.T) { }, } chainCfg, nodePool, nodes, err := client.NewClientConfigs(ptr("HighestHead"), time.Duration(0), "", nodeConfigs, - ptr[uint32](5), 10*time.Second, ptr[uint32](5), ptr(false), time.Minute, ptr[uint32](5), ptr(false), ptr(false), + ptr[uint32](5), ptr[uint32](0), 10*time.Second, ptr[uint32](5), ptr(false), time.Minute, ptr[uint32](5), ptr(false), ptr(false), ptr[uint32](5), ptr(false), 10*time.Second, 10*time.Second, 10*time.Second, 10*time.Second, 60*time.Second, ptr[uint32](10)) require.NoError(t, err) diff --git a/pkg/client/helpers_test.go b/pkg/client/helpers_test.go index 7734ea1bc6..848f500a63 100644 --- a/pkg/client/helpers_test.go +++ b/pkg/client/helpers_test.go @@ -89,6 +89,7 @@ func (c *TestClientErrors) MissingBlocks() string { return c.missingBl type TestNodePoolConfig struct { NodePollFailureThreshold uint32 + NodePollSuccessThreshold uint32 NodePollInterval time.Duration NodeSelectionMode string NodeSyncThreshold uint32 @@ -103,6 +104,7 @@ type TestNodePoolConfig struct { } func (tc TestNodePoolConfig) PollFailureThreshold() uint32 { return tc.NodePollFailureThreshold } +func (tc TestNodePoolConfig) PollSuccessThreshold() uint32 { return tc.NodePollSuccessThreshold } func (tc TestNodePoolConfig) PollInterval() time.Duration { return tc.NodePollInterval } func (tc TestNodePoolConfig) SelectionMode() string { return tc.NodeSelectionMode } func (tc TestNodePoolConfig) SyncThreshold() uint32 { return tc.NodeSyncThreshold } diff --git a/pkg/client/rpc_client.go b/pkg/client/rpc_client.go index 7fa52e3552..32a7328ee4 100644 --- a/pkg/client/rpc_client.go +++ b/pkg/client/rpc_client.go @@ -834,12 +834,12 @@ func (r *RPCClient) SendTransaction(ctx context.Context, tx *types.Transaction) lggr := r.newRqLggr().With("tx", tx) lggr.Debug("RPC call: evmclient.Client#SendTransaction") - start := time.Now() if r.isChainType(chaintype.ChainTron) { err := errors.New("SendTransaction not implemented for Tron, this should never be called") return struct{}{}, multinode.Fatal, err } + start := time.Now() err := r.wrapRPCClientError(client.geth.SendTransaction(ctx, tx)) duration := time.Since(start) diff --git a/pkg/config/chain_scoped_node_pool.go b/pkg/config/chain_scoped_node_pool.go index d9cd17ee72..cf4acae2f5 100644 --- a/pkg/config/chain_scoped_node_pool.go +++ b/pkg/config/chain_scoped_node_pool.go @@ -14,6 +14,10 @@ func (n *NodePoolConfig) PollFailureThreshold() uint32 { return *n.C.PollFailureThreshold } +func (n *NodePoolConfig) PollSuccessThreshold() uint32 { + return *n.C.PollSuccessThreshold +} + func (n *NodePoolConfig) PollInterval() time.Duration { return n.C.PollInterval.Duration() } diff --git a/pkg/config/config.go b/pkg/config/config.go index a2ab49364d..2f0d1f110c 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -206,6 +206,7 @@ type Workflow interface { type NodePool interface { PollFailureThreshold() uint32 + PollSuccessThreshold() uint32 PollInterval() time.Duration SelectionMode() string SyncThreshold() uint32 diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index 313f50a88e..789a10d0e2 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -366,6 +366,7 @@ func TestNodePoolConfig(t *testing.T) { require.Equal(t, uint32(5), cfg.EVM().NodePool().SyncThreshold()) require.Equal(t, time.Duration(10000000000), cfg.EVM().NodePool().PollInterval()) require.Equal(t, uint32(5), cfg.EVM().NodePool().PollFailureThreshold()) + require.Equal(t, uint32(0), cfg.EVM().NodePool().PollSuccessThreshold()) require.False(t, cfg.EVM().NodePool().NodeIsSyncingEnabled()) require.True(t, cfg.EVM().NodePool().EnforceRepeatableRead()) require.Equal(t, time.Minute, cfg.EVM().NodePool().DeathDeclarationDelay()) diff --git a/pkg/config/toml/config.go b/pkg/config/toml/config.go index 9b4bb17cf8..f062fadb05 100644 --- a/pkg/config/toml/config.go +++ b/pkg/config/toml/config.go @@ -1096,6 +1096,7 @@ func (r *ClientErrors) setFrom(f *ClientErrors) bool { type NodePool struct { PollFailureThreshold *uint32 + PollSuccessThreshold *uint32 PollInterval *commonconfig.Duration SelectionMode *string SyncThreshold *uint32 @@ -1114,6 +1115,9 @@ func (p *NodePool) setFrom(f *NodePool) { if v := f.PollFailureThreshold; v != nil { p.PollFailureThreshold = v } + if v := f.PollSuccessThreshold; v != nil { + p.PollSuccessThreshold = v + } if v := f.PollInterval; v != nil { p.PollInterval = v } diff --git a/pkg/config/toml/config_test.go b/pkg/config/toml/config_test.go index 996b91ea80..d5728bb995 100644 --- a/pkg/config/toml/config_test.go +++ b/pkg/config/toml/config_test.go @@ -328,6 +328,7 @@ var fullConfig = EVMConfig{ NodePool: NodePool{ PollFailureThreshold: ptr[uint32](5), + PollSuccessThreshold: ptr[uint32](0), PollInterval: config.MustNewDuration(time.Minute), SelectionMode: ptr(multinode.NodeSelectionModeHighestHead), SyncThreshold: ptr[uint32](13), diff --git a/pkg/config/toml/defaults/Blast_Mainnet.toml b/pkg/config/toml/defaults/Blast_Mainnet.toml index 65600dc710..f5fa3dface 100644 --- a/pkg/config/toml/defaults/Blast_Mainnet.toml +++ b/pkg/config/toml/defaults/Blast_Mainnet.toml @@ -20,6 +20,7 @@ HistoryDepth = 300 [NodePool] # 4 block sync time between nodes to ensure they aren't labelled unreachable too soon PollFailureThreshold = 4 +PollSuccessThreshold = 0 # polls every 4sec to check if there is a block produced, since blockRate is ~3sec PollInterval = '4s' diff --git a/pkg/config/toml/defaults/Blast_Sepolia.toml b/pkg/config/toml/defaults/Blast_Sepolia.toml index 77953a3ac7..3bde0acff5 100644 --- a/pkg/config/toml/defaults/Blast_Sepolia.toml +++ b/pkg/config/toml/defaults/Blast_Sepolia.toml @@ -20,6 +20,7 @@ HistoryDepth = 300 [NodePool] # 4 block sync time between nodes to ensure they aren't labelled unreachable too soon PollFailureThreshold = 4 +PollSuccessThreshold = 0 # polls every 4sec to check if there is a block produced, since blockRate is ~3sec PollInterval = '4s' diff --git a/pkg/config/toml/defaults/Ethereum_Holesky.toml b/pkg/config/toml/defaults/Ethereum_Holesky.toml index fe782cb876..cde697ed21 100644 --- a/pkg/config/toml/defaults/Ethereum_Holesky.toml +++ b/pkg/config/toml/defaults/Ethereum_Holesky.toml @@ -9,4 +9,5 @@ FeeCapDefault = '1000 gwei' [NodePool] PollFailureThreshold = 2 +PollSuccessThreshold = 0 PollInterval = '3s' diff --git a/pkg/config/toml/defaults/Hashkey_Mainnet.toml b/pkg/config/toml/defaults/Hashkey_Mainnet.toml index 2ba3766a00..95e6c8b5b5 100644 --- a/pkg/config/toml/defaults/Hashkey_Mainnet.toml +++ b/pkg/config/toml/defaults/Hashkey_Mainnet.toml @@ -9,6 +9,7 @@ FeeCapDefault = '1000 gwei' [NodePool] PollFailureThreshold = 2 +PollSuccessThreshold = 0 PollInterval = '8s' [GasEstimator.DAOracle] diff --git a/pkg/config/toml/defaults/Shibarium_Testnet.toml b/pkg/config/toml/defaults/Shibarium_Testnet.toml index 8f97771ead..9a806bf68f 100644 --- a/pkg/config/toml/defaults/Shibarium_Testnet.toml +++ b/pkg/config/toml/defaults/Shibarium_Testnet.toml @@ -29,5 +29,6 @@ SamplingInterval = '1s' [NodePool] PollFailureThreshold = 2 +PollSuccessThreshold = 0 PollInterval = '3s' SyncThreshold = 10 diff --git a/pkg/config/toml/defaults/fallback.toml b/pkg/config/toml/defaults/fallback.toml index 64afbf8842..f6961ddf9f 100644 --- a/pkg/config/toml/defaults/fallback.toml +++ b/pkg/config/toml/defaults/fallback.toml @@ -79,6 +79,7 @@ PersistenceBatchSize = 100 [NodePool] PollFailureThreshold = 5 +PollSuccessThreshold = 0 PollInterval = '10s' SelectionMode = 'HighestHead' SyncThreshold = 5 diff --git a/pkg/config/toml/docs.toml b/pkg/config/toml/docs.toml index 7b5fb5eb7f..9e61808268 100644 --- a/pkg/config/toml/docs.toml +++ b/pkg/config/toml/docs.toml @@ -425,6 +425,10 @@ GasEstimator.PriceMax = '79 gwei' # Example # # Set to zero to disable poll checking. PollFailureThreshold = 5 # Default +# PollSuccessThreshold indicates how many consecutive polls must succeed in order to mark a node as alive once it has been marked as unreachable. +# +# Set to zero to require no successful polls (previous behavior). +PollSuccessThreshold = 0 # Default # PollInterval controls how often to poll the node to check for liveness. # # Set to zero to disable poll checking. diff --git a/pkg/config/toml/testdata/config-full.toml b/pkg/config/toml/testdata/config-full.toml index 3ee1f839ba..c0390b1f01 100644 --- a/pkg/config/toml/testdata/config-full.toml +++ b/pkg/config/toml/testdata/config-full.toml @@ -116,6 +116,7 @@ PriceMax = '79.228162514264337593543950335 gether' [NodePool] PollFailureThreshold = 5 +PollSuccessThreshold = 0 PollInterval = '1m0s' SelectionMode = 'HighestHead' SyncThreshold = 13