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

[KS-331] Single contract serving as router and forwarder #13528

Merged
Show file tree
Hide file tree
Changes from 6 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
48 changes: 24 additions & 24 deletions contracts/gas-snapshots/keystone.gas-snapshot
Original file line number Diff line number Diff line change
Expand Up @@ -78,28 +78,28 @@ CapabilityRegistry_UpdateNodesTest:test_RevertWhen_P2PIDEmpty() (gas: 27291)
CapabilityRegistry_UpdateNodesTest:test_RevertWhen_SignerAddressEmpty() (gas: 26974)
CapabilityRegistry_UpdateNodesTest:test_RevertWhen_UpdatingNodeWithoutCapabilities() (gas: 25538)
CapabilityRegistry_UpdateNodesTest:test_UpdatesNodeInfo() (gas: 158803)
KeystoneForwarder_ReportTest:test_Report_ConfigVersion() (gas: 1807115)
KeystoneForwarder_ReportTest:test_Report_FailedDeliveryWhenReceiverInterfaceNotSupported() (gas: 132145)
KeystoneForwarder_ReportTest:test_Report_FailedDeliveryWhenReceiverNotContract() (gas: 133638)
KeystoneForwarder_ReportTest:test_Report_SuccessfulDelivery() (gas: 162153)
KeystoneForwarder_ReportTest:test_RevertWhen_AlreadyAttempted() (gas: 157192)
KeystoneForwarder_ReportTest:test_RevertWhen_AnySignatureIsInvalid() (gas: 86392)
KeystoneForwarder_ReportTest:test_RevertWhen_AnySignerIsInvalid() (gas: 118530)
KeystoneForwarder_ReportTest:test_RevertWhen_ReportHasDuplicateSignatures() (gas: 94560)
KeystoneForwarder_ReportTest:test_RevertWhen_ReportHasIncorrectDON() (gas: 75914)
KeystoneForwarder_ReportTest:test_RevertWhen_ReportHasInexistentConfigVersion() (gas: 76282)
KeystoneForwarder_ReportTest:test_RevertWhen_ReportIsMalformed() (gas: 45569)
KeystoneForwarder_ReportTest:test_RevertWhen_TooFewSignatures() (gas: 55282)
KeystoneForwarder_ReportTest:test_RevertWhen_TooManySignatures() (gas: 56028)
KeystoneForwarder_SetConfigTest:test_RevertWhen_ExcessSigners() (gas: 20162)
KeystoneForwarder_SetConfigTest:test_RevertWhen_FaultToleranceIsZero() (gas: 90143)
KeystoneForwarder_SetConfigTest:test_RevertWhen_InsufficientSigners() (gas: 14511)
KeystoneForwarder_SetConfigTest:test_RevertWhen_NotOwner() (gas: 90874)
KeystoneForwarder_SetConfigTest:test_RevertWhen_ProvidingDuplicateSigners() (gas: 116644)
KeystoneForwarder_SetConfigTest:test_SetConfig_FirstTime() (gas: 1543588)
KeystoneForwarder_SetConfigTest:test_SetConfig_WhenSignersAreRemoved() (gas: 1540255)
KeystoneForwarder_ReportTest:test_Report_ConfigVersion() (gas: 1799858)
KeystoneForwarder_ReportTest:test_Report_FailedDeliveryWhenReceiverInterfaceNotSupported() (gas: 123879)
KeystoneForwarder_ReportTest:test_Report_FailedDeliveryWhenReceiverNotContract() (gas: 125372)
KeystoneForwarder_ReportTest:test_Report_SuccessfulDelivery() (gas: 153887)
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

8k gas drop

KeystoneForwarder_ReportTest:test_RevertWhen_AlreadyAttempted() (gas: 150296)
KeystoneForwarder_ReportTest:test_RevertWhen_AnySignatureIsInvalid() (gas: 86398)
KeystoneForwarder_ReportTest:test_RevertWhen_AnySignerIsInvalid() (gas: 118536)
KeystoneForwarder_ReportTest:test_RevertWhen_ReportHasDuplicateSignatures() (gas: 94566)
KeystoneForwarder_ReportTest:test_RevertWhen_ReportHasIncorrectDON() (gas: 75920)
KeystoneForwarder_ReportTest:test_RevertWhen_ReportHasInexistentConfigVersion() (gas: 76288)
KeystoneForwarder_ReportTest:test_RevertWhen_ReportIsMalformed() (gas: 45575)
KeystoneForwarder_ReportTest:test_RevertWhen_TooFewSignatures() (gas: 55288)
KeystoneForwarder_ReportTest:test_RevertWhen_TooManySignatures() (gas: 56034)
KeystoneForwarder_SetConfigTest:test_RevertWhen_ExcessSigners() (gas: 20184)
KeystoneForwarder_SetConfigTest:test_RevertWhen_FaultToleranceIsZero() (gas: 90165)
KeystoneForwarder_SetConfigTest:test_RevertWhen_InsufficientSigners() (gas: 14533)
KeystoneForwarder_SetConfigTest:test_RevertWhen_NotOwner() (gas: 90896)
KeystoneForwarder_SetConfigTest:test_RevertWhen_ProvidingDuplicateSigners() (gas: 116678)
KeystoneForwarder_SetConfigTest:test_SetConfig_FirstTime() (gas: 1543982)
KeystoneForwarder_SetConfigTest:test_SetConfig_WhenSignersAreRemoved() (gas: 1540964)
KeystoneForwarder_TypeAndVersionTest:test_TypeAndVersion() (gas: 9641)
KeystoneRouter_SetConfigTest:test_AddForwarder_RevertWhen_NotOwner() (gas: 10956)
KeystoneRouter_SetConfigTest:test_RemoveForwarder_RevertWhen_NotOwner() (gas: 10879)
KeystoneRouter_SetConfigTest:test_Route_RevertWhen_Unauthorized() (gas: 18509)
KeystoneRouter_SetConfigTest:test_Route_Success() (gas: 75585)
KeystoneRouter_SetConfigTest:test_AddForwarder_RevertWhen_NotOwner() (gas: 10978)
KeystoneRouter_SetConfigTest:test_RemoveForwarder_RevertWhen_NotOwner() (gas: 10923)
KeystoneRouter_SetConfigTest:test_Route_RevertWhen_UnauthorizedForwarder() (gas: 18594)
KeystoneRouter_SetConfigTest:test_Route_Success() (gas: 75670)
1 change: 0 additions & 1 deletion contracts/scripts/native_solc_compile_all_keystone
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,4 @@ compileContract () {

compileContract keystone/CapabilityRegistry.sol
compileContract keystone/KeystoneForwarder.sol
compileContract keystone/KeystoneRouter.sol
compileContract keystone/OCR3Capability.sol
130 changes: 88 additions & 42 deletions contracts/src/v0.8/keystone/KeystoneForwarder.sol
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import {ITypeAndVersion} from "../shared/interfaces/ITypeAndVersion.sol";
import {IReceiver} from "./interfaces/IReceiver.sol";
import {IRouter} from "./interfaces/IRouter.sol";
import {IForwarder} from "./interfaces/IForwarder.sol";
import {ITypeAndVersion} from "../shared/interfaces/ITypeAndVersion.sol";

import {OwnerIsCreator} from "../shared/access/OwnerIsCreator.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, OwnerIsCreator, ITypeAndVersion {
contract KeystoneForwarder is OwnerIsCreator, ITypeAndVersion, IRouter {
/// @notice This error is returned when the report is shorter than
/// REPORT_METADATA_LENGTH, which is the minimum length of a report.
error InvalidReport();
Expand Down Expand Up @@ -64,8 +64,6 @@ contract KeystoneForwarder is IForwarder, OwnerIsCreator, ITypeAndVersion {
mapping(address signer => uint256 position) _positions; // 1-indexed to detect unset values
}

address internal s_router;

/// @notice Contains the configuration for each DON ID
// @param configId (uint64(donId) << 32) | configVersion
mapping(uint64 configId => OracleSet) internal s_configs;
Expand All @@ -81,17 +79,95 @@ contract KeystoneForwarder is IForwarder, OwnerIsCreator, ITypeAndVersion {
bool result
);

string public constant override typeAndVersion = "KeystoneForwarder 1.0.0";

constructor(address router) OwnerIsCreator() {
s_router = router;
}

string public constant override typeAndVersion = "Forwarder and Router 1.0.0";
uint256 internal constant MAX_ORACLES = 31;
uint256 internal constant METADATA_LENGTH = 109;
uint256 internal constant FORWARDER_METADATA_LENGTH = 45;
uint256 internal constant SIGNATURE_LENGTH = 65;

constructor() OwnerIsCreator() {}
Copy link
Contributor

Choose a reason for hiding this comment

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

Isn't this supposed to keep a router address? I assumed we'll deploy two forwarders: one to act as the forwarder, one to act as the router

Copy link
Contributor

Choose a reason for hiding this comment

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

I was imagining the initial deployment was one contract, and then when we need to update the functionality we transition from using the first contract as a forwarder to using it as a router.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

We'd deploy only one contract initially. If/once we need to upgrade, we'd create a new contract with the updates, and that contract would store this contract's address as the router address.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Having said this, we wouldn't be able to stop the current contract from sending. Let me make an update...


// ================================================================
// │ IRouter │
Copy link
Contributor

Choose a reason for hiding this comment

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

IRouter -> Router? (This is the implementation I believe, not the interface)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Will follow-up in rename.

// ================================================================

mapping(address forwarder => bool) internal s_forwarders;
mapping(bytes32 transmissionId => TransmissionInfo) internal s_transmissions;

function addForwarder(address forwarder) external onlyOwner {
s_forwarders[forwarder] = true;
emit ForwarderAdded(forwarder);
}

function removeForwarder(address forwarder) external onlyOwner {
s_forwarders[forwarder] = false;
emit ForwarderRemoved(forwarder);
}

function route(
bytes32 transmissionId,
address transmitter,
address receiver,
bytes calldata metadata,
bytes calldata validatedReport
) public returns (bool) {
if (msg.sender != address(this) && !s_forwarders[msg.sender]) {
revert UnauthorizedForwarder();
}

if (s_transmissions[transmissionId].transmitter != address(0)) revert AlreadyAttempted(transmissionId);
s_transmissions[transmissionId].transmitter = transmitter;

if (receiver.code.length == 0) return false;

try IReceiver(receiver).onReport(metadata, validatedReport) {
s_transmissions[transmissionId].success = true;
return true;
} catch {
return false;
}
}

function getTransmissionId(
address receiver,
bytes32 workflowExecutionId,
bytes2 reportId
) public pure returns (bytes32) {
// This is slightly cheaper compared to
// keccak256(abi.encode(receiver, workflowExecutionId, reportId));
return keccak256(bytes.concat(bytes20(uint160(receiver)), workflowExecutionId, reportId));
}

/// @notice Get transmitter of a given report or 0x0 if it wasn't transmitted yet
function getTransmitter(
address receiver,
bytes32 workflowExecutionId,
bytes2 reportId
) external view returns (address) {
return s_transmissions[getTransmissionId(receiver, workflowExecutionId, reportId)].transmitter;
}

/// @notice Get delivery status of a given report
function getTransmissionState(
address receiver,
bytes32 workflowExecutionId,
bytes2 reportId
) external view returns (IRouter.TransmissionState) {
bytes32 transmissionId = getTransmissionId(receiver, workflowExecutionId, reportId);

if (s_transmissions[transmissionId].transmitter == address(0)) return IRouter.TransmissionState.NOT_ATTEMPTED;
return
s_transmissions[transmissionId].success ? IRouter.TransmissionState.SUCCEEDED : IRouter.TransmissionState.FAILED;
}

function isForwarder(address forwarder) external view returns (bool) {
return s_forwarders[forwarder];
}

// ================================================================
// │ Forwarder │
// ================================================================

function setConfig(uint32 donId, uint32 configVersion, uint8 f, address[] calldata signers) external onlyOwner {
if (f == 0) revert FaultToleranceMustBePositive();
if (signers.length > MAX_ORACLES) revert ExcessSigners(signers.length, MAX_ORACLES);
Expand Down Expand Up @@ -170,7 +246,7 @@ contract KeystoneForwarder is IForwarder, OwnerIsCreator, ITypeAndVersion {
}
}

bool success = IRouter(s_router).route(
bool success = this.route(
getTransmissionId(receiver, workflowExecutionId, reportId),
msg.sender,
receiver,
Expand All @@ -181,36 +257,6 @@ contract KeystoneForwarder is IForwarder, OwnerIsCreator, ITypeAndVersion {
emit ReportProcessed(receiver, workflowExecutionId, reportId, success);
}

function getTransmitter(
address receiver,
bytes32 workflowExecutionId,
bytes2 reportId
) external view returns (address) {
return IRouter(s_router).getTransmitter(getTransmissionId(receiver, workflowExecutionId, reportId));
}

function getTransmissionState(
address receiver,
bytes32 workflowExecutionId,
bytes2 reportId
) external view returns (IRouter.TransmissionState) {
return IRouter(s_router).getTransmissionState(getTransmissionId(receiver, workflowExecutionId, reportId));
}

function getTransmissionId(
address receiver,
bytes32 workflowExecutionId,
bytes2 reportId
) public pure returns (bytes32) {
// This is slightly cheaper compared to
// keccak256(abi.encode(receiver, workflowExecutionId, reportId));
return keccak256(bytes.concat(bytes20(uint160(receiver)), workflowExecutionId, reportId));
}

function getRouter() external view returns (address) {
return s_router;
}

// solhint-disable-next-line chainlink-solidity/explicit-returns
function _getMetadata(
bytes memory rawReport
Expand Down
76 changes: 0 additions & 76 deletions contracts/src/v0.8/keystone/KeystoneRouter.sol

This file was deleted.

5 changes: 0 additions & 5 deletions contracts/src/v0.8/keystone/interfaces/IForwarder.sol

This file was deleted.

32 changes: 30 additions & 2 deletions contracts/src/v0.8/keystone/interfaces/IRouter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,26 @@ pragma solidity ^0.8.19;

/// @title IRouter - delivers keystone reports to receiver
interface IRouter {
error UnauthorizedForwarder();
error AlreadyAttempted(bytes32 transmissionId);

event ForwarderAdded(address indexed forwarder);
event ForwarderRemoved(address indexed forwarder);

enum TransmissionState {
NOT_ATTEMPTED,
SUCCEEDED,
FAILED
}

struct TransmissionInfo {
address transmitter;
bool success;
}

function addForwarder(address forwarder) external;
function removeForwarder(address forwarder) external;

function route(
bytes32 transmissionId,
address transmitter,
Expand All @@ -17,6 +31,20 @@ interface IRouter {
bytes calldata report
) external returns (bool);

function getTransmitter(bytes32 transmissionId) external view returns (address);
function getTransmissionState(bytes32 transmissionId) external view returns (TransmissionState);
function getTransmissionId(
address receiver,
bytes32 workflowExecutionId,
bytes2 reportId
) external pure returns (bytes32);
function getTransmitter(
address receiver,
bytes32 workflowExecutionId,
bytes2 reportId
) external view returns (address);
function getTransmissionState(
address receiver,
bytes32 workflowExecutionId,
bytes2 reportId
) external view returns (TransmissionState);
function isForwarder(address forwarder) external view returns (bool);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ pragma solidity ^0.8.19;

import {Test} from "forge-std/Test.sol";
import {Receiver} from "./mocks/Receiver.sol";
import {KeystoneRouter} from "../KeystoneRouter.sol";
import {KeystoneForwarder} from "../KeystoneForwarder.sol";

contract BaseTest is Test {
Expand All @@ -21,13 +20,13 @@ contract BaseTest is Test {

Signer[MAX_ORACLES] internal s_signers;
KeystoneForwarder internal s_forwarder;
KeystoneRouter internal s_router;
KeystoneForwarder internal s_router;
Receiver internal s_receiver;

function setUp() public virtual {
vm.startPrank(ADMIN);
s_router = new KeystoneRouter();
s_forwarder = new KeystoneForwarder(address(s_router));
s_router = new KeystoneForwarder();
s_forwarder = new KeystoneForwarder();
s_router.addForwarder(address(s_forwarder));
s_receiver = new Receiver();

Expand Down
Loading
Loading