-
Notifications
You must be signed in to change notification settings - Fork 53
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Co-authored-by: Ryan Hall <[email protected]>
- Loading branch information
Showing
4 changed files
with
336 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} |