diff --git a/contracts/.solhintignore b/contracts/.solhintignore index bad1935442..446f91f84f 100644 --- a/contracts/.solhintignore +++ b/contracts/.solhintignore @@ -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 \ No newline at end of file diff --git a/contracts/gas-snapshots/ccip.gas-snapshot b/contracts/gas-snapshots/ccip.gas-snapshot index 3c9bbec852..9680750a88 100644 --- a/contracts/gas-snapshots/ccip.gas-snapshot +++ b/contracts/gas-snapshots/ccip.gas-snapshot @@ -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) diff --git a/contracts/src/v0.8/ccip/RMNHome.sol b/contracts/src/v0.8/ccip/RMNHome.sol new file mode 100644 index 0000000000..f28a5da95b --- /dev/null +++ b/contracts/src/v0.8/ccip/RMNHome.sol @@ -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(); +} diff --git a/contracts/src/v0.8/ccip/RMNRemote.sol b/contracts/src/v0.8/ccip/RMNRemote.sol new file mode 100644 index 0000000000..19b7f89e1b --- /dev/null +++ b/contracts/src/v0.8/ccip/RMNRemote.sol @@ -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(); +}