Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WS URL can be optional when LogBroadcaster is disabled #14364

Merged
merged 55 commits into from
Oct 3, 2024
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
b05a1a7
WS URL can be optional
huangzhen1997 Sep 5, 2024
71c84c6
add changeset
huangzhen1997 Sep 6, 2024
e99aae0
change
huangzhen1997 Sep 6, 2024
6737f0c
make WSURL optional
huangzhen1997 Sep 6, 2024
8df1659
fix test, and enforce SubscribeFilterLogs to fail when ws url not pro…
huangzhen1997 Sep 6, 2024
e1fa795
add comments
huangzhen1997 Sep 6, 2024
7f59b07
update changeset
huangzhen1997 Sep 6, 2024
48c7f1a
update dial logic and make ws optional not required
huangzhen1997 Sep 6, 2024
515888c
fix test
huangzhen1997 Sep 6, 2024
9ecc624
fix
huangzhen1997 Sep 9, 2024
01e9e08
fix lint
huangzhen1997 Sep 9, 2024
4b860a1
Merge branch 'develop' of github.com:smartcontractkit/chainlink into …
huangzhen1997 Sep 9, 2024
fdc5c8c
address comments
huangzhen1997 Sep 9, 2024
2660903
update comments
huangzhen1997 Sep 9, 2024
b3e270d
fix test
huangzhen1997 Sep 9, 2024
ba73ef6
add check when both ws and http missing
huangzhen1997 Sep 9, 2024
5bec775
add test and add restrictions
huangzhen1997 Sep 9, 2024
9a841ce
add comment
huangzhen1997 Sep 9, 2024
670ebb8
revert outdated change
huangzhen1997 Sep 9, 2024
799dc6c
remove extra line
huangzhen1997 Sep 9, 2024
427a8a0
fix test
huangzhen1997 Sep 9, 2024
7738ff0
revert changes from rpc client
huangzhen1997 Sep 10, 2024
795ee7b
unintended change
huangzhen1997 Sep 10, 2024
3b27171
remove unused
huangzhen1997 Sep 10, 2024
d3f523c
update verification logic
huangzhen1997 Sep 10, 2024
a71015d
add test fix
huangzhen1997 Sep 10, 2024
7908e2a
modify unit test to cover logbroadcaster enabled false
huangzhen1997 Sep 10, 2024
ae09d35
update doc
huangzhen1997 Sep 12, 2024
fe19568
udpate changeset
huangzhen1997 Sep 12, 2024
cc05a3f
address PR comments
huangzhen1997 Sep 13, 2024
d7a7719
address pr comments
huangzhen1997 Sep 13, 2024
3e92077
update invalid toml config
huangzhen1997 Sep 13, 2024
d7af044
fix test
huangzhen1997 Sep 13, 2024
4d83e3f
ws required for primary nodes when logbroadcaster enabled
huangzhen1997 Sep 16, 2024
d5dbade
minor
huangzhen1997 Sep 16, 2024
e8901e7
Dmytro's comments
huangzhen1997 Sep 19, 2024
2713252
fix nil ptr, more fix to come
huangzhen1997 Sep 19, 2024
8a71a49
resolve merge conflicts
huangzhen1997 Sep 23, 2024
85f56ca
Merge branch 'develop' into BCFR-451/make-ws-url-optional
huangzhen1997 Sep 23, 2024
774d025
fix make
huangzhen1997 Sep 24, 2024
5b3918d
refactor function sig
huangzhen1997 Sep 24, 2024
691f0d1
Merge branch 'develop' into BCFR-451/make-ws-url-optional
huangzhen1997 Sep 25, 2024
971e300
fix test
huangzhen1997 Sep 25, 2024
7cf0ba5
fix
huangzhen1997 Sep 25, 2024
4502d4d
make ws pointer
dhaidashenko Sep 26, 2024
5e9e9b7
Merge pull request #14573 from smartcontractkit/ws-optional
huangzhen1997 Sep 26, 2024
615248b
fix
huangzhen1997 Sep 26, 2024
e87e3c1
fix make
huangzhen1997 Sep 26, 2024
d689696
address comments
huangzhen1997 Sep 27, 2024
b267e7a
fix lint
huangzhen1997 Sep 27, 2024
51c5d96
fix make
huangzhen1997 Sep 27, 2024
c5f6b90
fix make
huangzhen1997 Sep 27, 2024
4c81cac
fix test conflicts
huangzhen1997 Sep 30, 2024
e2d7a8e
fix make
huangzhen1997 Sep 30, 2024
580795e
fix rpc disconnect with optional ws url
dhaidashenko Oct 3, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/silly-lies-boil.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"chainlink": minor
---

Make websocket URL flag `WSURL` for `EVM.Nodes`, and apply logic so that:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Make websocket URL flag `WSURL` for `EVM.Nodes`, and apply logic so that:
Make websocket URL `WSURL` for `EVM.Nodes` optional, and apply logic so that:

* If WS URL was not provided, SubscribeFilterLogs should fail with an explicit error
* If WS URL was not provided LogBroadcaster should be disabled
#internal
jmank88 marked this conversation as resolved.
Show resolved Hide resolved
40 changes: 34 additions & 6 deletions core/chains/evm/client/config_builder.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package client

import (
"errors"
"fmt"
"net/url"
"time"
Expand All @@ -15,6 +16,8 @@ import (
"github.com/smartcontractkit/chainlink/v2/core/chains/evm/config/toml"
)

var ErrLogBroadcasterEnabledWithoutWSURL = errors.New("logbroadcaster cannot be enabled unless all nodes have WSURL provided")

type NodeConfig struct {
Name *string
WSURL *string
Expand Down Expand Up @@ -43,12 +46,18 @@ func NewClientConfigs(
deathDeclarationDelay time.Duration,
noNewFinalizedHeadsThreshold time.Duration,
finalizedBlockPollInterval time.Duration,
LogBroadcasterEnabled bool,

) (commonclient.ChainConfig, evmconfig.NodePool, []*toml.Node, error) {
nodes, err := parseNodeConfigs(nodeCfgs)
if err != nil {
return nil, nil, nil, err
}

if err = verifyLogBroadcasterFlag(nodes, LogBroadcasterEnabled); err != nil {
return nil, nil, nil, err
}

nodePool := toml.NodePool{
SelectionMode: selectionMode,
LeaseDuration: commonconfig.MustNewDuration(leaseDuration),
Expand Down Expand Up @@ -76,18 +85,37 @@ func NewClientConfigs(
return chainConfig, nodePoolCfg, nodes, nil
}

// verifyLogBroadcasterFlag checks node config and return error if LogBroadcaster enabled but some node missing WSURL
func verifyLogBroadcasterFlag(nodes []*toml.Node, LogBroadcasterEnabled bool) error {
dhaidashenko marked this conversation as resolved.
Show resolved Hide resolved
if !LogBroadcasterEnabled {
return nil
}
for _, node := range nodes {
if node.WSURL == nil || node.WSURL.IsZero() {
return ErrLogBroadcasterEnabledWithoutWSURL
}
}
return nil
}

func parseNodeConfigs(nodeCfgs []NodeConfig) ([]*toml.Node, error) {
nodes := make([]*toml.Node, len(nodeCfgs))
for i, nodeCfg := range nodeCfgs {
if nodeCfg.WSURL == nil || nodeCfg.HTTPURL == nil {
return nil, fmt.Errorf("node config [%d]: missing WS or HTTP URL", i)
var wsURL, httpURL *commonconfig.URL
// wsUrl is optional
if nodeCfg.WSURL != nil {
wsURL = commonconfig.MustParseURL(*nodeCfg.WSURL)
}
wsUrl := commonconfig.MustParseURL(*nodeCfg.WSURL)
httpUrl := commonconfig.MustParseURL(*nodeCfg.HTTPURL)

if nodeCfg.HTTPURL == nil {
return nil, fmt.Errorf("node config [%d]: HTTP URL", i)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return nil, fmt.Errorf("node config [%d]: HTTP URL", i)
return nil, fmt.Errorf("node config [%d]: missing HTTP URL", i)

}

httpURL = commonconfig.MustParseURL(*nodeCfg.HTTPURL)
node := &toml.Node{
Name: nodeCfg.Name,
WSURL: wsUrl,
HTTPURL: httpUrl,
WSURL: wsURL,
HTTPURL: httpURL,
SendOnly: nodeCfg.SendOnly,
Order: nodeCfg.Order,
}
Expand Down
6 changes: 3 additions & 3 deletions core/chains/evm/client/config_builder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ func TestClientConfigBuilder(t *testing.T) {
noNewHeadsThreshold := time.Second
chainCfg, nodePool, nodes, err := client.NewClientConfigs(selectionMode, leaseDuration, chainTypeStr, nodeConfigs,
pollFailureThreshold, pollInterval, syncThreshold, nodeIsSyncingEnabled, noNewHeadsThreshold, finalityDepth,
finalityTagEnabled, finalizedBlockOffset, enforceRepeatableRead, deathDeclarationDelay, noNewFinalizedBlocksThreshold, pollInterval)
finalityTagEnabled, finalizedBlockOffset, enforceRepeatableRead, deathDeclarationDelay, noNewFinalizedBlocksThreshold, pollInterval, true)
require.NoError(t, err)

// Validate node pool configs
Expand Down Expand Up @@ -90,15 +90,15 @@ func TestNodeConfigs(t *testing.T) {
require.Len(t, tomlNodes, len(nodeConfigs))
})

t.Run("parsing missing ws url fails", func(t *testing.T) {
t.Run("ws is optional", func(t *testing.T) {
nodeConfigs := []client.NodeConfig{
{
Name: ptr("foo1"),
HTTPURL: ptr("http://foo1.test"),
},
}
_, err := client.ParseTestNodeConfigs(nodeConfigs)
require.Error(t, err)
require.Nil(t, err)
})

t.Run("parsing missing http url fails", func(t *testing.T) {
Expand Down
2 changes: 1 addition & 1 deletion core/chains/evm/client/evm_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ func NewEvmClient(cfg evmconfig.NodePool, chainCfg commonclient.ChainConfig, cli
var sendonlys []commonclient.SendOnlyNode[*big.Int, RPCClient]
largePayloadRPCTimeout, defaultRPCTimeout := getRPCTimeouts(chainType)
for i, node := range nodes {
if node.SendOnly != nil && *node.SendOnly {
if node.WSURL == nil || (node.SendOnly != nil && *node.SendOnly) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A secondary or SendOnly node has limited functionality. It's only used to broadcast transactions (fire and forget), and we are not running health checks for it.
We should not change the type of the node. Instead, introduce changes to the RPC client.

Copy link
Contributor Author

@huangzhen1997 huangzhen1997 Sep 9, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moving the enforcement check to Dial() at rpc_client.go

rpc := NewRPCClient(lggr, empty, (*url.URL)(node.HTTPURL), *node.Name, int32(i), chainID,
commonclient.Secondary, cfg.FinalizedBlockPollInterval(), largePayloadRPCTimeout, defaultRPCTimeout, chainType)
sendonly := commonclient.NewSendOnlyNode(lggr, (url.URL)(*node.HTTPURL),
Expand Down
17 changes: 14 additions & 3 deletions core/chains/evm/client/evm_client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,20 @@ func TestNewEvmClient(t *testing.T) {
finalityTagEnabled := ptr(true)
chainCfg, nodePool, nodes, err := client.NewClientConfigs(selectionMode, leaseDuration, chainTypeStr, nodeConfigs,
pollFailureThreshold, pollInterval, syncThreshold, nodeIsSyncingEnabled, noNewHeadsThreshold, finalityDepth,
finalityTagEnabled, finalizedBlockOffset, enforceRepeatableRead, deathDeclarationDelay, noNewFinalizedBlocksThreshold, finalizedBlockPollInterval)
finalityTagEnabled, finalizedBlockOffset, enforceRepeatableRead, deathDeclarationDelay, noNewFinalizedBlocksThreshold, finalizedBlockPollInterval, true)
require.NoError(t, err)

client := client.NewEvmClient(nodePool, chainCfg, nil, logger.Test(t), testutils.FixtureChainID, nodes, chaintype.ChainType(chainTypeStr))
require.NotNil(t, client)
evmClient := client.NewEvmClient(nodePool, chainCfg, nil, logger.Test(t), testutils.FixtureChainID, nodes, chaintype.ChainType(chainTypeStr))
require.NotNil(t, evmClient)

nodeConfigs[0].WSURL = nil
_, _, _, err = client.NewClientConfigs(selectionMode, leaseDuration, chainTypeStr, nodeConfigs,
pollFailureThreshold, pollInterval, syncThreshold, nodeIsSyncingEnabled, noNewHeadsThreshold, finalityDepth,
finalityTagEnabled, finalizedBlockOffset, enforceRepeatableRead, deathDeclarationDelay, noNewFinalizedBlocksThreshold, finalizedBlockPollInterval, true)
require.Equal(t, client.ErrLogBroadcasterEnabledWithoutWSURL, err)

_, _, _, err = client.NewClientConfigs(selectionMode, leaseDuration, chainTypeStr, nodeConfigs,
pollFailureThreshold, pollInterval, syncThreshold, nodeIsSyncingEnabled, noNewHeadsThreshold, finalityDepth,
finalityTagEnabled, finalizedBlockOffset, enforceRepeatableRead, deathDeclarationDelay, noNewFinalizedBlocksThreshold, finalizedBlockPollInterval, false)
require.Nil(t, err)
}
29 changes: 16 additions & 13 deletions core/chains/evm/client/rpc_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ var (
float64(8 * time.Second),
},
}, []string{"evmChainID", "nodeName", "rpcHost", "isSendOnly", "success", "rpcCallName"})
errSubscribeFilterLogsNotAllowedWithoutWS = errors.New("SubscribeFilterLogs is not allowed without ws url")
dhaidashenko marked this conversation as resolved.
Show resolved Hide resolved
)

// RPCClient includes all the necessary generalized RPC methods along with any additional chain-specific methods.
Expand Down Expand Up @@ -196,29 +197,28 @@ func (r *rpcClient) Dial(callerCtx context.Context) error {
defer cancel()

promEVMPoolRPCNodeDials.WithLabelValues(r.chainID.String(), r.name).Inc()
lggr := r.rpcLog.With("wsuri", r.ws.uri.Redacted())
if r.http != nil {
lggr = lggr.With("httpuri", r.http.uri.Redacted())
}
lggr.Debugw("RPC dial: evmclient.Client#dial")
lggr := r.rpcLog
if r.ws.uri.String() != "" {
lggr = lggr.With("wsuri", r.ws.uri.Redacted())
wsrpc, err := rpc.DialWebsocket(ctx, r.ws.uri.String(), "")
if err != nil {
promEVMPoolRPCNodeDialsFailed.WithLabelValues(r.chainID.String(), r.name).Inc()
return r.wrapRPCClientError(pkgerrors.Wrapf(err, "error while dialing websocket: %v", r.ws.uri.Redacted()))
}

wsrpc, err := rpc.DialWebsocket(ctx, r.ws.uri.String(), "")
if err != nil {
promEVMPoolRPCNodeDialsFailed.WithLabelValues(r.chainID.String(), r.name).Inc()
return r.wrapRPCClientError(pkgerrors.Wrapf(err, "error while dialing websocket: %v", r.ws.uri.Redacted()))
r.ws.rpc = wsrpc
r.ws.geth = ethclient.NewClient(wsrpc)
}

r.ws.rpc = wsrpc
r.ws.geth = ethclient.NewClient(wsrpc)

if r.http != nil {
lggr = lggr.With("httpuri", r.http.uri.Redacted())
if err := r.DialHTTP(); err != nil {
return err
}
}

lggr.Debugw("RPC dial: evmclient.Client#dial")
promEVMPoolRPCNodeDialsSuccess.WithLabelValues(r.chainID.String(), r.name).Inc()

return nil
}

Expand Down Expand Up @@ -1215,6 +1215,9 @@ func (r *rpcClient) ClientVersion(ctx context.Context) (version string, err erro
}

func (r *rpcClient) SubscribeFilterLogs(ctx context.Context, q ethereum.FilterQuery, ch chan<- types.Log) (_ ethereum.Subscription, err error) {
if r.ws.uri.String() == "" {
return nil, errSubscribeFilterLogsNotAllowedWithoutWS
}
ctx, cancel, chStopInFlight, ws, _ := r.acquireQueryCtx(ctx, r.rpcTimeout)
defer cancel()
lggr := r.newRqLggr().With("q", q)
Expand Down
8 changes: 8 additions & 0 deletions core/chains/evm/client/rpc_client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,14 @@ func TestRPCClient_SubscribeFilterLogs(t *testing.T) {
lggr := logger.Test(t)
ctx, cancel := context.WithTimeout(tests.Context(t), tests.WaitTimeout(t))
defer cancel()
t.Run("Failed SubscribeFilterLogs when WSURL is empty", func(t *testing.T) {
observedLggr, _ := logger.TestObserved(t, zap.DebugLevel)
rpcClient := client.NewRPCClient(observedLggr, url.URL{}, nil, "rpc", 1, chainId, commonclient.Primary, 0, commonclient.QueryTimeout, commonclient.QueryTimeout, "")
require.Nil(t, rpcClient.Dial(ctx))

_, err := rpcClient.SubscribeFilterLogs(ctx, ethereum.FilterQuery{}, make(chan types.Log))
require.Error(t, err)
dhaidashenko marked this conversation as resolved.
Show resolved Hide resolved
})
t.Run("Failed SubscribeFilterLogs logs and returns proper error", func(t *testing.T) {
server := testutils.NewWSServer(t, chainId, func(reqMethod string, reqParams gjson.Result) (resp testutils.JSONRPCResponse) {
return resp
Expand Down
15 changes: 2 additions & 13 deletions core/chains/evm/config/toml/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -964,19 +964,8 @@ func (n *Node) ValidateConfig() (err error) {
err = multierr.Append(err, commonconfig.ErrEmpty{Name: "Name", Msg: "required for all nodes"})
}

var sendOnly bool
if n.SendOnly != nil {
sendOnly = *n.SendOnly
}
if n.WSURL == nil {
if !sendOnly {
err = multierr.Append(err, commonconfig.ErrMissing{Name: "WSURL", Msg: "required for primary nodes"})
}
} else if n.WSURL.IsZero() {
if !sendOnly {
err = multierr.Append(err, commonconfig.ErrEmpty{Name: "WSURL", Msg: "required for primary nodes"})
}
} else {
// WSURL can be optional when LogBroadcaster is disabled, if WSURL not provided, rpc will return error for SubscribeFilterLogs
jmank88 marked this conversation as resolved.
Show resolved Hide resolved
if n.WSURL != nil && !n.WSURL.IsZero() {
switch n.WSURL.Scheme {
case "ws", "wss":
default:
Expand Down
2 changes: 1 addition & 1 deletion core/config/docs/chains-evm.toml
Original file line number Diff line number Diff line change
Expand Up @@ -462,7 +462,7 @@ ObservationGracePeriod = '1s' # Default
[[EVM.Nodes]]
# Name is a unique (per-chain) identifier for this node.
Name = 'foo' # Example
# WSURL is the WS(S) endpoint for this node. Required for primary nodes.
# WSURL is the WS(S) endpoint for this node. Optional when `LogBroadcasterEnabled` is `false`
WSURL = 'wss://web.socket/test' # Example
# HTTPURL is the HTTP(S) endpoint for this node. Required for all nodes.
HTTPURL = 'https://foo.web' # Example
Expand Down
10 changes: 3 additions & 7 deletions core/services/chainlink/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1328,9 +1328,7 @@ func TestConfig_Validate(t *testing.T) {
- PriceMax: invalid value (10 gwei): must be greater than or equal to PriceDefault
- BlockHistory.BlockHistorySize: invalid value (0): must be greater than or equal to 1 with BlockHistory Mode
- Nodes: 2 errors:
- 0: 2 errors:
- WSURL: missing: required for primary nodes
- HTTPURL: missing: required for all nodes
- 0.HTTPURL: missing: required for all nodes
- 1.HTTPURL: missing: required for all nodes
- 1: 10 errors:
- ChainType: invalid value (Foo): must not be set with this chain id
Expand All @@ -1352,17 +1350,15 @@ func TestConfig_Validate(t *testing.T) {
- FinalityDepth: invalid value (0): must be greater than or equal to 1
- MinIncomingConfirmations: invalid value (0): must be greater than or equal to 1
- 3.Nodes: 5 errors:
- 0: 3 errors:
- 0: 2 errors:
- Name: missing: required for all nodes
- WSURL: missing: required for primary nodes
- HTTPURL: empty: required for all nodes
- 1: 3 errors:
- Name: missing: required for all nodes
- WSURL: invalid value (http): must be ws or wss
- HTTPURL: missing: required for all nodes
- 2: 3 errors:
- 2: 2 errors:
- Name: empty: required for all nodes
- WSURL: missing: required for primary nodes
- HTTPURL: invalid value (ws): must be http or https
- 3.HTTPURL: missing: required for all nodes
- 4.HTTPURL: missing: required for all nodes
Expand Down
2 changes: 1 addition & 1 deletion docs/CONFIG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9002,7 +9002,7 @@ Name is a unique (per-chain) identifier for this node.
```toml
WSURL = 'wss://web.socket/test' # Example
```
WSURL is the WS(S) endpoint for this node. Required for primary nodes.
WSURL is the WS(S) endpoint for this node. Optional when `LogBroadcasterEnabled` is `false`

### HTTPURL
```toml
Expand Down
Loading