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

Keystone: add router and other refactors #13426

Merged
merged 51 commits into from
Jun 10, 2024
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
a99dc5f
keystone: Split forwarder into two contracts
archseer Jun 5, 2024
bb12d5f
keystone: Add a ConfigSet event
archseer Jun 7, 2024
b92b116
add events to router, further changes
archseer Jun 7, 2024
98d50f1
forwarder: Use OwnerIsCreator, ITypeAndVersion
archseer Jun 7, 2024
993075f
Remove TODO
archseer Jun 7, 2024
703bd0f
Replace _splitSignature with code
archseer Jun 7, 2024
0dd9de0
minor gas savings by caching config var
archseer Jun 7, 2024
1597c47
Cache array length
archseer Jun 7, 2024
4ecff4c
Remove TODO
archseer Jun 7, 2024
9429f75
Port over reentrancy guard optimization
archseer Jun 7, 2024
2c1557f
Don't assign signers twice
archseer Jun 7, 2024
04a24c2
Merge 2c1557f4b89fdde917f9585aef2bb6b9cff0f226 into 066afc0877a9e953b…
archseer Jun 7, 2024
4fb78db
Update gethwrappers
app-token-issuer-infra-releng[bot] Jun 7, 2024
cb4e203
Address some feedback on KeystoneFeedsConsumer
archseer Jun 7, 2024
524d92e
Inline signer in setConfig/clearConfig
archseer Jun 7, 2024
f88b268
Merge 524d92edceca98f0ddc9ac265186328bb727d485 into 066afc0877a9e953b…
archseer Jun 7, 2024
554a211
Update gethwrappers
app-token-issuer-infra-releng[bot] Jun 7, 2024
c065972
Remove unused import
archseer Jun 7, 2024
534fb23
prettier format
archseer Jun 7, 2024
8788619
Generate bindings for router too
archseer Jun 7, 2024
741aaac
Merge 87886191b955643e886aad4922086cf618dbb43d into 066afc0877a9e953b…
archseer Jun 7, 2024
ee20856
Update gethwrappers
app-token-issuer-infra-releng[bot] Jun 7, 2024
f721969
Fix keystone scripts
archseer Jun 7, 2024
6302a2c
Add KeystoneRouter tests
archseer Jun 7, 2024
562947d
Update snapshot
DeividasK Jun 7, 2024
87c1499
Remove redundant operations
DeividasK Jun 7, 2024
a4bb4fe
Remove more variables
DeividasK Jun 7, 2024
34af340
Remove another unnecessary conversion
DeividasK Jun 7, 2024
cedb25c
Remove unnecessary version check
DeividasK Jun 7, 2024
d3c9c39
ConfigId bytes32 => uint64
DeividasK Jun 7, 2024
9442537
Slight improvement to error path with duplicate signer
DeividasK Jun 7, 2024
4840536
Optimize clearConfig function
DeividasK Jun 7, 2024
7ea86d0
More micro optimizations
DeividasK Jun 7, 2024
8bd2537
Inline functions
DeividasK Jun 7, 2024
e03a7b8
Remove dangling error
DeividasK Jun 7, 2024
385a346
Move things around
DeividasK Jun 7, 2024
13731d1
Add reportId to ReportProcessed
DeividasK Jun 7, 2024
357bd54
Handle empty receiver address case
DeividasK Jun 7, 2024
b4f9481
Merge remote-tracking branch 'origin/develop' into keystone-router-co…
DeividasK Jun 7, 2024
75196ea
Side: update "f" value in CapabilityRegistry
DeividasK Jun 6, 2024
fcffda8
Nit: import order
DeividasK Jun 7, 2024
86bd0a3
Style Guide: uint256 i = 0
DeividasK Jun 7, 2024
fc64eb4
Add forwarder and router getters
DeividasK Jun 7, 2024
9172e6f
Add changesets
DeividasK Jun 7, 2024
4de76c3
Merge branch 'develop' into keystone-router-contract
archseer Jun 10, 2024
c1ffbfc
Remove unused error
archseer Jun 10, 2024
f1f81c0
Merge c1ffbfcc7d55d688c90588d55125850facd4b1a4 into 8c98c80376c3b6d72…
archseer Jun 10, 2024
434ab51
Update gethwrappers
app-token-issuer-infra-releng[bot] Jun 10, 2024
b36fe56
receiverAddress -> receiver
archseer Jun 10, 2024
0857c79
Merge b36fe56d826a39bec616a6d3b504b0b4e7d6ed8f into 8c98c80376c3b6d72…
archseer Jun 10, 2024
2df7de5
Update gethwrappers
app-token-issuer-infra-releng[bot] Jun 10, 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
1 change: 1 addition & 0 deletions contracts/scripts/native_solc_compile_all_keystone
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,5 @@ compileContract () {

compileContract keystone/CapabilityRegistry.sol
compileContract keystone/KeystoneForwarder.sol
compileContract keystone/KeystoneRouter.sol
compileContract keystone/OCR3Capability.sol
32 changes: 15 additions & 17 deletions contracts/src/v0.8/keystone/KeystoneFeedsConsumer.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,15 @@
pragma solidity ^0.8.19;

import {IReceiver} from "./interfaces/IReceiver.sol";
import {ConfirmedOwner} from "../shared/access/ConfirmedOwner.sol";
import {OwnerIsCreator} from "../shared/access/OwnerIsCreator.sol";
Copy link
Collaborator

@DeividasK DeividasK Jun 7, 2024

Choose a reason for hiding this comment

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

We should drop this contract or merge it with the mock one we have for tests. I'll skip the review of the rest of the parts (the checks aren't reflective of what would happen in practice as they don't optimize storage reads).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's not just an example contract and will be used by CCIP, maybe we can drop the mock instead

Copy link
Contributor

@bolekk bolekk Jun 7, 2024

Choose a reason for hiding this comment

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

@archseer it actually won't be used as-is by CCIP in prod, I was wrong about that.
However, I'd rather still keep it here as an example template on how to implement a basic consumer.


contract KeystoneFeedsConsumer is IReceiver, ConfirmedOwner {
contract KeystoneFeedsConsumer is IReceiver, OwnerIsCreator {
event FeedReceived(bytes32 indexed feedId, int192 price, uint32 timestamp);

error UnauthorizedSender(address sender);
error UnauthorizedWorkflowOwner(address workflowOwner);
error UnauthorizedWorkflowName(bytes10 workflowName);

constructor() ConfirmedOwner(msg.sender) {}

struct ReceivedFeedReport {
bytes32 FeedId;
int192 Price;
Expand All @@ -26,55 +24,55 @@ contract KeystoneFeedsConsumer is IReceiver, ConfirmedOwner {

mapping(bytes32 feedId => StoredFeedReport feedReport) internal s_feedReports;
address[] internal s_allowedSendersList;
mapping(address => bool) internal s_allowedSenders;
Copy link
Contributor

Choose a reason for hiding this comment

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

Perfect place for enumerableSet

mapping(address sender => bool) internal s_allowedSenders;
address[] internal s_allowedWorkflowOwnersList;
mapping(address => bool) internal s_allowedWorkflowOwners;
mapping(address owner => bool) internal s_allowedWorkflowOwners;
bytes10[] internal s_allowedWorkflowNamesList;
mapping(bytes10 => bool) internal s_allowedWorkflowNames;
mapping(bytes10 workflowName => bool) internal s_allowedWorkflowNames;

function setConfig(
address[] calldata _allowedSendersList,
address[] calldata _allowedWorkflowOwnersList,
bytes10[] calldata _allowedWorkflowNamesList
) external onlyOwner {
for (uint32 i = 0; i < s_allowedSendersList.length; i++) {
for (uint32 i = 0; i < s_allowedSendersList.length; ++i) {
s_allowedSenders[s_allowedSendersList[i]] = false;
}
for (uint32 i = 0; i < _allowedSendersList.length; i++) {
for (uint32 i = 0; i < _allowedSendersList.length; ++i) {
s_allowedSenders[_allowedSendersList[i]] = true;
}
s_allowedSendersList = _allowedSendersList;
for (uint32 i = 0; i < s_allowedWorkflowOwnersList.length; i++) {
for (uint32 i = 0; i < s_allowedWorkflowOwnersList.length; ++i) {
s_allowedWorkflowOwners[s_allowedWorkflowOwnersList[i]] = false;
}
for (uint32 i = 0; i < _allowedWorkflowOwnersList.length; i++) {
for (uint32 i = 0; i < _allowedWorkflowOwnersList.length; ++i) {
s_allowedWorkflowOwners[_allowedWorkflowOwnersList[i]] = true;
}
s_allowedWorkflowOwnersList = _allowedWorkflowOwnersList;
for (uint32 i = 0; i < s_allowedWorkflowNamesList.length; i++) {
for (uint32 i = 0; i < s_allowedWorkflowNamesList.length; ++i) {
s_allowedWorkflowNames[s_allowedWorkflowNamesList[i]] = false;
}
for (uint32 i = 0; i < _allowedWorkflowNamesList.length; i++) {
for (uint32 i = 0; i < _allowedWorkflowNamesList.length; ++i) {
s_allowedWorkflowNames[_allowedWorkflowNamesList[i]] = true;
}
s_allowedWorkflowNamesList = _allowedWorkflowNamesList;
}

function onReport(bytes calldata metadata, bytes calldata rawReport) external {
if (s_allowedSenders[msg.sender] == false) {
if (!s_allowedSenders[msg.sender]) {
revert UnauthorizedSender(msg.sender);
}

(bytes10 workflowName, address workflowOwner) = _getInfo(metadata);
if (s_allowedWorkflowNames[workflowName] == false) {
if (!s_allowedWorkflowNames[workflowName]) {
revert UnauthorizedWorkflowName(workflowName);
}
if (s_allowedWorkflowOwners[workflowOwner] == false) {
if (!s_allowedWorkflowOwners[workflowOwner]) {
revert UnauthorizedWorkflowOwner(workflowOwner);
}

ReceivedFeedReport[] memory feeds = abi.decode(rawReport, (ReceivedFeedReport[]));
for (uint32 i = 0; i < feeds.length; i++) {
for (uint256 i = 0; i < feeds.length; ++i) {
s_feedReports[feeds[i].FeedId] = StoredFeedReport(feeds[i].Price, feeds[i].Timestamp);
emit FeedReceived(feeds[i].FeedId, feeds[i].Price, feeds[i].Timestamp);
}
Expand Down
177 changes: 72 additions & 105 deletions contracts/src/v0.8/keystone/KeystoneForwarder.sol
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import {IRouter} from "./interfaces/IRouter.sol";
import {IForwarder} from "./interfaces/IForwarder.sol";
import {IReceiver} from "./interfaces/IReceiver.sol";
import {ConfirmedOwner} from "../shared/access/ConfirmedOwner.sol";
import {TypeAndVersionInterface} from "../interfaces/TypeAndVersionInterface.sol";
DeividasK marked this conversation as resolved.
Show resolved Hide resolved

import {OwnerIsCreator} from "../shared/access/OwnerIsCreator.sol";
import {ITypeAndVersion} from "../shared/interfaces/ITypeAndVersion.sol";

/// @notice This is an entry point for `write_${chain}` Target capability. It
/// allows nodes to determine if reports have been processed (successfully or
/// not) in a decentralized and product-agnostic way by recording processed
/// reports.
contract KeystoneForwarder is IForwarder, ConfirmedOwner, TypeAndVersionInterface {
contract KeystoneForwarder is IForwarder, OwnerIsCreator, ITypeAndVersion {
error ReentrantCall();
archseer marked this conversation as resolved.
Show resolved Hide resolved

/// @notice This error is returned when the report is shorter than
Expand Down Expand Up @@ -66,33 +67,32 @@ contract KeystoneForwarder is IForwarder, ConfirmedOwner, TypeAndVersionInterfac
/// @param messageId The ID of the message that was already processed
error AlreadyProcessed(bytes32 messageId);

bool internal s_reentrancyGuard; // guard against reentrancy

/// @notice Contains the signing address of each oracle
struct OracleSet {
uint8 f; // Number of faulty nodes allowed
address[] signers;
mapping(address => uint256) _positions; // 1-indexed to detect unset values
}

address internal s_router;

/// @notice Contains the configuration for each DON ID
// @param configId keccak256(donId, donConfigVersion)
mapping(bytes32 configId => OracleSet) internal s_configs;

struct DeliveryStatus {
address transmitter;
bool success;
}

mapping(bytes32 reportId => DeliveryStatus status) internal s_reports;
event ConfigSet(uint32 indexed donId, uint32 indexed configVersion, uint8 f, address[] signers);

/// @notice Emitted when a report is processed
/// @param receiver The address of the receiver contract
/// @param workflowExecutionId The ID of the workflow execution
/// @param result The result of the attempted delivery. True if successful.
event ReportProcessed(address indexed receiver, bytes32 indexed workflowExecutionId, bool result);

constructor() ConfirmedOwner(msg.sender) {}
string public constant override typeAndVersion = "KeystoneForwarder 1.0.0";

constructor(address router) OwnerIsCreator() {
s_router = router;
Copy link
Contributor

Choose a reason for hiding this comment

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

It's interesting that our Router is not the first contract called. The flow is Forwarder->Router while elsewhere (e.g. in Functions) the Router (proxy) is the first point of entry. I wonder if this could be confusing to readers.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm okay with swapping the names as long as we use the same terminology on other chains where we'd have a single, upgradable contract. (So on those chains, we'd only have a router contract)

Copy link
Contributor

Choose a reason for hiding this comment

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

We don't necessarily have to swap but maybe rename the Router? Might be good to get some opinions from other Solidity experts too.

}

uint256 internal constant MAX_ORACLES = 31;
uint256 internal constant METADATA_LENGTH = 109;
Expand All @@ -108,32 +108,33 @@ contract KeystoneForwarder is IForwarder, ConfirmedOwner, TypeAndVersionInterfac

// remove any old signer addresses
for (uint256 i; i < s_configs[configId].signers.length; ++i) {
DeividasK marked this conversation as resolved.
Show resolved Hide resolved
address signer = s_configs[configId].signers[i];
delete s_configs[configId]._positions[signer];
delete s_configs[configId]._positions[s_configs[configId].signers[i]];
}

// add new signer addresses
s_configs[configId].signers = signers;
for (uint256 i; i < signers.length; ++i) {
for (uint256 i = 0; i < signers.length; ++i) {
// assign indices, detect duplicates
address signer = signers[i];
if (s_configs[configId]._positions[signer] != 0) revert DuplicateSigner(signer);
s_configs[configId]._positions[signer] = uint8(i) + 1;
s_configs[configId].signers.push(signer);
}
s_configs[configId].f = f;

emit ConfigSet(donId, configVersion, f, signers);
}

function clearConfig(uint32 donId, uint32 configVersion) external onlyOwner {
bytes32 configId = keccak256(abi.encode(donId, configVersion));

// remove any old signer addresses
for (uint256 i; i < s_configs[configId].signers.length; ++i) {
address signer = s_configs[configId].signers[i];
delete s_configs[configId]._positions[signer];
for (uint256 i = 0; i < s_configs[configId].signers.length; ++i) {
delete s_configs[configId]._positions[s_configs[configId].signers[i]];
}

s_configs[configId].f = 0;

emit ConfigSet(donId, configVersion, 0, new address[](0));
}

// send a report to receiver
Expand All @@ -142,101 +143,82 @@ contract KeystoneForwarder is IForwarder, ConfirmedOwner, TypeAndVersionInterfac
bytes calldata rawReport,
bytes calldata reportContext,
bytes[] calldata signatures
) external nonReentrant {
) external {
if (rawReport.length < METADATA_LENGTH) {
revert InvalidReport();
}

bytes32 workflowExecutionId;
DeividasK marked this conversation as resolved.
Show resolved Hide resolved
bytes2 reportId;
bytes32 configId;
bytes32 combinedId;
{
uint32 donId;
uint32 configVersion;
(workflowExecutionId, donId, configVersion, reportId) = _getMetadata(rawReport);
bytes32 configId;
OracleSet storage config;
{
uint32 donId;
uint32 configVersion;
bytes2 reportId;
(workflowExecutionId, donId, configVersion, reportId) = _getMetadata(rawReport);

configId = keccak256(abi.encode(donId, configVersion));
configId = keccak256(abi.encode(donId, configVersion));

uint8 f = s_configs[configId].f;
// f can never be 0, so this means the config doesn't actually exist
if (f == 0) revert InvalidConfig(donId, configVersion);
if (f + 1 != signatures.length) revert InvalidSignatureCount(f + 1, signatures.length);
}
config = s_configs[configId];

bytes32 combinedId = _combinedId(receiverAddress, workflowExecutionId, reportId);
if (s_reports[combinedId].transmitter != address(0)) revert AlreadyProcessed(combinedId);
uint8 f = config.f;
// f can never be 0, so this means the config doesn't actually exist
if (f == 0) revert InvalidConfig(donId, configVersion);
if (f + 1 != signatures.length) revert InvalidSignatureCount(f + 1, signatures.length);

// validate signatures
{
bytes32 completeHash = keccak256(abi.encodePacked(keccak256(rawReport), reportContext));

address[MAX_ORACLES] memory signed;
uint8 index;
for (uint256 i; i < signatures.length; ++i) {
(bytes32 r, bytes32 s, uint8 v) = _splitSignature(signatures[i]);
address signer = ecrecover(completeHash, v + 27, r, s);

// validate signer is trusted and signature is unique
index = uint8(s_configs[configId]._positions[signer]);
if (index == 0) revert InvalidSigner(signer); // index is 1-indexed so we can detect unset signers
index -= 1;
if (signed[index] != address(0)) revert DuplicateSigner(signer);
signed[index] = signer;
combinedId = _combinedId(receiverAddress, workflowExecutionId, reportId);
}
}

bool success;
try
IReceiver(receiverAddress).onReport(
rawReport[FORWARDER_METADATA_LENGTH:METADATA_LENGTH],
rawReport[METADATA_LENGTH:]
)
{
success = true;
} catch {
// Do nothing, success is already false
// validate signatures
{
uint256 numSignatures = signatures.length;
bytes32 completeHash = keccak256(abi.encodePacked(keccak256(rawReport), reportContext));

address[MAX_ORACLES] memory signed;
uint8 index;
bytes calldata signature;
for (uint256 i; i < numSignatures; ++i) {
signature = signatures[i];
if (signature.length != SIGNATURE_LENGTH) revert InvalidSignature(signature);
bytes32 r = bytes32(signature[0:32]);
bytes32 s = bytes32(signature[32:64]);
uint8 v = uint8(signature[64]);
address signer = ecrecover(completeHash, v + 27, r, s);

// validate signer is trusted and signature is unique
index = uint8(config._positions[signer]);
if (index == 0) revert InvalidSigner(signer); // index is 1-indexed so we can detect unset signers
index -= 1;
if (signed[index] != address(0)) revert DuplicateSigner(signer);
signed[index] = signer;
}
}
}

s_reports[combinedId] = DeliveryStatus(msg.sender, success);
bool success = IRouter(s_router).route(
combinedId,
msg.sender,
receiverAddress,
rawReport[FORWARDER_METADATA_LENGTH:METADATA_LENGTH],
rawReport[METADATA_LENGTH:]
);

emit ReportProcessed(receiverAddress, workflowExecutionId, success);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This emit could be moved into the router?

}

function _combinedId(address receiver, bytes32 workflowExecutionId, bytes2 reportId) internal pure returns (bytes32) {
// TODO: gas savings: could we just use a bytes key and avoid another keccak256 call
return keccak256(bytes.concat(bytes20(uint160(receiver)), workflowExecutionId, reportId));
}

// get transmitter of a given report or 0x0 if it wasn't transmitted yet
function getTransmitter(
DeividasK marked this conversation as resolved.
Show resolved Hide resolved
address receiver,
address receiverAddress,
bytes32 workflowExecutionId,
bytes2 reportId
) external view returns (address) {
bytes32 combinedId = _combinedId(receiver, workflowExecutionId, reportId);
return s_reports[combinedId].transmitter;
}

// solhint-disable-next-line chainlink-solidity/explicit-returns
function _splitSignature(bytes memory sig) internal pure returns (bytes32 r, bytes32 s, uint8 v) {
if (sig.length != SIGNATURE_LENGTH) revert InvalidSignature(sig);

assembly {
/*
First 32 bytes stores the length of the signature

add(sig, 32) = pointer of sig + 32
effectively, skips first 32 bytes of signature

mload(p) loads next 32 bytes starting at the memory address p into memory
*/

// first 32 bytes, after the length prefix
r := mload(add(sig, 32))
// second 32 bytes
s := mload(add(sig, 64))
// final byte (first byte of the next 32 bytes)
v := byte(0, mload(add(sig, 96)))
}
bytes32 combinedId = _combinedId(receiverAddress, workflowExecutionId, reportId);
return IRouter(s_router).getTransmitter(combinedId);
}

// solhint-disable-next-line chainlink-solidity/explicit-returns
Expand Down Expand Up @@ -265,19 +247,4 @@ contract KeystoneForwarder is IForwarder, ConfirmedOwner, TypeAndVersionInterfac
reportId := mload(add(rawReport, 139))
}
}

/// @inheritdoc TypeAndVersionInterface
function typeAndVersion() external pure override returns (string memory) {
return "KeystoneForwarder 1.0.0";
}

/**
* @dev replicates Open Zeppelin's ReentrancyGuard but optimized to fit our storage
*/
modifier nonReentrant() {
if (s_reentrancyGuard) revert ReentrantCall();
s_reentrancyGuard = true;
_;
s_reentrancyGuard = false;
}
}
Loading
Loading