Skip to content

Commit 974def5

Browse files
Change minSigners to f in RMNRemote/RMNHome (#14817)
* switch minsigners to f in RMNRemote * switch minsigners to f in RMNHome * update struck packing comments * change && to nested if * reduce signer requirement from 2f+1 to f+1 * remove enabled flag from RMN * move rmn activation flag to offramp * add changeset * reduce optimizer runs for offramp * [Bot] Update changeset file with jira issues * swap minSigners config for f in offchain code * fix integration test * don't use f=0 in RMNHome tests * fix TestRMNHomeReader_GetRMNNodesInfo * update TestInitialDeploy with timeout and RMN disabled * change commitment timeout to 3 minutes * add error handling to wg.wait() * [Bot] Update changeset file with jira issues * update rmn_test.go * update snapshot & wrappers --------- Co-authored-by: app-token-issuer-infra-releng[bot] <120227048+app-token-issuer-infra-releng[bot]@users.noreply.github.com>
1 parent 4640812 commit 974def5

File tree

21 files changed

+845
-802
lines changed

21 files changed

+845
-802
lines changed
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
'@chainlink/contracts': minor
3+
---
4+
5+
change minSigners to f in RMNRemote/RMNHome
6+
7+
8+
PR issue: CCIP-3614
9+
10+
Solidity Review issue: CCIP-3966

contracts/foundry.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ single_line_statement_blocks = "preserve"
2626
solc_version = '0.8.24'
2727
src = 'src/v0.8/ccip'
2828
test = 'src/v0.8/ccip/test'
29-
optimizer_runs = 800
29+
optimizer_runs = 500
3030
evm_version = 'paris'
3131

3232
[profile.functions]

contracts/gas-snapshots/ccip.gas-snapshot

Lines changed: 693 additions & 694 deletions
Large diffs are not rendered by default.

contracts/scripts/native_solc_compile_all_ccip

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ echo " └───────────────────────
88

99
SOLC_VERSION="0.8.24"
1010
OPTIMIZE_RUNS=26000
11-
OPTIMIZE_RUNS_OFFRAMP=800
11+
OPTIMIZE_RUNS_OFFRAMP=500
1212

1313

1414
SCRIPTPATH="$( cd "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )"

contracts/src/v0.8/ccip/offRamp/OffRamp.sol

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,8 @@ contract OffRamp is ITypeAndVersion, MultiOCR3Base {
123123
/// @dev Since DynamicConfig is part of DynamicConfigSet event, if changing it, we should update the ABI on Atlas
124124
struct DynamicConfig {
125125
address feeQuoter; // ──────────────────────────────╮ FeeQuoter address on the local chain
126-
uint32 permissionLessExecutionThresholdSeconds; // ─╯ Waiting time before manual execution is enabled
126+
uint32 permissionLessExecutionThresholdSeconds; // | Waiting time before manual execution is enabled
127+
bool isRMNVerificationDisabled; // ─────────────────╯ Flag whether the RMN verification is disabled or not
127128
address messageInterceptor; // Optional message interceptor to validate incoming messages (zero address = no interceptor)
128129
}
129130

@@ -787,10 +788,13 @@ contract OffRamp is ITypeAndVersion, MultiOCR3Base {
787788
bytes32 rawVs
788789
) external {
789790
CommitReport memory commitReport = abi.decode(report, (CommitReport));
791+
DynamicConfig storage dynamicConfig = s_dynamicConfig;
790792

791793
// Verify RMN signatures
792-
if (commitReport.merkleRoots.length > 0) {
793-
i_rmnRemote.verify(address(this), commitReport.merkleRoots, commitReport.rmnSignatures, commitReport.rmnRawVs);
794+
if (!dynamicConfig.isRMNVerificationDisabled) {
795+
if (commitReport.merkleRoots.length > 0) {
796+
i_rmnRemote.verify(address(this), commitReport.merkleRoots, commitReport.rmnSignatures, commitReport.rmnRawVs);
797+
}
794798
}
795799

796800
// Check if the report contains price updates
@@ -803,7 +807,7 @@ contract OffRamp is ITypeAndVersion, MultiOCR3Base {
803807
// If prices are not stale, update the latest epoch and round
804808
s_latestPriceSequenceNumber = ocrSequenceNumber;
805809
// And update the prices in the fee quoter
806-
IFeeQuoter(s_dynamicConfig.feeQuoter).updatePrices(commitReport.priceUpdates);
810+
IFeeQuoter(dynamicConfig.feeQuoter).updatePrices(commitReport.priceUpdates);
807811
} else {
808812
// If prices are stale and the report doesn't contain a root, this report
809813
// does not have any valid information and we revert.

contracts/src/v0.8/ccip/rmn/RMNHome.sol

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ contract RMNHome is OwnerIsCreator, ITypeAndVersion {
6868
error DuplicateOffchainPublicKey();
6969
error DuplicateSourceChain();
7070
error OutOfBoundsObserverNodeIndex();
71-
error MinObserversTooHigh();
71+
error NotEnoughObservers();
7272
error ConfigDigestMismatch(bytes32 expectedConfigDigest, bytes32 gotConfigDigest);
7373
error DigestNotFound(bytes32 configDigest);
7474
error RevokingZeroDigestNotAllowed();
@@ -81,7 +81,7 @@ contract RMNHome is OwnerIsCreator, ITypeAndVersion {
8181

8282
struct SourceChain {
8383
uint64 chainSelector; // ─────╮ The Source chain selector.
84-
uint64 minObservers; // ──────╯ Required number of observers to agree on an observation for this source chain.
84+
uint64 f; // ─────────────────╯ Maximum number of faulty observers; f+1 observers required to agree on an observation for this source chain.
8585
// ObserverNodesBitmap & (1<<i) == (1<<i) iff StaticConfig.nodes[i] is an observer for this source chain.
8686
uint256 observerNodesBitmap;
8787
}
@@ -386,9 +386,9 @@ contract RMNHome is OwnerIsCreator, ITypeAndVersion {
386386
bitmap &= bitmap - 1;
387387
}
388388

389-
// minObservers are tenable
390-
if (currentSourceChain.minObservers > observersCount) {
391-
revert MinObserversTooHigh();
389+
// min observers are tenable
390+
if (observersCount < 2 * currentSourceChain.f + 1) {
391+
revert NotEnoughObservers();
392392
}
393393
}
394394
}

contracts/src/v0.8/ccip/rmn/RMNRemote.sol

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ contract RMNRemote is OwnerIsCreator, ITypeAndVersion, IRMNRemote {
2727
error DuplicateOnchainPublicKey();
2828
error InvalidSignature();
2929
error InvalidSignerOrder();
30-
error MinSignersTooHigh();
30+
error NotEnoughSigners();
3131
error NotCursed(bytes16 subject);
3232
error OutOfOrderSignatures();
3333
error ThresholdNotMet();
@@ -45,11 +45,10 @@ contract RMNRemote is OwnerIsCreator, ITypeAndVersion, IRMNRemote {
4545
}
4646

4747
/// @dev the contract config
48-
/// @dev note: minSigners can be set to 0 to disable verification for chains without RMN support
4948
struct Config {
5049
bytes32 rmnHomeContractConfigDigest; // Digest of the RMNHome contract config
51-
Signer[] signers; // List of signers
52-
uint64 minSigners; // Threshold for the number of signers required to verify a report
50+
Signer[] signers; // List of signers
51+
uint64 f; // Max number of faulty RMN nodes; f+1 signers are required to verify a report, must configure 2f+1 signers in total
5352
}
5453

5554
/// @dev part of the payload that RMN nodes sign: keccak256(abi.encode(RMN_V1_6_ANY2EVM_REPORT, report))
@@ -60,7 +59,7 @@ contract RMNRemote is OwnerIsCreator, ITypeAndVersion, IRMNRemote {
6059
address rmnRemoteContractAddress; // ─────╯ The address of this contract
6160
address offrampAddress; // The address of the offramp on the same chain as this contract
6261
bytes32 rmnHomeContractConfigDigest; // The digest of the RMNHome contract config
63-
Internal.MerkleRoot[] merkleRoots; // The dest lane updates
62+
Internal.MerkleRoot[] merkleRoots; // The dest lane updates
6463
}
6564

6665
/// @dev this is included in the preimage of the digest that RMN nodes sign
@@ -97,7 +96,7 @@ contract RMNRemote is OwnerIsCreator, ITypeAndVersion, IRMNRemote {
9796
if (s_configCount == 0) {
9897
revert ConfigNotSet();
9998
}
100-
if (signatures.length < s_config.minSigners) revert ThresholdNotMet();
99+
if (signatures.length < s_config.f + 1) revert ThresholdNotMet();
101100

102101
bytes32 digest = keccak256(
103102
abi.encode(
@@ -142,9 +141,9 @@ contract RMNRemote is OwnerIsCreator, ITypeAndVersion, IRMNRemote {
142141
}
143142
}
144143

145-
// minSigners is tenable
146-
if (!(newConfig.minSigners <= newConfig.signers.length)) {
147-
revert MinSignersTooHigh();
144+
// min signers requirement is tenable
145+
if (newConfig.signers.length < 2 * newConfig.f + 1) {
146+
revert NotEnoughSigners();
148147
}
149148

150149
// clear the old signers

contracts/src/v0.8/ccip/test/offRamp/OffRamp.t.sol

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3336,6 +3336,47 @@ contract OffRamp_commit is OffRampSetup {
33363336
assertEq(block.timestamp, s_offRamp.getMerkleRoot(SOURCE_CHAIN_SELECTOR_1, root));
33373337
}
33383338

3339+
function test_RootWithRMNDisabled_success() public {
3340+
// force RMN verification to fail
3341+
vm.mockCallRevert(address(s_mockRMNRemote), abi.encodeWithSelector(IRMNRemote.verify.selector), bytes(""));
3342+
3343+
// but ☝️ doesn't matter because RMN verification is disabled
3344+
OffRamp.DynamicConfig memory dynamicConfig = _generateDynamicOffRampConfig(address(s_feeQuoter));
3345+
dynamicConfig.isRMNVerificationDisabled = true;
3346+
s_offRamp.setDynamicConfig(dynamicConfig);
3347+
3348+
uint64 max1 = 931;
3349+
bytes32 root = "Only a single root";
3350+
3351+
Internal.MerkleRoot[] memory roots = new Internal.MerkleRoot[](1);
3352+
roots[0] = Internal.MerkleRoot({
3353+
sourceChainSelector: SOURCE_CHAIN_SELECTOR_1,
3354+
onRampAddress: ON_RAMP_ADDRESS_1,
3355+
minSeqNr: 1,
3356+
maxSeqNr: max1,
3357+
merkleRoot: root
3358+
});
3359+
3360+
OffRamp.CommitReport memory commitReport = OffRamp.CommitReport({
3361+
priceUpdates: _getEmptyPriceUpdates(),
3362+
merkleRoots: roots,
3363+
rmnSignatures: s_rmnSignatures,
3364+
rmnRawVs: 0
3365+
});
3366+
3367+
vm.expectEmit();
3368+
emit OffRamp.CommitReportAccepted(commitReport.merkleRoots, commitReport.priceUpdates);
3369+
3370+
vm.expectEmit();
3371+
emit MultiOCR3Base.Transmitted(uint8(Internal.OCRPluginType.Commit), s_configDigestCommit, s_latestSequenceNumber);
3372+
3373+
_commit(commitReport, s_latestSequenceNumber);
3374+
3375+
assertEq(max1 + 1, s_offRamp.getSourceChainConfig(SOURCE_CHAIN_SELECTOR).minSeqNr);
3376+
assertEq(0, s_offRamp.getLatestPriceSequenceNumber());
3377+
assertEq(block.timestamp, s_offRamp.getMerkleRoot(SOURCE_CHAIN_SELECTOR_1, root));
3378+
}
3379+
33393380
function test_StaleReportWithRoot_Success() public {
33403381
uint64 maxSeq = 12;
33413382
uint224 tokenStartPrice = IFeeQuoter(s_offRamp.getDynamicConfig().feeQuoter).getTokenPrice(s_sourceFeeToken).value;

contracts/src/v0.8/ccip/test/offRamp/OffRampSetup.t.sol

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,8 +173,9 @@ contract OffRampSetup is FeeQuoterSetup, MultiOCR3BaseSetup {
173173
address feeQuoter
174174
) internal pure returns (OffRamp.DynamicConfig memory) {
175175
return OffRamp.DynamicConfig({
176-
permissionLessExecutionThresholdSeconds: PERMISSION_LESS_EXECUTION_THRESHOLD_SECONDS,
177176
feeQuoter: feeQuoter,
177+
permissionLessExecutionThresholdSeconds: PERMISSION_LESS_EXECUTION_THRESHOLD_SECONDS,
178+
isRMNVerificationDisabled: false,
178179
messageInterceptor: address(0)
179180
});
180181
}

contracts/src/v0.8/ccip/test/rmn/RMNHome.t.sol

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,9 @@ contract RMNHomeTest is Test {
2323

2424
RMNHome.SourceChain[] memory sourceChains = new RMNHome.SourceChain[](2);
2525
// Observer 0 for source chain 9000
26-
sourceChains[0] = RMNHome.SourceChain({chainSelector: 9000, minObservers: 1, observerNodesBitmap: 1 << 0});
27-
// Observers 1 and 2 for source chain 9001
28-
sourceChains[1] = RMNHome.SourceChain({chainSelector: 9001, minObservers: 2, observerNodesBitmap: 1 << 1 | 1 << 2});
26+
sourceChains[0] = RMNHome.SourceChain({chainSelector: 9000, f: 1, observerNodesBitmap: 1 << 0 | 1 << 1 | 1 << 2});
27+
// Observers 0, 1 and 2 for source chain 9001
28+
sourceChains[1] = RMNHome.SourceChain({chainSelector: 9001, f: 1, observerNodesBitmap: 1 << 0 | 1 << 1 | 1 << 2});
2929

3030
return Config({
3131
staticConfig: RMNHome.StaticConfig({nodes: nodes, offchainConfig: abi.encode("static_config")}),
@@ -114,7 +114,7 @@ contract RMNHome_setCandidate is RMNHomeTest {
114114
for (uint256 i = 0; i < storedDynamicConfig.sourceChains.length; i++) {
115115
RMNHome.SourceChain memory storedSourceChain = storedDynamicConfig.sourceChains[i];
116116
assertEq(storedSourceChain.chainSelector, versionedConfig.dynamicConfig.sourceChains[i].chainSelector);
117-
assertEq(storedSourceChain.minObservers, versionedConfig.dynamicConfig.sourceChains[i].minObservers);
117+
assertEq(storedSourceChain.f, versionedConfig.dynamicConfig.sourceChains[i].f);
118118
assertEq(storedSourceChain.observerNodesBitmap, versionedConfig.dynamicConfig.sourceChains[i].observerNodesBitmap);
119119
}
120120
assertEq(storedDynamicConfig.offchainConfig, versionedConfig.dynamicConfig.offchainConfig);
@@ -152,7 +152,7 @@ contract RMNHome_revokeCandidate is RMNHomeTest {
152152
bytes32 digest = s_rmnHome.setCandidate(config.staticConfig, config.dynamicConfig, ZERO_DIGEST);
153153
s_rmnHome.promoteCandidateAndRevokeActive(digest, ZERO_DIGEST);
154154

155-
config.dynamicConfig.sourceChains[0].minObservers--;
155+
config.dynamicConfig.sourceChains[1].f--;
156156
s_rmnHome.setCandidate(config.staticConfig, config.dynamicConfig, ZERO_DIGEST);
157157
}
158158

@@ -305,11 +305,11 @@ contract RMNHome__validateStaticAndDynamicConfig is RMNHomeTest {
305305
s_rmnHome.setCandidate(config.staticConfig, config.dynamicConfig, ZERO_DIGEST);
306306
}
307307

308-
function test_validateStaticAndDynamicConfig_MinObserversTooHigh_reverts() public {
308+
function test_validateStaticAndDynamicConfig_NotEnoughObservers_reverts() public {
309309
Config memory config = _getBaseConfig();
310-
config.dynamicConfig.sourceChains[0].minObservers++;
310+
config.dynamicConfig.sourceChains[0].f++;
311311

312-
vm.expectRevert(RMNHome.MinObserversTooHigh.selector);
312+
vm.expectRevert(RMNHome.NotEnoughObservers.selector);
313313
s_rmnHome.setCandidate(config.staticConfig, config.dynamicConfig, ZERO_DIGEST);
314314
}
315315
}
@@ -324,7 +324,7 @@ contract RMNHome_setDynamicConfig is RMNHomeTest {
324324
(bytes32 priorActiveDigest,) = s_rmnHome.getConfigDigests();
325325

326326
Config memory config = _getBaseConfig();
327-
config.dynamicConfig.sourceChains[0].minObservers--;
327+
config.dynamicConfig.sourceChains[1].f--;
328328

329329
(, bytes32 candidateConfigDigest) = s_rmnHome.getConfigDigests();
330330

@@ -335,10 +335,7 @@ contract RMNHome_setDynamicConfig is RMNHomeTest {
335335

336336
(RMNHome.VersionedConfig memory storedVersionedConfig, bool ok) = s_rmnHome.getConfig(candidateConfigDigest);
337337
assertTrue(ok);
338-
assertEq(
339-
storedVersionedConfig.dynamicConfig.sourceChains[0].minObservers,
340-
config.dynamicConfig.sourceChains[0].minObservers
341-
);
338+
assertEq(storedVersionedConfig.dynamicConfig.sourceChains[0].f, config.dynamicConfig.sourceChains[0].f);
342339

343340
// Asser the digests don't change when updating the dynamic config
344341
(bytes32 activeDigest, bytes32 candidateDigest) = s_rmnHome.getConfigDigests();
@@ -349,7 +346,7 @@ contract RMNHome_setDynamicConfig is RMNHomeTest {
349346
// Asserts the validation function is being called
350347
function test_setDynamicConfig_MinObserversTooHigh_reverts() public {
351348
Config memory config = _getBaseConfig();
352-
config.dynamicConfig.sourceChains[0].minObservers++;
349+
config.dynamicConfig.sourceChains[0].f++;
353350

354351
vm.expectRevert(abi.encodeWithSelector(RMNHome.DigestNotFound.selector, ZERO_DIGEST));
355352
s_rmnHome.setDynamicConfig(config.dynamicConfig, ZERO_DIGEST);

0 commit comments

Comments
 (0)