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

[WIP] RMNHome #1443

Closed
wants to merge 40 commits into from
Closed
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
32000bf
fix pragma to 0.8.24
RyanRHall Sep 6, 2024
7cea609
switch to OwnerIsCreator
RyanRHall Sep 6, 2024
e915acc
remove XXX comments and test functions
RyanRHall Sep 6, 2024
1061999
reorder imports
RyanRHall Sep 6, 2024
be026b6
reorder contract contents
RyanRHall Sep 6, 2024
3dc38c2
create RMNRemote.t.sol and RMNHome.t.sol
RyanRHall Sep 6, 2024
23b02e5
setup RMNHome test
RyanRHall Sep 6, 2024
b7627f5
add RMNRemote constructor test
RyanRHall Sep 6, 2024
f138f21
add setConfig success test for RMNRemote
RyanRHall Sep 9, 2024
d672d82
write setconfig tests
RyanRHall Sep 9, 2024
c909164
add initial verify() tests
RyanRHall Sep 9, 2024
1588379
move RMNRemoteSetup contract to it's own file; pre-sort signing addre…
RyanRHall Sep 10, 2024
ebfee0c
add out of order / duplicate sig tests
RyanRHall Sep 10, 2024
a5c299d
add test_verify_unknownSigner_reverts
RyanRHall Sep 10, 2024
340e24f
write test_verify_insufficientSignatures_reverts
RyanRHall Sep 10, 2024
854ce20
refactor verify() tests
RyanRHall Sep 10, 2024
1f7fba3
incorporate Kostis's RMNRemote changes
RyanRHall Sep 10, 2024
6c592f3
refactor
RyanRHall Sep 10, 2024
9ce03aa
rename curse functions
RyanRHall Sep 11, 2024
156fd67
write tests for cursing / uncursing
RyanRHall Sep 11, 2024
8f44b59
add RMNRemote_global_and_legacy_curses
RyanRHall Sep 11, 2024
0274861
update comments
RyanRHall Sep 11, 2024
14fca9e
move natspec from RMNRemote to IRMNV2
RyanRHall Sep 11, 2024
1e31a6f
add natspec @dev comments to structs
RyanRHall Sep 12, 2024
4ff5f3b
improve natspec
RyanRHall Sep 12, 2024
da72d74
update snapshot & wrappers
RyanRHall Sep 12, 2024
cb8c0c9
ignore RMNHome from coverage checks
RyanRHall Sep 12, 2024
39f6bfa
Update comment
RyanRHall Sep 13, 2024
750deb2
rename i_chainSelector => i_localChainSelector
RyanRHall Sep 13, 2024
4a9a3f3
remove storage / constructor headers
RyanRHall Sep 13, 2024
fb5433d
add comment
RyanRHall Sep 13, 2024
73db111
update comments
RyanRHall Sep 13, 2024
2804858
add new constructor test
RyanRHall Sep 13, 2024
e9f90b8
rmn home baseline
RensR Sep 16, 2024
16e15f4
add tests
RensR Sep 16, 2024
5237a5b
add comments, golf
RensR Sep 16, 2024
f039e73
increase ring buffer size
RensR Sep 16, 2024
af1c04d
Merge f039e73f479ca1fb41ae24dff03148c245df9643 into 2804858b473b732dc…
RensR Sep 16, 2024
d6ba29c
Update gethwrappers
app-token-issuer-infra-releng[bot] Sep 16, 2024
2894740
add only owner tests
RensR Sep 16, 2024
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
399 changes: 217 additions & 182 deletions contracts/gas-snapshots/ccip.gas-snapshot

Large diffs are not rendered by default.

17 changes: 16 additions & 1 deletion contracts/src/v0.8/ccip/interfaces/IRMNV2.sol
Original file line number Diff line number Diff line change
@@ -11,12 +11,27 @@ interface IRMNV2 {
bytes32 s;
}

function verify(Internal.MerkleRoot[] memory merkleRoots, Signature[] memory signatures) external view;
/// @notice Verifies signatures of RMN nodes, on dest lane updates as provided in the CommitReport
/// @param offRampAddress is not inferred by msg.sender, in case the call is made through ARMProxy
/// @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
function verify(
address offRampAddress,
Internal.MerkleRoot[] memory destLaneUpdates,
Signature[] memory signatures
) external view;

/// @notice gets the current set of cursed subjects
/// @return subjects the list of cursed subjects
function getCursedSubjects() external view returns (bytes16[] memory subjects);

/// @notice If there is an active global or legacy curse, this function returns true.
/// @return bool true if there is an active global curse
function isCursed() external view returns (bool);

/// @notice If there is an active global curse, or an active curse for `subject`, this function returns true.
/// @param subject To check whether a particular chain is cursed, set to bytes16(uint128(chainSelector)).
/// @return bool true if the profived subject is cured *or* if there is an active global curse
function isCursed(bytes16 subject) external view returns (bool);
}
2 changes: 1 addition & 1 deletion contracts/src/v0.8/ccip/offRamp/OffRamp.sol
Original file line number Diff line number Diff line change
@@ -626,7 +626,7 @@ contract OffRamp is ITypeAndVersion, MultiOCR3Base {

// Verify RMN signatures
if (commitReport.merkleRoots.length > 0) {
i_rmn.verify(commitReport.merkleRoots, commitReport.rmnSignatures);
i_rmn.verify(address(this), commitReport.merkleRoots, commitReport.rmnSignatures);
}

// Check if the report contains price updates
300 changes: 205 additions & 95 deletions contracts/src/v0.8/ccip/rmn/RMNHome.sol

Large diffs are not rendered by default.

270 changes: 185 additions & 85 deletions contracts/src/v0.8/ccip/rmn/RMNRemote.sol
Original file line number Diff line number Diff line change
@@ -1,48 +1,140 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.24;
pragma solidity 0.8.24;

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

import {OwnerIsCreator} from "../../shared/access/OwnerIsCreator.sol";
import {Internal} from "../libraries/Internal.sol";

/// @dev this is included in the preimage of the digest that RMN nodes sign
bytes32 constant RMN_V1_6_ANY2EVM_REPORT = keccak256("RMN_V1_6_ANY2EVM_REPORT");
Comment on lines +10 to 11
Copy link
Contributor

Choose a reason for hiding this comment

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

Would it make sense to add a getter for this? That way we don't have to duplicate this constant offchain and nodes can fetch it as part of reading from the remote.


/// @dev XXX DO NOT USE THIS CONTRACT, NOT PRODUCTION READY XXX
/// @notice This contract supports verification of RMN reports for any Any2EVM OffRamp.
contract RMNRemote is OwnerIsCreator, ITypeAndVersion, IRMNV2 {
/// @dev temp placeholder to exclude this contract from coverage
function test() public {}
/// @dev An active curse on this subject will cause isCursed() to return true. Use this subject if there is an issue with a
/// remote chain, for which there exists a legacy lane contract deployed on the same chain as this RMN contract is
/// deployed, relying on isCursed().
bytes16 constant LEGACY_CURSE_SUBJECT = 0x01000000000000000000000000000000;

string public constant override typeAndVersion = "RMNRemote 1.6.0-dev";
/// @dev An active curse on this subject will cause isCursed() and isCursed(bytes16) to return true. Use this subject for
/// issues affecting all of CCIP chains, or pertaining to the chain that this contract is deployed on, instead of using
/// the local chain selector as a subject.
bytes16 constant GLOBAL_CURSE_SUBJECT = 0x01000000000000000000000000000001;

uint64 internal immutable i_chainSelector;
/// @notice This contract supports verification of RMN reports for any Any2EVM OffRamp.
contract RMNRemote is OwnerIsCreator, ITypeAndVersion, IRMNV2 {
error AlreadyCursed(bytes16 subject);
error ConfigNotSet();
error DuplicateOnchainPublicKey();
error InvalidSignature();
error InvalidSignerOrder();
error MinSignersTooHigh();
error NotCursed(bytes16 subject);
error OutOfOrderSignatures();
error ThresholdNotMet();
error UnexpectedSigner();
error ZeroValueNotAllowed();

constructor(uint64 chainSelector) {
i_chainSelector = chainSelector;
}
event ConfigSet(VersionedConfig versionedConfig);
event Cursed(bytes16[] subjects);
event Uncursed(bytes16[] subjects);

/// @dev the configuration of an RMN signer
struct Signer {
address onchainPublicKey; // for signing reports
uint64 nodeIndex; // maps to nodes in home chain config, should be strictly increasing
address onchainPublicKey; // ────╮ For signing reports
uint64 nodeIndex; // ────────────╯ Maps to nodes in home chain config, should be strictly increasing
}

/// @dev the contract config
/// @dev note: minSigners can be set to 0 to disable verification for chains without RMN support
struct Config {
bytes32 rmnHomeContractConfigDigest;
Signer[] signers;
uint64 minSigners;
bytes32 rmnHomeContractConfigDigest; // Digest of the RMNHome contract config
Signer[] signers; // List of signers
uint64 minSigners; // Threshold for the number of signers required to verify a report
}

/// @dev the contract config + a version number
struct VersionedConfig {
uint32 version;
Config config;
uint32 version; // For tracking the version of the config
Config config; // The config
}

/// @dev part of the payload that RMN nodes sign: keccak256(abi.encode(RMN_V1_6_ANY2EVM_REPORT, report))
/// @dev this struct is only ever abi-encoded and hashed; it is never stored
struct Report {
uint256 destChainId; // To guard against chain selector misconfiguration
uint64 destChainSelector; // ────────────╮ The chain selector of the destination chain
address rmnRemoteContractAddress; // ─────╯ The address of this contract
address offrampAddress; // The address of the offramp on the same chain as this contract
bytes32 rmnHomeContractConfigDigest; // The digest of the RMNHome contract config
Internal.MerkleRoot[] destLaneUpdates; // The dest lane updates
}

Config s_config;
uint32 s_configCount;

string public constant override typeAndVersion = "RMNRemote 1.6.0-dev";
uint64 internal immutable i_localChainSelector;

bytes16[] private s_cursedSubjectsSequence;
/// @dev the index+1 is stored to easily distinguish b/t noncursed and cursed at the 0 index
mapping(bytes16 subject => uint256 indexPlusOne) private s_cursedSubjectsIndexPlusOne;
mapping(address signer => bool exists) s_signers; // for more gas efficient verify

/// @param localChainSelector the chain selector of the chain this contract is deployed to
constructor(uint64 localChainSelector) {
if (localChainSelector == 0) revert ZeroValueNotAllowed();
i_localChainSelector = localChainSelector;
}

// ================================================================
// │ Verification │
// ================================================================

/// @inheritdoc IRMNV2
function verify(
address offrampAddress,
Internal.MerkleRoot[] 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_localChainSelector,
rmnRemoteContractAddress: address(this),
offrampAddress: offrampAddress,
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();
}

// ================================================================
// │ Config │
// ================================================================

/// @notice Sets the configuration of the contract
/// @param newConfig the new configuration
/// @dev setting congig is atomic; we delete all pre-existing config and set everything from scratch
function setConfig(Config calldata newConfig) external onlyOwner {
// sanity checks
{
@@ -83,86 +175,94 @@ contract RMNRemote is OwnerIsCreator, ITypeAndVersion, IRMNV2 {
emit ConfigSet(VersionedConfig({version: newConfigCount, config: newConfig}));
}

/// @notice Returns the current configuration of the contract + a version number
/// @return versionedConfig the current configuration + version
function getVersionedConfig() external view returns (VersionedConfig memory) {
return VersionedConfig({version: s_configCount, config: s_config});
}

struct Report {
uint256 destChainId; // to guard against chain selector misconfiguration
uint64 destChainSelector;
address rmnRemoteContractAddress;
address offrampAddress;
bytes32 rmnHomeContractConfigDigest;
Internal.MerkleRoot[] destLaneUpdates;
}

/// @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(Internal.MerkleRoot[] memory destLaneUpdates, Signature[] memory signatures) external view {
return; // XXX temporary workaround to fix integration tests while we wait to productionize this contract
/// @notice Returns the chain selector configured at deployment time
/// @return localChainSelector the chain selector (not the chain ID)
function getLocalChainSelector() external view returns (uint64 localChainSelector) {
return i_localChainSelector;
}

if (s_configCount == 0) {
revert ConfigNotSet();
}
// ================================================================
// │ Cursing │
// ================================================================

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
})
)
);
/// @notice Curse a single subject
/// @param subject the subject to curse
function curse(bytes16 subject) external {
bytes16[] memory subjects = new bytes16[](1);
subjects[0] = subject;
curse(subjects);
}

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;
/// @notice Curse an array of subjects
/// @param subjects the subjects to curse
/// @dev reverts if any of the subjects are already cursed or if there is a duplicate
function curse(bytes16[] memory subjects) public onlyOwner {
for (uint256 i = 0; i < subjects.length; ++i) {
bytes16 toCurseSubject = subjects[i];
if (s_cursedSubjectsIndexPlusOne[toCurseSubject] != 0) {
revert AlreadyCursed(toCurseSubject);
}
s_cursedSubjectsSequence.push(toCurseSubject);
s_cursedSubjectsIndexPlusOne[toCurseSubject] = s_cursedSubjectsSequence.length;
}
if (numSigners < s_config.minSigners) revert ThresholdNotMet();
emit Cursed(subjects);
}

/// @notice If there is an active global or legacy curse, this function returns true.
function isCursed() external view returns (bool) {
return false; // XXX temporary workaround
/// @notice Uncurse a single subject
/// @param subject the subject to uncurse
function uncurse(bytes16 subject) external {
bytes16[] memory subjects = new bytes16[](1);
subjects[0] = subject;
uncurse(subjects);
}

/// @notice If there is an active global curse, or an active curse for `subject`, this function returns true.
/// @param subject To check whether a particular chain is cursed, set to bytes16(uint128(chainSelector)).
function isCursed(bytes16 subject) external view returns (bool) {
return false; // XXX temporary workaround
/// @notice Uncurse an array of subjects
/// @param subjects the subjects to uncurse
/// @dev reverts if any of the subjects are not cursed or if there is a duplicate
function uncurse(bytes16[] memory subjects) public onlyOwner {
for (uint256 i = 0; i < subjects.length; ++i) {
bytes16 toUncurseSubject = subjects[i];
uint256 toUncurseSubjectIndexPlusOne = s_cursedSubjectsIndexPlusOne[toUncurseSubject];
if (toUncurseSubjectIndexPlusOne == 0) {
revert NotCursed(toUncurseSubject);
}
uint256 toUncurseSubjectIndex = toUncurseSubjectIndexPlusOne - 1;
// copy the last subject to the position of the subject to uncurse
bytes16 lastSubject = s_cursedSubjectsSequence[s_cursedSubjectsSequence.length - 1];
s_cursedSubjectsSequence[toUncurseSubjectIndex] = lastSubject;
s_cursedSubjectsIndexPlusOne[lastSubject] = toUncurseSubjectIndexPlusOne;
// then pop, since we have the last subject also in toUncurseSubjectIndex
s_cursedSubjectsSequence.pop();
delete s_cursedSubjectsIndexPlusOne[toUncurseSubject];
}
emit Uncursed(subjects);
}

///
/// Events
///

event ConfigSet(VersionedConfig versionedConfig);
/// @inheritdoc IRMNV2
function getCursedSubjects() external view returns (bytes16[] memory subjects) {
return s_cursedSubjectsSequence;
}

///
/// Errors
///
/// @inheritdoc IRMNV2
function isCursed() external view returns (bool) {
if (s_cursedSubjectsSequence.length == 0) {
return false;
}
return
s_cursedSubjectsIndexPlusOne[LEGACY_CURSE_SUBJECT] > 0 || s_cursedSubjectsIndexPlusOne[GLOBAL_CURSE_SUBJECT] > 0;
}

error InvalidSignature();
error OutOfOrderSignatures();
error UnexpectedSigner();
error ThresholdNotMet();
error ConfigNotSet();
error InvalidSignerOrder();
error MinSignersTooHigh();
error DuplicateOnchainPublicKey();
/// @inheritdoc IRMNV2
function isCursed(bytes16 subject) external view returns (bool) {
if (s_cursedSubjectsSequence.length == 0) {
return false;
}
return s_cursedSubjectsIndexPlusOne[subject] > 0 || s_cursedSubjectsIndexPlusOne[GLOBAL_CURSE_SUBJECT] > 0;
}
}
18 changes: 18 additions & 0 deletions contracts/src/v0.8/ccip/test/BaseTest.t.sol
Original file line number Diff line number Diff line change
@@ -73,6 +73,9 @@ contract BaseTest is Test {
MockRMN internal s_mockRMN;
IRMNV2 internal s_mockRMNRemote;

// nonce for pseudo-random number generation, not to be exposed to test suites
uint256 private randNonce;

function setUp() public virtual {
// BaseTest.setUp is often called multiple times from tests' setUp due to inheritance.
if (s_baseTestInitialized) return;
@@ -128,4 +131,19 @@ contract BaseTest is Test {

return priceUpdates;
}

/// @dev returns a pseudo-random bytes32
function _randomBytes32() internal returns (bytes32) {
return keccak256(abi.encodePacked(++randNonce));
}

/// @dev returns a pseudo-random number
function _randomNum() internal returns (uint256) {
return uint256(_randomBytes32());
}

/// @dev returns a pseudo-random address
function _randomAddress() internal returns (address) {
return address(uint160(_randomNum()));
}
}
Loading
Loading