Skip to content

Commit

Permalink
CapabilityRegistry: add capability version (#12996)
Browse files Browse the repository at this point in the history
* Add capability response type and configuration contract

* MUST NOT override existing capability version

* MUST match configuration contract interface

* Add wrappers

* Include a tag to core changeset

* Update ICapabilityConfiguration

* Regen wrappers
  • Loading branch information
DeividasK authored Apr 29, 2024
1 parent 2e99468 commit 0a37c0e
Show file tree
Hide file tree
Showing 9 changed files with 195 additions and 10 deletions.
5 changes: 5 additions & 0 deletions .changeset/curvy-weeks-cover.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"chainlink": patch
---

#wip Keystone contract wrappers updated
5 changes: 5 additions & 0 deletions contracts/.changeset/funny-eagles-know.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@chainlink/contracts": patch
---

#wip addCapability udpates
58 changes: 58 additions & 0 deletions contracts/src/v0.8/keystone/CapabilityRegistry.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,14 @@ pragma solidity ^0.8.0;

import {TypeAndVersionInterface} from "../interfaces/TypeAndVersionInterface.sol";
import {OwnerIsCreator} from "../shared/access/OwnerIsCreator.sol";
import {IERC165} from "../vendor/openzeppelin-solidity/v4.8.3/contracts/interfaces/IERC165.sol";
import {EnumerableSet} from "../vendor/openzeppelin-solidity/v4.8.3/contracts/utils/structs/EnumerableSet.sol";
import {ICapabilityConfiguration} from "./interfaces/ICapabilityConfiguration.sol";

contract CapabilityRegistry is OwnerIsCreator, TypeAndVersionInterface {
// Add the library methods
using EnumerableSet for EnumerableSet.Bytes32Set;

struct NodeOperator {
/// @notice The address of the admin that can manage a node
/// operator
Expand All @@ -13,6 +19,16 @@ contract CapabilityRegistry is OwnerIsCreator, TypeAndVersionInterface {
string name;
}

// CapabilityResponseType indicates whether remote response requires
// aggregation or is an already aggregated report. There are multiple
// possible ways to aggregate.
enum CapabilityResponseType {
// No additional aggregation is needed on the remote response.
REPORT,
// A number of identical observations need to be aggregated.
OBSERVATION_IDENTICAL
}

struct Capability {
// Capability type, e.g. "data-streams-reports"
// bytes32(string); validation regex: ^[a-z0-9_\-:]{1,32}$
Expand All @@ -21,12 +37,39 @@ contract CapabilityRegistry is OwnerIsCreator, TypeAndVersionInterface {
// Semver, e.g., "1.2.3"
// bytes32(string); must be valid Semver + max 32 characters.
bytes32 version;
// responseType indicates whether remote response requires
// aggregation or is an OCR report. There are multiple possible
// ways to aggregate.
CapabilityResponseType responseType;
// An address to the capability configuration contract. Having this defined
// on a capability enforces consistent configuration across DON instances
// serving the same capability. Configuration contract MUST implement
// CapabilityConfigurationContractInterface.
//
// The main use cases are:
// 1) Sharing capability configuration across DON instances
// 2) Inspect and modify on-chain configuration without off-chain
// capability code.
//
// It is not recommended to store configuration which requires knowledge of
// the DON membership.
address configurationContract;
}

/// @notice This error is thrown when trying to set a node operator's
/// admin address to the zero address
error InvalidNodeOperatorAdmin();

/// @notice This error is thrown when trying add a capability that already
/// exists.
error CapabilityAlreadyExists();

/// @notice This error is thrown when trying to add a capability with a
/// configuration contract that does not implement the required interface.
/// @param proposedConfigurationContract The address of the proposed
/// configuration contract.
error InvalidCapabilityConfigurationContractInterface(address proposedConfigurationContract);

/// @notice This event is emitted when a new node operator is added
/// @param nodeOperatorId The ID of the newly added node operator
/// @param admin The address of the admin that can manage the node
Expand All @@ -43,6 +86,7 @@ contract CapabilityRegistry is OwnerIsCreator, TypeAndVersionInterface {
event CapabilityAdded(bytes32 indexed capabilityId);

mapping(bytes32 => Capability) private s_capabilities;
EnumerableSet.Bytes32Set private s_capabilityIds;

/// @notice Mapping of node operators
mapping(uint256 nodeOperatorId => NodeOperator) private s_nodeOperators;
Expand Down Expand Up @@ -87,7 +131,21 @@ contract CapabilityRegistry is OwnerIsCreator, TypeAndVersionInterface {

function addCapability(Capability calldata capability) external onlyOwner {
bytes32 capabilityId = getCapabilityID(capability.capabilityType, capability.version);

if (s_capabilityIds.contains(capabilityId)) revert CapabilityAlreadyExists();

if (capability.configurationContract != address(0)) {
if (
capability.configurationContract.code.length == 0 ||
!IERC165(capability.configurationContract).supportsInterface(
ICapabilityConfiguration.getCapabilityConfiguration.selector
)
) revert InvalidCapabilityConfigurationContractInterface(capability.configurationContract);
}

s_capabilityIds.add(capabilityId);
s_capabilities[capabilityId] = capability;

emit CapabilityAdded(capabilityId);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

/// @notice Interface for capability configuration contract. It MUST be
/// implemented for a contract to be used as a capability configuration.
/// The contract MAY store configuration that is shared across multiple
/// DON instances and capability versions.
/// @dev This interface does not guarantee the configuration contract's
/// correctness. It is the responsibility of the contract owner to ensure
/// that the configuration contract emits the CapabilityConfigurationSet
/// event when the configuration is set.
interface ICapabilityConfiguration {
/// @notice Emitted when a capability configuration is set.
event CapabilityConfigurationSet();

/// @notice Returns the capability configuration for a particular DON instance.
/// @dev donId is required to get DON-specific configuration. It avoids a
/// situation where configuration size grows too large.
/// @param donId The DON instance ID. These are stored in the CapabilityRegistry.
/// @return configuration DON's configuration for the capability.
function getCapabilityConfiguration(uint256 donId) external view returns (bytes memory configuration);

// Solidity does not support generic returns types, so this cannot be part of
// the interface. However, the implementation contract MAY implement this
// function to enable configuration decoding on-chain.
// function decodeCapabilityConfiguration(bytes configuration) external returns (TypedCapabilityConfigStruct config)
}
3 changes: 3 additions & 0 deletions contracts/src/v0.8/keystone/test/BaseTest.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,17 @@ pragma solidity ^0.8.19;

import {Test} from "forge-std/Test.sol";
import {Constants} from "./Constants.t.sol";
import {CapabilityConfigurationContract} from "./mocks/CapabilityConfigurationContract.sol";
import {CapabilityRegistry} from "../CapabilityRegistry.sol";

contract BaseTest is Test, Constants {
CapabilityRegistry internal s_capabilityRegistry;
CapabilityConfigurationContract internal s_capabilityConfigurationContract;

function setUp() public virtual {
vm.startPrank(ADMIN);
s_capabilityRegistry = new CapabilityRegistry();
s_capabilityConfigurationContract = new CapabilityConfigurationContract();
}

function _getNodeOperators() internal view returns (CapabilityRegistry.NodeOperator[] memory) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,88 @@
pragma solidity ^0.8.19;

import {BaseTest} from "./BaseTest.t.sol";
import {CapabilityConfigurationContract} from "./mocks/CapabilityConfigurationContract.sol";

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

contract CapabilityRegistry_AddCapabilityTest is BaseTest {
function test_AddCapability() public {
s_capabilityRegistry.addCapability(CapabilityRegistry.Capability("data-streams-reports", "1.0.0"));
CapabilityRegistry.Capability private basicCapability =
CapabilityRegistry.Capability({
capabilityType: "data-streams-reports",
version: "1.0.0",
responseType: CapabilityRegistry.CapabilityResponseType.REPORT,
configurationContract: address(0)
});

CapabilityRegistry.Capability private capabilityWithConfigurationContract =
CapabilityRegistry.Capability({
capabilityType: "read-ethereum-mainnet-gas-price",
version: "1.0.2",
responseType: CapabilityRegistry.CapabilityResponseType.OBSERVATION_IDENTICAL,
configurationContract: address(s_capabilityConfigurationContract)
});

function test_RevertWhen_CalledByNonAdmin() public {
changePrank(STRANGER);

vm.expectRevert("Only callable by owner");
s_capabilityRegistry.addCapability(basicCapability);
}

function test_RevertWhen_CapabilityExists() public {
// Successfully add the capability the first time
s_capabilityRegistry.addCapability(basicCapability);

// Try to add the same capability again
vm.expectRevert(CapabilityRegistry.CapabilityAlreadyExists.selector);
s_capabilityRegistry.addCapability(basicCapability);
}

function test_RevertWhen_ConfigurationContractNotDeployed() public {
address nonExistentContract = address(1);
capabilityWithConfigurationContract.configurationContract = nonExistentContract;

vm.expectRevert(
abi.encodeWithSelector(
CapabilityRegistry.InvalidCapabilityConfigurationContractInterface.selector,
nonExistentContract
)
);
s_capabilityRegistry.addCapability(capabilityWithConfigurationContract);
}

function test_RevertWhen_ConfigurationContractDoesNotMatchInterface() public {
CapabilityRegistry contractWithoutERC165 = new CapabilityRegistry();

vm.expectRevert();
capabilityWithConfigurationContract.configurationContract = address(contractWithoutERC165);
s_capabilityRegistry.addCapability(capabilityWithConfigurationContract);
}

function test_AddCapability_NoConfigurationContract() public {
s_capabilityRegistry.addCapability(basicCapability);

bytes32 capabilityId = s_capabilityRegistry.getCapabilityID(bytes32("data-streams-reports"), bytes32("1.0.0"));
CapabilityRegistry.Capability memory capability = s_capabilityRegistry.getCapability(capabilityId);
CapabilityRegistry.Capability memory storedCapability = s_capabilityRegistry.getCapability(capabilityId);

assertEq(storedCapability.capabilityType, basicCapability.capabilityType);
assertEq(storedCapability.version, basicCapability.version);
assertEq(uint256(storedCapability.responseType), uint256(basicCapability.responseType));
assertEq(storedCapability.configurationContract, basicCapability.configurationContract);
}

function test_AddCapability_WithConfiguration() public {
s_capabilityRegistry.addCapability(capabilityWithConfigurationContract);

bytes32 capabilityId = s_capabilityRegistry.getCapabilityID(
bytes32(capabilityWithConfigurationContract.capabilityType),
bytes32(capabilityWithConfigurationContract.version)
);
CapabilityRegistry.Capability memory storedCapability = s_capabilityRegistry.getCapability(capabilityId);

assertEq(capability.capabilityType, "data-streams-reports");
assertEq(capability.version, "1.0.0");
assertEq(storedCapability.capabilityType, capabilityWithConfigurationContract.capabilityType);
assertEq(storedCapability.version, capabilityWithConfigurationContract.version);
assertEq(uint256(storedCapability.responseType), uint256(capabilityWithConfigurationContract.responseType));
assertEq(storedCapability.configurationContract, capabilityWithConfigurationContract.configurationContract);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;

import {ICapabilityConfiguration} from "../../interfaces/ICapabilityConfiguration.sol";
import {ERC165} from "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/utils/introspection/ERC165.sol";

contract CapabilityConfigurationContract is ICapabilityConfiguration, ERC165 {
mapping(uint256 => bytes) private s_donConfiguration;

function getCapabilityConfiguration(uint256 donId) external view returns (bytes memory configuration) {
return s_donConfiguration[donId];
}
}
Loading

0 comments on commit 0a37c0e

Please sign in to comment.