Skip to content

Commit

Permalink
Merge branch 'develop' of github.com:smartcontractkit/chainlink into …
Browse files Browse the repository at this point in the history
…develop
  • Loading branch information
Tofel committed Apr 29, 2024
2 parents 22fe929 + 1b99404 commit 8b55cec
Show file tree
Hide file tree
Showing 36 changed files with 957 additions and 359 deletions.
5 changes: 5 additions & 0 deletions .changeset/beige-socks-cover.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"chainlink": minor
---

#internal changes to core required by change BCF3168 in common to add relayer set
5 changes: 5 additions & 0 deletions .changeset/early-paws-end.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"chainlink": patch
---

generate gethwrappers for updating node operators in capability registry #internal
5 changes: 5 additions & 0 deletions .changeset/fresh-rice-learn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"chainlink": minor
---

Moved test functions under evm package to support evm extraction #internal
5 changes: 5 additions & 0 deletions contracts/.changeset/lucky-bugs-buy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@chainlink/contracts": patch
---

Add function to update node operator'
40 changes: 40 additions & 0 deletions contracts/src/v0.8/keystone/CapabilityRegistry.sol
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,16 @@ contract CapabilityRegistry is OwnerIsCreator, TypeAndVersionInterface {
address configurationContract;
}

/// @notice This error is thrown when a caller is not allowed
/// to execute the transaction
error AccessForbidden();

/// @notice This error is thrown when there is a mismatch between
/// array arguments
/// @param lengthOne The length of the first array argument
/// @param lengthTwo The length of the second array argument
error LengthMismatch(uint256 lengthOne, uint256 lengthTwo);

/// @notice This error is thrown when trying to set a node operator's
/// admin address to the zero address
error InvalidNodeOperatorAdmin();
Expand All @@ -81,6 +91,12 @@ contract CapabilityRegistry is OwnerIsCreator, TypeAndVersionInterface {
/// @param nodeOperatorId The ID of the node operator that was removed
event NodeOperatorRemoved(uint256 nodeOperatorId);

/// @notice This event is emitted when a node operator is updated
/// @param nodeOperatorId The ID of the node operator that was updated
/// @param admin The address of the node operator's admin
/// @param name The node operator's human readable name
event NodeOperatorUpdated(uint256 nodeOperatorId, address indexed admin, string name);

/// @notice This event is emitted when a new capability is added
/// @param capabilityId The ID of the newly added capability
event CapabilityAdded(bytes32 indexed capabilityId);
Expand Down Expand Up @@ -122,6 +138,30 @@ contract CapabilityRegistry is OwnerIsCreator, TypeAndVersionInterface {
}
}

/// @notice Updates a node operator
/// @param nodeOperatorIds The ID of the node operator being updated
function updateNodeOperators(uint256[] calldata nodeOperatorIds, NodeOperator[] calldata nodeOperators) external {
if (nodeOperatorIds.length != nodeOperators.length)
revert LengthMismatch(nodeOperatorIds.length, nodeOperators.length);

address owner = owner();
for (uint256 i; i < nodeOperatorIds.length; ++i) {
uint256 nodeOperatorId = nodeOperatorIds[i];
NodeOperator memory nodeOperator = nodeOperators[i];
if (nodeOperator.admin == address(0)) revert InvalidNodeOperatorAdmin();
if (msg.sender != nodeOperator.admin && msg.sender != owner) revert AccessForbidden();

if (
s_nodeOperators[nodeOperatorId].admin != nodeOperator.admin ||
keccak256(abi.encode(s_nodeOperators[nodeOperatorId].name)) != keccak256(abi.encode(nodeOperator.name))
) {
s_nodeOperators[nodeOperatorId].admin = nodeOperator.admin;
s_nodeOperators[nodeOperatorId].name = nodeOperator.name;
emit NodeOperatorUpdated(nodeOperatorId, nodeOperator.admin, nodeOperator.name);
}
}
}

/// @notice Gets a node operator's data
/// @param nodeOperatorId The ID of the node operator to query for
/// @return NodeOperator The node operator data
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import {BaseTest} from "./BaseTest.t.sol";
import {CapabilityRegistry} from "../CapabilityRegistry.sol";

contract CapabilityRegistry_UpdateNodeOperatorTest is BaseTest {
event NodeOperatorUpdated(uint256 nodeOperatorId, address indexed admin, string name);

uint256 private constant TEST_NODE_OPERATOR_ID = 0;
address private constant NEW_NODE_OPERATOR_ADMIN = address(3);
string private constant NEW_NODE_OPERATOR_NAME = "new-node-operator";

function setUp() public override {
BaseTest.setUp();
changePrank(ADMIN);
s_capabilityRegistry.addNodeOperators(_getNodeOperators());
}

function test_RevertWhen_CalledByNonAdminAndNonOwner() public {
changePrank(STRANGER);
vm.expectRevert(CapabilityRegistry.AccessForbidden.selector);

CapabilityRegistry.NodeOperator[] memory nodeOperators = new CapabilityRegistry.NodeOperator[](1);
nodeOperators[0] = CapabilityRegistry.NodeOperator({admin: NEW_NODE_OPERATOR_ADMIN, name: NEW_NODE_OPERATOR_NAME});

uint256[] memory nodeOperatorIds = new uint256[](1);
nodeOperatorIds[0] = TEST_NODE_OPERATOR_ID;
s_capabilityRegistry.updateNodeOperators(nodeOperatorIds, nodeOperators);
}

function test_RevertWhen_NodeOperatorAdminIsZeroAddress() public {
changePrank(ADMIN);
vm.expectRevert(CapabilityRegistry.InvalidNodeOperatorAdmin.selector);
CapabilityRegistry.NodeOperator[] memory nodeOperators = new CapabilityRegistry.NodeOperator[](1);
nodeOperators[0] = CapabilityRegistry.NodeOperator({admin: address(0), name: NEW_NODE_OPERATOR_NAME});

uint256[] memory nodeOperatorIds = new uint256[](1);
nodeOperatorIds[0] = TEST_NODE_OPERATOR_ID;
s_capabilityRegistry.updateNodeOperators(nodeOperatorIds, nodeOperators);
}

function test_UpdatesNodeOperator() public {
changePrank(ADMIN);

CapabilityRegistry.NodeOperator[] memory nodeOperators = new CapabilityRegistry.NodeOperator[](1);
nodeOperators[0] = CapabilityRegistry.NodeOperator({admin: NEW_NODE_OPERATOR_ADMIN, name: NEW_NODE_OPERATOR_NAME});

uint256[] memory nodeOperatorIds = new uint256[](1);
nodeOperatorIds[0] = TEST_NODE_OPERATOR_ID;

vm.expectEmit(true, true, true, true, address(s_capabilityRegistry));
emit NodeOperatorUpdated(TEST_NODE_OPERATOR_ID, NEW_NODE_OPERATOR_ADMIN, NEW_NODE_OPERATOR_NAME);
s_capabilityRegistry.updateNodeOperators(nodeOperatorIds, nodeOperators);

CapabilityRegistry.NodeOperator memory nodeOperator = s_capabilityRegistry.getNodeOperator(0);
assertEq(nodeOperator.admin, NEW_NODE_OPERATOR_ADMIN);
assertEq(nodeOperator.name, NEW_NODE_OPERATOR_NAME);
}
}
198 changes: 198 additions & 0 deletions core/chains/evm/testutils/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
package testutils

import (
"fmt"
"math/big"
"net/http"
"net/http/httptest"
"net/url"
"sync"
"testing"
"time"

"github.com/gorilla/websocket"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/tidwall/gjson"

evmclmocks "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client/mocks"
)

func NewEthClientMock(t *testing.T) *evmclmocks.Client {
return evmclmocks.NewClient(t)
}

func NewEthClientMockWithDefaultChain(t *testing.T) *evmclmocks.Client {
c := NewEthClientMock(t)
c.On("ConfiguredChainID").Return(FixtureChainID).Maybe()
//c.On("IsL2").Return(false).Maybe()
return c
}

// JSONRPCHandler is called with the method and request param(s).
// respResult will be sent immediately. notifyResult is optional, and sent after a short delay.
type JSONRPCHandler func(reqMethod string, reqParams gjson.Result) JSONRPCResponse

type JSONRPCResponse struct {
Result, Notify string // raw JSON (i.e. quoted strings etc.)

Error struct {
Code int
Message string
}
}

type testWSServer struct {
t *testing.T
s *httptest.Server
mu sync.RWMutex
wsconns []*websocket.Conn
wg sync.WaitGroup
}

// NewWSServer starts a websocket server which invokes callback for each message received.
// If chainID is set, then eth_chainId calls will be automatically handled.
func NewWSServer(t *testing.T, chainID *big.Int, callback JSONRPCHandler) (ts *testWSServer) {
ts = new(testWSServer)
ts.t = t
ts.wsconns = make([]*websocket.Conn, 0)
handler := ts.newWSHandler(chainID, callback)
ts.s = httptest.NewServer(handler)
t.Cleanup(ts.Close)
return
}

func (ts *testWSServer) Close() {
if func() bool {
ts.mu.Lock()
defer ts.mu.Unlock()
if ts.wsconns == nil {
ts.t.Log("Test WS server already closed")
return false
}
ts.s.CloseClientConnections()
ts.s.Close()
for _, ws := range ts.wsconns {
ws.Close()
}
ts.wsconns = nil // nil indicates server closed
return true
}() {
ts.wg.Wait()
}
}

func (ts *testWSServer) WSURL() *url.URL {
return WSServerURL(ts.t, ts.s)
}

// WSServerURL returns a ws:// url for the server
func WSServerURL(t *testing.T, s *httptest.Server) *url.URL {
u, err := url.Parse(s.URL)
require.NoError(t, err, "Failed to parse url")
u.Scheme = "ws"
return u
}

func (ts *testWSServer) MustWriteBinaryMessageSync(t *testing.T, msg string) {
ts.mu.Lock()
defer ts.mu.Unlock()
conns := ts.wsconns
if len(conns) != 1 {
t.Fatalf("expected 1 conn, got %d", len(conns))
}
conn := conns[0]
err := conn.WriteMessage(websocket.BinaryMessage, []byte(msg))
require.NoError(t, err)
}

func (ts *testWSServer) newWSHandler(chainID *big.Int, callback JSONRPCHandler) (handler http.HandlerFunc) {
if callback == nil {
callback = func(method string, params gjson.Result) (resp JSONRPCResponse) { return }
}
t := ts.t
upgrader := websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool { return true },
}
return func(w http.ResponseWriter, r *http.Request) {
ts.mu.Lock()
if ts.wsconns == nil { // closed
ts.mu.Unlock()
return
}
ts.wg.Add(1)
defer ts.wg.Done()
conn, err := upgrader.Upgrade(w, r, nil)
if !assert.NoError(t, err, "Failed to upgrade WS connection") {
ts.mu.Unlock()
return
}
defer conn.Close()
ts.wsconns = append(ts.wsconns, conn)
ts.mu.Unlock()

for {
_, data, err := conn.ReadMessage()
if err != nil {
if websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseAbnormalClosure) {
ts.t.Log("Websocket closing")
return
}
ts.t.Logf("Failed to read message: %v", err)
return
}
ts.t.Log("Received message", string(data))
req := gjson.ParseBytes(data)
if !req.IsObject() {
ts.t.Logf("Request must be object: %v", req.Type)
return
}
if e := req.Get("error"); e.Exists() {
ts.t.Logf("Received jsonrpc error: %v", e)
continue
}
m := req.Get("method")
if m.Type != gjson.String {
ts.t.Logf("Method must be string: %v", m.Type)
return
}

var resp JSONRPCResponse
if chainID != nil && m.String() == "eth_chainId" {
resp.Result = `"0x` + chainID.Text(16) + `"`
} else if m.String() == "eth_syncing" {
resp.Result = "false"
} else {
resp = callback(m.String(), req.Get("params"))
}
id := req.Get("id")
var msg string
if resp.Error.Message != "" {
msg = fmt.Sprintf(`{"jsonrpc":"2.0","id":%s,"error":{"code":%d,"message":"%s"}}`, id, resp.Error.Code, resp.Error.Message)
} else {
msg = fmt.Sprintf(`{"jsonrpc":"2.0","id":%s,"result":%s}`, id, resp.Result)
}
ts.t.Logf("Sending message: %v", msg)
ts.mu.Lock()
err = conn.WriteMessage(websocket.BinaryMessage, []byte(msg))
ts.mu.Unlock()
if err != nil {
ts.t.Logf("Failed to write message: %v", err)
return
}

if resp.Notify != "" {
time.Sleep(100 * time.Millisecond)
msg := fmt.Sprintf(`{"jsonrpc":"2.0","method":"eth_subscription","params":{"subscription":"0x00","result":%s}}`, resp.Notify)
ts.t.Log("Sending message", msg)
ts.mu.Lock()
err = conn.WriteMessage(websocket.BinaryMessage, []byte(msg))
ts.mu.Unlock()
if err != nil {
ts.t.Logf("Failed to write message: %v", err)
return
}
}
}
}
}
29 changes: 29 additions & 0 deletions core/chains/evm/testutils/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package testutils

import (
"testing"

"github.com/smartcontractkit/chainlink-common/pkg/logger"

"github.com/smartcontractkit/chainlink/v2/core/chains/evm/config"
"github.com/smartcontractkit/chainlink/v2/core/chains/evm/config/toml"
"github.com/smartcontractkit/chainlink/v2/core/chains/evm/utils/big"
)

func NewTestChainScopedConfig(t testing.TB, overrideFn func(c *toml.EVMConfig)) config.ChainScopedConfig {
var chainID = (*big.Big)(FixtureChainID)
evmCfg := &toml.EVMConfig{
ChainID: chainID,
Chain: toml.Defaults(chainID),
}

if overrideFn != nil {
// We need to get the chainID from the override function first to load the correct chain defaults.
// Then we apply the override values on top
overrideFn(evmCfg)
evmCfg.Chain = toml.Defaults(evmCfg.ChainID)
overrideFn(evmCfg)
}

return config.NewTOMLChainScopedConfig(evmCfg, logger.Test(t))
}
22 changes: 22 additions & 0 deletions core/chains/evm/testutils/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package testutils

import (
"testing"

"github.com/stretchr/testify/assert"

"github.com/smartcontractkit/chainlink/v2/core/chains/evm/config/toml"
)

func TestNewTestChainScopedConfigOverride(t *testing.T) {
c := NewTestChainScopedConfig(t, func(c *toml.EVMConfig) {
finalityDepth := uint32(100)
c.FinalityDepth = &finalityDepth
})

// Overrides values
assert.Equal(t, uint32(100), c.EVM().FinalityDepth())
// fallback.toml values
assert.Equal(t, false, c.EVM().GasEstimator().EIP1559DynamicFees())

}
Loading

0 comments on commit 8b55cec

Please sign in to comment.