// SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.24; import {ITypeAndVersion} from "../../shared/interfaces/ITypeAndVersion.sol"; import {OwnerIsCreator} from "../../shared/access/OwnerIsCreator.sol"; /// @notice Stores the home configuration for RMN, that is referenced by CCIP oracles, RMN nodes, and the RMNRemote /// contracts. /// @dev This contract is a state machine with the following states: /// - Init: The initial state of the contract, no config has been set, or all configs have been revoked. /// [0, 0] /// /// - Candidate: A new config has been set, but it has not been promoted yet, or all active configs have been revoked. /// [0, 1] /// /// - Active: A non-zero config has been promoted and is active, there is no candidate configured. /// [1, 0] /// /// - ActiveAndCandidate: A non-zero config has been promoted and is active, and a new config has been set as candidate. /// [1, 1] /// /// The following state transitions are allowed: /// - Init -> Candidate: setCandidate() /// - Candidate -> Active: promoteCandidateAndRevokeActive() /// - Candidate -> Candidate: setCandidate() /// - Candidate -> Init: revokeCandidate() /// - Active -> ActiveAndCandidate: setCandidate() /// - Active -> Init: promoteCandidateAndRevokeActive() /// - ActiveAndCandidate -> Active: promoteCandidateAndRevokeActive() /// - ActiveAndCandidate -> Active: revokeCandidate() /// - ActiveAndCandidate -> ActiveAndCandidate: setCandidate() /// /// This means the following calls are not allowed at the following states: /// - Init: promoteCandidateAndRevokeActive(), as there is no config to promote. /// - Init: revokeCandidate(), as there is no config to revoke /// - Active: revokeCandidate(), as there is no candidate to revoke /// Note that we explicitly do allow promoteCandidateAndRevokeActive() to be called when there is an active config but /// no candidate config. This is the only way to remove the active config. The alternative would be to set some unusable /// config as candidate and promote that, but fully clearing it is cleaner. /// /// ┌─────────────┐ setCandidate ┌─────────────┐ /// │ ├───────────────────►│ │ setCandidate /// │ Init │ revokeCandidate │ Candidate │◄───────────┐ /// │ [0,0] │◄───────────────────┤ [0,1] │────────────┘ /// │ │ ┌─────────────────┤ │ /// └─────────────┘ │ promote- └─────────────┘ /// ▲ │ Candidate /// promote- │ │ /// Candidate │ │ /// │ │ /// ┌──────────┴──┐ │ promote- ┌─────────────┐ /// │ │◄─┘ Candidate OR │ Active & │ setCandidate /// │ Active │ revokeCandidate │ Candidate │◄───────────┐ /// │ [1,0] │◄───────────────────┤ [1,1] │────────────┘ /// │ ├───────────────────►│ │ /// └─────────────┘ setSecondary └─────────────┘ /// contract RMNHome is OwnerIsCreator, ITypeAndVersion { event ConfigSet(bytes32 indexed configDigest, uint32 version, StaticConfig staticConfig, DynamicConfig dynamicConfig); event ActiveConfigRevoked(bytes32 indexed configDigest); event CandidateConfigRevoked(bytes32 indexed configDigest); event DynamicConfigSet(bytes32 indexed configDigest, DynamicConfig dynamicConfig); event ConfigPromoted(bytes32 indexed configDigest); error OutOfBoundsNodesLength(); error DuplicatePeerId(); error DuplicateOffchainPublicKey(); error DuplicateSourceChain(); error OutOfBoundsObserverNodeIndex(); error MinObserversTooHigh(); error ConfigDigestMismatch(bytes32 expectedConfigDigest, bytes32 gotConfigDigest); error DigestNotFound(bytes32 configDigest); error RevokingZeroDigestNotAllowed(); error NoOpStateTransitionNotAllowed(); struct Node { bytes32 peerId; // Used for p2p communication. bytes32 offchainPublicKey; // Observations are signed with this public key, and are only verified offchain. } struct SourceChain { uint64 chainSelector; // ─────╮ The Source chain selector. uint64 minObservers; // ──────╯ Required number of observers to agree on an observation for this source chain. // ObserverNodesBitmap & (1< MAX_NODES) { revert OutOfBoundsNodesLength(); } // Ensure no peerId or offchainPublicKey is duplicated. for (uint256 i = 0; i < staticConfig.nodes.length; ++i) { for (uint256 j = i + 1; j < staticConfig.nodes.length; ++j) { if (staticConfig.nodes[i].peerId == staticConfig.nodes[j].peerId) { revert DuplicatePeerId(); } if (staticConfig.nodes[i].offchainPublicKey == staticConfig.nodes[j].offchainPublicKey) { revert DuplicateOffchainPublicKey(); } } } _validateDynamicConfig(dynamicConfig, staticConfig.nodes.length); } /// @notice Validates the dynamic config. Reverts when the config is invalid. /// @param dynamicConfig The dynamic part of the config. /// @param numberOfNodes The number of nodes in the static config. function _validateDynamicConfig(DynamicConfig memory dynamicConfig, uint256 numberOfNodes) internal pure { uint256 numberOfSourceChains = dynamicConfig.sourceChains.length; for (uint256 i = 0; i < numberOfSourceChains; ++i) { SourceChain memory currentSourceChain = dynamicConfig.sourceChains[i]; // Ensure the source chain is unique. for (uint256 j = i + 1; j < numberOfSourceChains; ++j) { if (currentSourceChain.chainSelector == dynamicConfig.sourceChains[j].chainSelector) { revert DuplicateSourceChain(); } } // all observer node indices are valid uint256 bitmap = currentSourceChain.observerNodesBitmap; // Check if there are any bits set for indexes outside of the expected range. if (bitmap & (type(uint256).max >> (256 - numberOfNodes)) != bitmap) { revert OutOfBoundsObserverNodeIndex(); } uint256 observersCount = 0; for (; bitmap != 0; ++observersCount) { bitmap &= bitmap - 1; } // minObservers are tenable if (currentSourceChain.minObservers > observersCount) { revert MinObserversTooHigh(); } } } }