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

CapabilityRegistry: add capability version #12996

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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)
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we remove this then?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The idea was to keep it for informational purposes.

}
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
Loading