-
Notifications
You must be signed in to change notification settings - Fork 1.7k
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
[KS-241] Update metadata passed to Forwarder and Receiver #13389
Changes from 4 commits
82e99fc
61201d9
8ed3224
3ce2f6d
109c965
a36ebde
e9c0a47
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
"chainlink": patch | ||
--- | ||
|
||
#internal Update metadata passed to Forwarder and Receiver |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
'@chainlink/contracts': patch | ||
--- | ||
|
||
#internal Keystone Forwarder and Feeds Consumer |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,27 +2,101 @@ | |
pragma solidity ^0.8.19; | ||
|
||
import {IReceiver} from "./interfaces/IReceiver.sol"; | ||
import {ConfirmedOwner} from "../shared/access/ConfirmedOwner.sol"; | ||
|
||
contract KeystoneFeedsConsumer is IReceiver { | ||
event MessageReceived(bytes32 indexed workflowId, address indexed workflowOwner, uint256 nReports); | ||
event FeedReceived(bytes32 indexed feedId, uint256 price, uint64 timestamp); | ||
contract KeystoneFeedsConsumer is IReceiver, ConfirmedOwner { | ||
event FeedReceived(bytes32 indexed feedId, int192 price, uint32 timestamp); | ||
|
||
constructor() {} | ||
error UnauthorizedSender(address sender); | ||
error UnauthorizedWorkflowOwner(address workflowOwner); | ||
error UnauthorizedWorkflowName(bytes10 workflowName); | ||
|
||
struct FeedReport { | ||
bytes32 FeedID; | ||
uint256 Price; | ||
uint64 Timestamp; | ||
constructor() ConfirmedOwner(msg.sender) {} | ||
|
||
struct ReceivedFeedReport { | ||
bytes32 FeedId; | ||
int192 Price; | ||
uint32 Timestamp; | ||
} | ||
|
||
struct StoredFeedReport { | ||
int192 Price; | ||
uint32 Timestamp; | ||
} | ||
|
||
mapping(bytes32 feedId => StoredFeedReport feedReport) internal s_feedReports; | ||
address[] internal s_allowedSendersList; | ||
mapping(address => bool) internal s_allowedSenders; | ||
address[] internal s_allowedWorkflowOwnersList; | ||
mapping(address => bool) internal s_allowedWorkflowOwners; | ||
bytes10[] internal s_allowedWorkflowNamesList; | ||
mapping(bytes10 => 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++) { | ||
s_allowedSenders[s_allowedSendersList[i]] = false; | ||
} | ||
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++) { | ||
s_allowedWorkflowOwners[s_allowedWorkflowOwnersList[i]] = false; | ||
} | ||
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++) { | ||
s_allowedWorkflowNames[s_allowedWorkflowNamesList[i]] = false; | ||
} | ||
for (uint32 i = 0; i < _allowedWorkflowNamesList.length; i++) { | ||
s_allowedWorkflowNames[_allowedWorkflowNamesList[i]] = true; | ||
} | ||
s_allowedWorkflowNamesList = _allowedWorkflowNamesList; | ||
} | ||
|
||
function onReport(bytes32 workflowId, address workflowOwner, bytes calldata rawReport) external { | ||
// TODO: validate sender and workflowOwner | ||
function onReport(bytes calldata metadata, bytes calldata rawReport) external { | ||
if (s_allowedSenders[msg.sender] == false) { | ||
revert UnauthorizedSender(msg.sender); | ||
} | ||
|
||
(bytes10 workflowName, address workflowOwner) = _getInfo(metadata); | ||
if (s_allowedWorkflowNames[workflowName] == false) { | ||
revert UnauthorizedWorkflowName(workflowName); | ||
} | ||
if (s_allowedWorkflowOwners[workflowOwner] == false) { | ||
revert UnauthorizedWorkflowOwner(workflowOwner); | ||
} | ||
|
||
FeedReport[] memory feeds = abi.decode(rawReport, (FeedReport[])); | ||
ReceivedFeedReport[] memory feeds = abi.decode(rawReport, (ReceivedFeedReport[])); | ||
for (uint32 i = 0; i < feeds.length; i++) { | ||
emit FeedReceived(feeds[i].FeedID, feeds[i].Price, feeds[i].Timestamp); | ||
s_feedReports[feeds[i].FeedId] = StoredFeedReport(feeds[i].Price, feeds[i].Timestamp); | ||
emit FeedReceived(feeds[i].FeedId, feeds[i].Price, feeds[i].Timestamp); | ||
} | ||
} | ||
|
||
// solhint-disable-next-line chainlink-solidity/explicit-returns | ||
function _getInfo(bytes memory metadata) internal pure returns (bytes10 workflowName, address workflowOwner) { | ||
// (first 32 bytes contain length of the byte array) | ||
// workflow_cid // offset 32, size 32 | ||
// workflow_name // offset 64, size 10 | ||
// workflow_owner // offset 74, size 20 | ||
// report_name // offset 94, size 2 | ||
Comment on lines
+87
to
+89
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is 32 bytes total. You could return just the 32 bytes and do an equality comparison (even a hash is not needed). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This might be tough when we allow multiple owners and/or names. |
||
assembly { | ||
// shift right by 22 bytes to get the actual value | ||
workflowName := shr(mul(22, 8), mload(add(metadata, 64))) | ||
// shift right by 12 bytes to get the actual value | ||
workflowOwner := shr(mul(12, 8), mload(add(metadata, 74))) | ||
} | ||
} | ||
|
||
emit MessageReceived(workflowId, workflowOwner, feeds.length); | ||
function getPrice(bytes32 feedId) external view returns (int192, uint32) { | ||
StoredFeedReport memory report = s_feedReports[feedId]; | ||
return (report.Price, report.Timestamp); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -17,6 +17,9 @@ contract KeystoneForwarder is IForwarder, ConfirmedOwner, TypeAndVersionInterfac | |||||
/// REPORT_METADATA_LENGTH, which is the minimum length of a report. | ||||||
error InvalidReport(); | ||||||
|
||||||
/// @notice This error is returned when the metadata version is not supported. | ||||||
error InvalidVersion(uint8 version); | ||||||
|
||||||
/// @notice This error is thrown whenever trying to set a config with a fault | ||||||
/// tolerance of 0. | ||||||
error FaultToleranceMustBePositive(); | ||||||
|
@@ -58,9 +61,9 @@ contract KeystoneForwarder is IForwarder, ConfirmedOwner, TypeAndVersionInterfac | |||||
/// @param signature The signature that was invalid | ||||||
error InvalidSignature(bytes signature); | ||||||
|
||||||
/// @notice This error is thrown whenever a report has already been processed. | ||||||
/// @param reportId The ID of the report that was already processed | ||||||
error ReportAlreadyProcessed(bytes32 reportId); | ||||||
/// @notice This error is thrown whenever a message has already been processed. | ||||||
/// @param messageId The ID of the message that was already processed | ||||||
error AlreadyProcessed(bytes32 messageId); | ||||||
|
||||||
bool internal s_reentrancyGuard; // guard against reentrancy | ||||||
|
||||||
|
@@ -83,22 +86,15 @@ contract KeystoneForwarder is IForwarder, ConfirmedOwner, TypeAndVersionInterfac | |||||
|
||||||
/// @notice Emitted when a report is processed | ||||||
/// @param receiver The address of the receiver contract | ||||||
/// @param workflowOwner The address of the workflow owner | ||||||
/// @param workflowExecutionId The ID of the workflow execution | ||||||
/// @param result The result of the attempted delivery. True if successful. | ||||||
event ReportProcessed( | ||||||
address indexed receiver, | ||||||
address indexed workflowOwner, | ||||||
bytes32 indexed workflowExecutionId, | ||||||
bool result | ||||||
); | ||||||
event ReportProcessed(address indexed receiver, bytes32 indexed workflowExecutionId, bool result); | ||||||
|
||||||
constructor() ConfirmedOwner(msg.sender) {} | ||||||
|
||||||
uint256 internal constant MAX_ORACLES = 31; | ||||||
// 32 bytes for workflowId, 4 bytes for donId, 32 bytes for | ||||||
// workflowExecutionId, 20 bytes for workflowOwner | ||||||
uint256 internal constant REPORT_METADATA_LENGTH = 88; | ||||||
uint256 internal constant METADATA_LENGTH = 109; | ||||||
uint256 internal constant FORWARDER_METADATA_LENGTH = 45; | ||||||
uint256 internal constant SIGNATURE_LENGTH = 65; | ||||||
|
||||||
function setConfig(uint32 donId, uint8 f, address[] calldata signers) external onlyOwner { | ||||||
|
@@ -133,17 +129,19 @@ contract KeystoneForwarder is IForwarder, ConfirmedOwner, TypeAndVersionInterfac | |||||
bytes calldata reportContext, | ||||||
bytes[] calldata signatures | ||||||
) external nonReentrant { | ||||||
if (rawReport.length < REPORT_METADATA_LENGTH) { | ||||||
if (rawReport.length < METADATA_LENGTH) { | ||||||
revert InvalidReport(); | ||||||
} | ||||||
|
||||||
(bytes32 workflowId, uint32 donId, bytes32 workflowExecutionId, address workflowOwner) = _getMetadata(rawReport); | ||||||
(bytes32 workflowExecutionId, uint32 donId /* uint32 donConfigVersion */, , bytes2 reportId) = _getMetadata( | ||||||
rawReport | ||||||
); | ||||||
|
||||||
// f can never be 0, so this means the config doesn't actually exist | ||||||
if (s_configs[donId].f == 0) revert InvalidDonId(donId); | ||||||
|
||||||
bytes32 reportId = _reportId(receiverAddress, workflowExecutionId); | ||||||
if (s_reports[reportId].transmitter != address(0)) revert ReportAlreadyProcessed(reportId); | ||||||
bytes32 combinedId = _combinedId(receiverAddress, workflowExecutionId, reportId); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Inconsistent naming
Suggested change
I would prefer transmissionId. We can fix in a follow-up. |
||||||
if (s_reports[combinedId].transmitter != address(0)) revert AlreadyProcessed(combinedId); | ||||||
|
||||||
if (s_configs[donId].f + 1 != signatures.length) | ||||||
revert InvalidSignatureCount(s_configs[donId].f + 1, signatures.length); | ||||||
|
@@ -169,28 +167,37 @@ contract KeystoneForwarder is IForwarder, ConfirmedOwner, TypeAndVersionInterfac | |||||
} | ||||||
|
||||||
bool success; | ||||||
try IReceiver(receiverAddress).onReport(workflowId, workflowOwner, rawReport[REPORT_METADATA_LENGTH:]) { | ||||||
try | ||||||
IReceiver(receiverAddress).onReport( | ||||||
rawReport[FORWARDER_METADATA_LENGTH:METADATA_LENGTH], | ||||||
rawReport[METADATA_LENGTH:] | ||||||
) | ||||||
{ | ||||||
success = true; | ||||||
} catch { | ||||||
// Do nothing, success is already false | ||||||
} | ||||||
|
||||||
s_reports[reportId] = DeliveryStatus(msg.sender, success); | ||||||
|
||||||
emit ReportProcessed(receiverAddress, workflowOwner, workflowExecutionId, success); | ||||||
s_reports[combinedId] = DeliveryStatus(msg.sender, success); | ||||||
emit ReportProcessed(receiverAddress, workflowExecutionId, success); | ||||||
} | ||||||
|
||||||
function _reportId(address receiver, bytes32 workflowExecutionId) internal pure returns (bytes32) { | ||||||
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)); | ||||||
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(address receiver, bytes32 workflowExecutionId) external view returns (address) { | ||||||
bytes32 reportId = _reportId(receiver, workflowExecutionId); | ||||||
return s_reports[reportId].transmitter; | ||||||
function getTransmitter( | ||||||
address receiver, | ||||||
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); | ||||||
|
||||||
|
@@ -213,20 +220,30 @@ contract KeystoneForwarder is IForwarder, ConfirmedOwner, TypeAndVersionInterfac | |||||
} | ||||||
} | ||||||
|
||||||
// solhint-disable-next-line chainlink-solidity/explicit-returns | ||||||
function _getMetadata( | ||||||
bytes memory rawReport | ||||||
) internal pure returns (bytes32 workflowId, uint32 donId, bytes32 workflowExecutionId, address workflowOwner) { | ||||||
) internal pure returns (bytes32 workflowExecutionId, uint32 donId, uint32 donConfigVersion, bytes2 reportId) { | ||||||
// (first 32 bytes of memory contain length of the report) | ||||||
// version // offset 32, size 1 | ||||||
bolekk marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
// workflow_execution_id // offset 33, size 32 | ||||||
// timestamp // offset 65, size 4 | ||||||
// don_id // offset 69, size 4 | ||||||
// don_config_version, // offset 73, size 4 | ||||||
// workflow_cid // offset 77, size 32 | ||||||
// workflow_name // offset 109, size 10 | ||||||
// workflow_owner // offset 119, size 20 | ||||||
// report_name // offset 139, size 2 | ||||||
if (uint8(rawReport[0]) != 1) { | ||||||
revert InvalidVersion(uint8(rawReport[0])); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As discussed, we don't need this validation today. We can fix it in a follow-up. |
||||||
} | ||||||
assembly { | ||||||
// skip first 32 bytes, contains length of the report | ||||||
// first 32 bytes is the workflowId | ||||||
workflowId := mload(add(rawReport, 32)) | ||||||
// next 4 bytes is donId. We shift right by 28 bytes to get the actual value | ||||||
donId := shr(mul(28, 8), mload(add(rawReport, 64))) | ||||||
// next 32 bytes is the workflowExecutionId | ||||||
workflowExecutionId := mload(add(rawReport, 68)) | ||||||
// next 20 bytes is the workflowOwner. We shift right by 12 bytes to get | ||||||
// the actual value | ||||||
workflowOwner := shr(mul(12, 8), mload(add(rawReport, 100))) | ||||||
workflowExecutionId := mload(add(rawReport, 33)) | ||||||
// shift right by 28 bytes to get the actual value | ||||||
donId := shr(mul(28, 8), mload(add(rawReport, 69))) | ||||||
// shift right by 28 bytes to get the actual value | ||||||
donConfigVersion := shr(mul(28, 8), mload(add(rawReport, 73))) | ||||||
reportId := mload(add(rawReport, 139)) | ||||||
} | ||||||
} | ||||||
|
||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is not efficient (I understand it is just an example). We can fix it in a follow-up gas optimization PR.