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

Open
wants to merge 37 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
37 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
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
18 changes: 12 additions & 6 deletions core/chains/evm/client/config_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,15 +79,21 @@ func NewClientConfigs(
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 LogBroadcaster is enabled, at least one wsURL is required and this will be checked in EVMConfig validation
Copy link
Collaborator

Choose a reason for hiding this comment

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

LogBroadcaster is not relevant to the context of evm/client. It's one of many users, we should not reference it here.

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
4 changes: 2 additions & 2 deletions core/chains/evm/client/config_builder_test.go
Original file line number Diff line number Diff line change
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 can be 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
36 changes: 23 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,8 @@ var (
float64(8 * time.Second),
},
}, []string{"evmChainID", "nodeName", "rpcHost", "isSendOnly", "success", "rpcCallName"})
errSubscribeFilterLogsNotAllowedWithoutWS = errors.New("SubscribeFilterLogs is not allowed without ws url")
Copy link
Collaborator

Choose a reason for hiding this comment

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

Are we reusing these variables across multiple functions? If not, it might make sense to define them inline or make them public and use in tests

errWSAndHTTPBothMissing = errors.New("cannot dial rpc client when both ws and http info are missing")
)

// RPCClient includes all the necessary generalized RPC methods along with any additional chain-specific methods.
Expand Down Expand Up @@ -195,30 +197,35 @@ func (r *rpcClient) Dial(callerCtx context.Context) error {
ctx, cancel := r.makeQueryCtx(callerCtx, r.rpcTimeout)
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())
if r.ws.uri.String() == "" && r.http == nil {
Copy link
Collaborator

Choose a reason for hiding this comment

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

WS is optional. Can't we make it a *url.URL instead of url.URL?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We can, but it requires a lot refactor and adding nil ptr check and not sure if we want to do in this PR

return errWSAndHTTPBothMissing
}
lggr.Debugw("RPC dial: evmclient.Client#dial")

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()))
}
promEVMPoolRPCNodeDials.WithLabelValues(r.chainID.String(), r.name).Inc()
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()))
}

r.ws.rpc = wsrpc
r.ws.geth = ethclient.NewClient(wsrpc)
r.ws.rpc = wsrpc
r.ws.geth = ethclient.NewClient(wsrpc)
} else {
lggr = lggr.With("wsuri", "empty WS URI")
Copy link
Collaborator

Choose a reason for hiding this comment

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

There is no need to include empty was URI into the log

}

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 +1222,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
10 changes: 10 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,16 @@ 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) {
// ws is optional when LogBroadcaster is disabled, however SubscribeFilterLogs will return error if ws is missing
httpURL := url.URL{}
observedLggr, _ := logger.TestObserved(t, zap.DebugLevel)
rpcClient := client.NewRPCClient(observedLggr, url.URL{}, &httpURL, "rpc", 1, chainId, commonclient.Primary, 0, commonclient.QueryTimeout, commonclient.QueryTimeout, "")
Copy link
Contributor

Choose a reason for hiding this comment

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

Could httpURL be omitted?

Suggested change
httpURL := url.URL{}
observedLggr, _ := logger.TestObserved(t, zap.DebugLevel)
rpcClient := client.NewRPCClient(observedLggr, url.URL{}, &httpURL, "rpc", 1, chainId, commonclient.Primary, 0, commonclient.QueryTimeout, commonclient.QueryTimeout, "")
observedLggr, _ := logger.TestObserved(t, zap.DebugLevel)
rpcClient := client.NewRPCClient(observedLggr, url.URL{}, nil, "rpc", 1, chainId, commonclient.Primary, 0, commonclient.QueryTimeout, commonclient.QueryTimeout, "")

Copy link
Contributor Author

@huangzhen1997 huangzhen1997 Sep 13, 2024

Choose a reason for hiding this comment

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

For this specific test it can't, since Dial() will fail due to errWSAndHTTPBothMissing. I can add another test that covers when both WS and HTTPURL are empty

require.Nil(t, rpcClient.Dial(ctx))

_, err := rpcClient.SubscribeFilterLogs(ctx, ethereum.FilterQuery{}, make(chan types.Log))
require.Error(t, err)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Could you ensure that we return proper error msg?

})
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
39 changes: 25 additions & 14 deletions core/chains/evm/config/toml/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,10 @@ import (
"github.com/smartcontractkit/chainlink/v2/core/chains/evm/utils/big"
)

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

type HasEVMConfigs interface {
EVMConfigs() EVMConfigs
Expand Down Expand Up @@ -310,6 +313,12 @@ func (c *EVMConfig) ValidateConfig() (err error) {
if len(c.Nodes) == 0 {
err = multierr.Append(err, commonconfig.ErrMissing{Name: "Nodes", Msg: "must have at least one node"})
} else {
if c.LogBroadcasterEnabled != nil {
if err = verifyLogBroadcasterFlag(c.Nodes, *c.LogBroadcasterEnabled); err != nil {
err = multierr.Append(err, commonconfig.ErrMissing{Name: "Nodes", Msg: "must have at least one ws uri when LogBroadcaster is enabled"})
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
err = multierr.Append(err, commonconfig.ErrMissing{Name: "Nodes", Msg: "must have at least one ws uri when LogBroadcaster is enabled"})
err = multierr.Append(err, commonconfig.ErrMissing{Name: "Nodes", Msg: "must have at least one node with a WSURL when LogBroadcaster is enabled"})

}
}

var hasPrimary bool
for _, n := range c.Nodes {
if n.SendOnly != nil && *n.SendOnly {
Expand Down Expand Up @@ -965,19 +974,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 {
// relax the check here as WSURL can potentially be empty if LogBroadcaster is disabled (checked in EVMConfig Validation)
if n.WSURL != nil && !n.WSURL.IsZero() {
switch n.WSURL.Scheme {
case "ws", "wss":
default:
Expand Down Expand Up @@ -1028,3 +1026,16 @@ func (n *Node) SetFrom(f *Node) {
func ChainIDInt64(cid string) (int64, error) {
return strconv.ParseInt(cid, 10, 64)
}

// verifyLogBroadcasterFlag checks node config and return error if LogBroadcaster enabled but some node missing WSURL
func verifyLogBroadcasterFlag(nodes []*Node, LogBroadcasterEnabled bool) error {
if !LogBroadcasterEnabled {
return nil
}
for _, node := range nodes {
if node.WSURL == nil || node.WSURL.IsZero() {
return ErrLogBroadcasterEnabledWithoutWSURL
}
}
return nil
}
2 changes: 1 addition & 1 deletion core/config/docs/chains-evm.toml
Original file line number Diff line number Diff line change
Expand Up @@ -464,7 +464,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. Required for primary nodes when `LogBroadcasterEnabled` is `true`
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
14 changes: 6 additions & 8 deletions core/services/chainlink/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1331,7 +1331,9 @@ func TestConfig_Validate(t *testing.T) {
- 1.ChainID: invalid value (1): duplicate - must be unique
- 0.Nodes.1.Name: invalid value (foo): duplicate - must be unique
- 3.Nodes.4.WSURL: invalid value (ws://dupe.com): duplicate - must be unique
- 0: 3 errors:
- 0: 5 errors:
- logbroadcaster cannot be enabled unless all nodes have WSURL provided
- Nodes: missing: must have at least one ws uri when LogBroadcaster is enabled
Copy link
Contributor

Choose a reason for hiding this comment

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

These seem contradictory. Must all nodes have WSURL, or just one?

Copy link
Contributor Author

@huangzhen1997 huangzhen1997 Sep 13, 2024

Choose a reason for hiding this comment

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

Thanks for catching it. According to the requirement, I was intended to enforce Must all nodes have WSURL when LogBroadcaster is enabled, so the second error message is incorrect. But I will double check if the requirement is correct, as it might not backward compatible

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated the error message. We going to require all primary nodes to provide valid WS URL when LogBroadcaster is enabled.

- GasEstimator.BumpTxDepth: invalid value (11): must be less than or equal to Transactions.MaxInFlight
- GasEstimator: 6 errors:
- BumpPercent: invalid value (1): may not be less than Geth's default of 10
Expand All @@ -1341,9 +1343,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 @@ -1365,17 +1365,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
1 change: 1 addition & 0 deletions core/services/chainlink/testdata/config-invalid.toml
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ MinIncomingConfirmations = 0

[[EVM]]
ChainID = '99'
LogBroadcasterEnabled = false
jmank88 marked this conversation as resolved.
Show resolved Hide resolved

[[EVM.Nodes]]
HTTPURl = ''
Expand Down
2 changes: 1 addition & 1 deletion docs/CONFIG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9137,7 +9137,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. Required for primary nodes when `LogBroadcasterEnabled` is `true`

### HTTPURL
```toml
Expand Down
Loading