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

RMN home & remote contracts #1308

Merged
merged 6 commits into from
Aug 23, 2024
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
4 changes: 4 additions & 0 deletions contracts/.solhintignore
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,7 @@
# Always ignore vendor
./src/v0.8/vendor
./node_modules/

# Ignore RMN contracts temporarily
./src/v0.8/ccip/RMNRemote.sol
./src/v0.8/ccip/RMNHome.sol
1 change: 1 addition & 0 deletions contracts/gas-snapshots/ccip.gas-snapshot
Original file line number Diff line number Diff line change
Expand Up @@ -772,6 +772,7 @@ PriceRegistry_validatePoolReturnData:test_InvalidEVMAddressDestToken_Revert() (g
PriceRegistry_validatePoolReturnData:test_SourceTokenDataTooLarge_Revert() (gas: 90819)
PriceRegistry_validatePoolReturnData:test_TokenAmountArraysMismatching_Revert() (gas: 32771)
PriceRegistry_validatePoolReturnData:test_WithSingleToken_Success() (gas: 31315)
RMNHome:test() (gas: 186)
RMN_constructor:test_Constructor_Success() (gas: 48838)
RMN_getRecordedCurseRelatedOps:test_OpsPostDeployment() (gas: 19666)
RMN_lazyVoteToCurseUpdate_Benchmark:test_VoteToCurseLazilyRetain3VotersUponConfigChange_gas() (gas: 152152)
Expand Down
164 changes: 164 additions & 0 deletions contracts/src/v0.8/ccip/RMNHome.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.24;

import "@openzeppelin/contracts/access/Ownable2Step.sol";
RyanRHall marked this conversation as resolved.
Show resolved Hide resolved

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

/// @notice Stores the home configuration for RMN, that is referenced by CCIP oracles, RMN nodes, and the RMNRemote
Copy link
Collaborator

Choose a reason for hiding this comment

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

q: should this conform to the Keystone Capability interface, just like the CCIPConfig contract?

/// contracts.
contract RMNHome is Ownable2Step, ITypeAndVersion {
/// @dev temp placeholder to exclude this contract from coverage
function test() public {}

string public constant override typeAndVersion = "RMNHome 1.6.0-dev";
uint256 public constant CONFIG_RING_BUFFER_SIZE = 2;

struct Node {
Copy link
Collaborator

Choose a reason for hiding this comment

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

q: any reason RMN shouldn't be a capability in Keystone? This way you can manage DON / node configurations directly in the CapabilitiesRegistry

string peerId; // used for p2p communication, base58 encoded
bytes32 offchainPublicKey; // observations are signed with this public key, and are only verified offchain
}

struct SourceChain {
uint64 chainSelector;
uint64[] observerNodeIndices; // indices into Config.nodes, strictly increasing
uint64 minObservers; // required to agree on an observation for this source chain
}

struct Config {
// No sorting requirement for nodes, but ensure that SourceChain.observerNodeIndices in the home chain config &
// Signer.nodeIndex in the remote chain configs are appropriately updated when changing this field
Node[] nodes;
// Should be in ascending order of chainSelector
SourceChain[] sourceChains;
}

struct VersionedConfig {
uint32 version;
Config config;
}

function _configDigest(VersionedConfig memory versionedConfig) internal pure returns (bytes32) {
uint256 h = uint256(keccak256(abi.encode(versionedConfig)));
uint256 prefixMask = type(uint256).max << (256 - 16); // 0xFFFF00..00
uint256 prefix = 0x000b << (256 - 16); // 0x000b00..00
return bytes32((prefix & prefixMask) | (h & ~prefixMask));
}

// if we were to have VersionedConfig instead of Config in the ring buffer, we couldn't assign directly to it in
// setConfig without via-ir
uint32[CONFIG_RING_BUFFER_SIZE] s_configCounts; // s_configCounts[i] == 0 iff s_configs[i] is unusable
Config[CONFIG_RING_BUFFER_SIZE] s_configs;
uint256 s_latestConfigIndex;
bytes32 s_latestConfigDigest;

/// @param revokePastConfigs if one wants to revoke all past configs, because some past config is faulty
function setConfig(Config calldata newConfig, bool revokePastConfigs) external onlyOwner {
// sanity checks
{
// no peerId or offchainPublicKey is duplicated
for (uint256 i = 0; i < newConfig.nodes.length; ++i) {
for (uint256 j = i + 1; j < newConfig.nodes.length; ++j) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

q: how large can the nodes array be, and how much gas would the largest case use?

if (keccak256(abi.encode(newConfig.nodes[i].peerId)) == keccak256(abi.encode(newConfig.nodes[j].peerId))) {
revert DuplicatePeerId();
}
if (newConfig.nodes[i].offchainPublicKey == newConfig.nodes[j].offchainPublicKey) {
revert DuplicateOffchainPublicKey();
}
}
}

for (uint256 i = 0; i < newConfig.sourceChains.length; ++i) {
// source chains are in strictly increasing order of chain selectors
if (i > 0 && !(newConfig.sourceChains[i - 1].chainSelector < newConfig.sourceChains[i].chainSelector)) {
revert OutOfOrderSourceChains();
}

// all observerNodeIndices are valid
for (uint256 j = 0; j < newConfig.sourceChains[i].observerNodeIndices.length; ++j) {
if (
j > 0
&& !(newConfig.sourceChains[i].observerNodeIndices[j - 1] < newConfig.sourceChains[i].observerNodeIndices[j])
) {
revert OutOfOrderObserverNodeIndices();
}
if (!(newConfig.sourceChains[i].observerNodeIndices[j] < newConfig.nodes.length)) {
revert OutOfBoundsObserverNodeIndex();
}
}

// minObservers are tenable
if (!(newConfig.sourceChains[i].minObservers <= newConfig.sourceChains[i].observerNodeIndices.length)) {
revert MinObserversTooHigh();
}
}
}

uint256 oldConfigIndex = s_latestConfigIndex;
uint32 oldConfigCount = s_configCounts[oldConfigIndex];
uint256 newConfigIndex = (oldConfigIndex + 1) % CONFIG_RING_BUFFER_SIZE;

for (uint256 i = 0; i < CONFIG_RING_BUFFER_SIZE; ++i) {
if ((i == newConfigIndex || revokePastConfigs) && s_configCounts[i] > 0) {
emit ConfigRevoked(_configDigest(VersionedConfig({version: s_configCounts[i], config: s_configs[i]})));
delete s_configCounts[i];
}
}

uint32 newConfigCount = oldConfigCount + 1;
VersionedConfig memory newVersionedConfig = VersionedConfig({version: newConfigCount, config: newConfig});
bytes32 newConfigDigest = _configDigest(newVersionedConfig);
s_configs[newConfigIndex] = newConfig;
s_configCounts[newConfigIndex] = newConfigCount;
s_latestConfigIndex = newConfigIndex;
s_latestConfigDigest = newConfigDigest;
emit ConfigSet(newConfigDigest, newVersionedConfig);
}

/// @return configDigest will be zero in case no config has been set
function getLatestConfigDigestAndVersionedConfig()
external
view
returns (bytes32 configDigest, VersionedConfig memory)
{
return (
s_latestConfigDigest,
VersionedConfig({version: s_configCounts[s_latestConfigIndex], config: s_configs[s_latestConfigIndex]})
);
}

/// @notice The offchain code can use this to fetch an old config which might still be in use by some remotes
/// @dev Only to be called by offchain code, efficiency is not a concern
function getConfig(bytes32 configDigest) external view returns (VersionedConfig memory versionedConfig, bool ok) {
for (uint256 i = 0; i < CONFIG_RING_BUFFER_SIZE; ++i) {
if (s_configCounts[i] == 0) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

q: how many max versions do we expect? Would we ever need to paginate / offset to not reach RPC timeouts due to too many versions?

// unset config
continue;
}
VersionedConfig memory vc = VersionedConfig({version: s_configCounts[i], config: s_configs[i]});
if (_configDigest(vc) == configDigest) {
versionedConfig = vc;
ok = true;
break;
}
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: from style guide, we should explicitly return (versionedConfig, ok)


///
/// Events
///

event ConfigSet(bytes32 configDigest, VersionedConfig versionedConfig);
RyanRHall marked this conversation as resolved.
Show resolved Hide resolved
event ConfigRevoked(bytes32 configDigest);

///
/// Errors
///

error DuplicatePeerId();
error DuplicateOffchainPublicKey();
error OutOfOrderSourceChains();
error OutOfOrderObserverNodeIndices();
error OutOfBoundsObserverNodeIndex();
error MinObserversTooHigh();
}
167 changes: 167 additions & 0 deletions contracts/src/v0.8/ccip/RMNRemote.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.24;

import "@openzeppelin/contracts/access/Ownable2Step.sol";

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

bytes32 constant RMN_V1_6_ANY2EVM_REPORT = keccak256("RMN_V1_6_ANY2EVM_REPORT");

/// @notice This contract supports verification of RMN reports for any Any2EVM OffRamp.
elatoskinas marked this conversation as resolved.
Show resolved Hide resolved
contract RMNRemote is Ownable2Step, ITypeAndVersion {
/// @dev temp placeholder to exclude this contract from coverage
function test() public {}

string public constant override typeAndVersion = "RMNRemote 1.6.0-dev";

uint64 internal immutable i_chainSelector;

constructor(uint64 chainSelector) {
i_chainSelector = chainSelector;
}

struct Signer {
address onchainPublicKey; // for signing reports
uint64 nodeIndex; // maps to nodes in home chain config, should be strictly increasing
}

struct Config {
bytes32 rmnHomeContractConfigDigest;
Signer[] signers;
uint64 minSigners;
}

struct VersionedConfig {
uint32 version;
Config config;
}

Config s_config;
uint32 s_configCount;

mapping(address signer => bool exists) s_signers; // for more gas efficient verify

function setConfig(Config calldata newConfig) external onlyOwner {
// sanity checks
{
// signers are in ascending order of nodeIndex
for (uint256 i = 1; i < newConfig.signers.length; ++i) {
if (!(newConfig.signers[i - 1].nodeIndex < newConfig.signers[i].nodeIndex)) {
revert InvalidSignerOrder();
}
}

// minSigners is tenable
if (!(newConfig.minSigners <= newConfig.signers.length)) {
revert MinSignersTooHigh();
}
}

// clear the old signers
{
Config storage oldConfig = s_config;
while (oldConfig.signers.length > 0) {
delete s_signers[oldConfig.signers[oldConfig.signers.length - 1].onchainPublicKey];
oldConfig.signers.pop();
}
}

// set the new signers
{
for (uint256 i = 0; i < newConfig.signers.length; ++i) {
if (s_signers[newConfig.signers[i].onchainPublicKey]) {
revert DuplicateOnchainPublicKey();
}
s_signers[newConfig.signers[i].onchainPublicKey] = true;
}
}

s_config = newConfig;
uint32 newConfigCount = ++s_configCount;
emit ConfigSet(VersionedConfig({version: newConfigCount, config: newConfig}));
}

function getVersionedConfig() external view returns (VersionedConfig memory) {
return VersionedConfig({version: s_configCount, config: s_config});
}

/// @notice The part of the LaneUpdate for a fixed destination chain and OffRamp, to avoid verbosity in Report
struct DestLaneUpdate {
uint64 sourceChainSelector;
bytes onrampAddress; // generic, to support arbitrary sources; for EVM2EVM, use abi.encodePacked
elatoskinas marked this conversation as resolved.
Show resolved Hide resolved
uint64 minMsgNr;
elatoskinas marked this conversation as resolved.
Show resolved Hide resolved
uint64 maxMsgNr;
elatoskinas marked this conversation as resolved.
Show resolved Hide resolved
bytes32 root;
}

struct Report {
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: should we name it RMNReport?

uint256 destChainId; // to guard against chain selector misconfiguration
uint64 destChainSelector;
address rmnRemoteContractAddress;
address offrampAddress;
bytes32 rmnHomeContractConfigDigest;
DestLaneUpdate[] destLaneUpdates;
}

struct Signature {
bytes32 r;
bytes32 s;
}

/// @notice Verifies signatures of RMN nodes, on dest lane updates as provided in the CommitReport
/// @param destLaneUpdates must be well formed, and is a representation of the CommitReport received from the oracles
/// @param signatures must be sorted in ascending order by signer address
/// @dev Will revert if verification fails. Needs to be called by the OffRamp for which the signatures are produced,
/// otherwise verification will fail.
function verify(DestLaneUpdate[] memory destLaneUpdates, Signature[] memory signatures) external view {
if (s_configCount == 0) {
revert ConfigNotSet();
}

bytes32 signedHash = keccak256(
abi.encode(
RMN_V1_6_ANY2EVM_REPORT,
Report({
destChainId: block.chainid,
destChainSelector: i_chainSelector,
rmnRemoteContractAddress: address(this),
offrampAddress: msg.sender,
rmnHomeContractConfigDigest: s_config.rmnHomeContractConfigDigest,
destLaneUpdates: destLaneUpdates
})
)
);

uint256 numSigners = 0;
address prevAddress = address(0);
for (uint256 i = 0; i < signatures.length; ++i) {
Signature memory sig = signatures[i];
address signerAddress = ecrecover(signedHash, 27, sig.r, sig.s);
if (signerAddress == address(0)) revert InvalidSignature();
if (!(prevAddress < signerAddress)) revert OutOfOrderSignatures();
Copy link
Collaborator

Choose a reason for hiding this comment

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

q: is sorting feasible, and more gas efficient, as opposed to the OCR2/3Base approach of maintaining a signature-by-index array? https://github.com/smartcontractkit/chainlink/blob/develop/contracts/src/v0.8/ccip/ocr/MultiOCR3Base.sol#L297

if (!s_signers[signerAddress]) revert UnexpectedSigner();
prevAddress = signerAddress;
++numSigners;
}
if (numSigners < s_config.minSigners) revert ThresholdNotMet();
}

///
/// Events
///

event ConfigSet(VersionedConfig versionedConfig);

///
/// Errors
///

error InvalidSignature();
error OutOfOrderSignatures();
error UnexpectedSigner();
error ThresholdNotMet();
error ConfigNotSet();
error InvalidSignerOrder();
error MinSignersTooHigh();
error DuplicateOnchainPublicKey();
}
Loading