Skip to content

Commit

Permalink
RMN home & remote contracts (#1308)
Browse files Browse the repository at this point in the history
Co-authored-by: Ryan Hall <[email protected]>
  • Loading branch information
gtklocker and RyanRHall authored Aug 23, 2024
1 parent 7d29c2e commit e9e71f5
Show file tree
Hide file tree
Showing 4 changed files with 336 additions and 0 deletions.
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";

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
/// 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 {
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) {
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) {
// unset config
continue;
}
VersionedConfig memory vc = VersionedConfig({version: s_configCounts[i], config: s_configs[i]});
if (_configDigest(vc) == configDigest) {
versionedConfig = vc;
ok = true;
break;
}
}
}

///
/// Events
///

event ConfigSet(bytes32 configDigest, VersionedConfig versionedConfig);
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.
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
uint64 minMsgNr;
uint64 maxMsgNr;
bytes32 root;
}

struct Report {
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();
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();
}

0 comments on commit e9e71f5

Please sign in to comment.