From 922687668c31ce5e5db3457d93e15b03ab24959e Mon Sep 17 00:00:00 2001 From: nogo <0xnogo@gmail.com> Date: Thu, 19 Sep 2024 11:05:34 +0400 Subject: [PATCH 01/16] rmn home and remote readers --- .mockery.yaml | 2 + commit/merkleroot/rmn/types/config.go | 84 +++++ internal/reader/home_chain_test.go | 29 +- internal/reader/rmn_home.go | 279 +++++++++++++++ internal/reader/rmn_home_test.go | 305 ++++++++++++++++ internal/reader/rmn_remote.go | 47 +++ internal/reader/test_helpers.go | 20 ++ mocks/internal_/reader/rmn_home.go | 498 ++++++++++++++++++++++++++ mocks/internal_/reader/rmn_remote.go | 266 ++++++++++++++ pkg/consts/consts.go | 6 + 10 files changed, 1514 insertions(+), 22 deletions(-) create mode 100644 commit/merkleroot/rmn/types/config.go create mode 100644 internal/reader/rmn_home.go create mode 100644 internal/reader/rmn_home_test.go create mode 100644 internal/reader/rmn_remote.go create mode 100644 internal/reader/test_helpers.go create mode 100644 mocks/internal_/reader/rmn_home.go create mode 100644 mocks/internal_/reader/rmn_remote.go diff --git a/.mockery.yaml b/.mockery.yaml index 790f3ce2..1eb02c85 100644 --- a/.mockery.yaml +++ b/.mockery.yaml @@ -14,6 +14,8 @@ packages: github.com/smartcontractkit/chainlink-ccip/internal/reader: interfaces: HomeChain: + RMNHome: + RMNRemote: CCIP: PriceReader: github.com/smartcontractkit/chainlink-ccip/internal/reader/contractreader: diff --git a/commit/merkleroot/rmn/types/config.go b/commit/merkleroot/rmn/types/config.go new file mode 100644 index 00000000..370fdfd2 --- /dev/null +++ b/commit/merkleroot/rmn/types/config.go @@ -0,0 +1,84 @@ +package types + +import ( + "crypto/ed25519" + + mapset "github.com/deckarep/golang-set/v2" + + cciptypes "github.com/smartcontractkit/chainlink-common/pkg/types/ccipocr3" +) + +type NodeID uint32 + +// RMNConfig contains the RMN configuration required by the plugin and the RMN client in a single struct. +type RMNConfig struct { + Home RMNHomeConfig + Remote RMNRemoteConfig +} + +// RMNHomeConfig contains the configuration fetched from the RMNHome contract. +type RMNHomeConfig struct { + Nodes []RMNHomeNodeInfo + MinObservers map[cciptypes.ChainSelector]uint64 + ConfigDigest cciptypes.Bytes32 + OffchainConfig cciptypes.Bytes // Raw offchain configuration bytes +} + +// RMNHomeNodeInfo contains information about a node from the RMNHome contract. +type RMNHomeNodeInfo struct { + ID NodeID // ID is the index of this node in the RMN config + PeerID cciptypes.Bytes32 + SupportedSourceChains mapset.Set[cciptypes.ChainSelector] + SignObservationsPublicKey *ed25519.PublicKey // offchainPublicKey +} + +// RMNRemoteConfig contains the configuration fetched from the RMNRemote contract. +type RMNRemoteConfig struct { + ContractAddress cciptypes.Bytes + ConfigDigest cciptypes.Bytes32 + Signers []RMNRemoteSignerInfo + MinSigners uint64 + ConfigVersion uint32 + RmnReportVersion string // e.g., "RMN_V1_6_ANY2EVM_REPORT" +} + +// RMNRemoteSignerInfo contains information about a signer from the RMNRemote contract. +type RMNRemoteSignerInfo struct { + SignReportsAddress cciptypes.Bytes // for signing reports + NodeIndex uint64 // maps to nodes in RMNHome + SignObservationPrefix string // for signing observations +} + +// VersionedConfigWithDigest mirrors RMNHome.sol's VersionedConfigWithDigest struct +type VersionedConfigWithDigest struct { + // nolint:lll // don't split up the long url + // https://github.com/smartcontractkit/ccip/blob/e6e26ad31eef625faf68806a2b4f0549bc89b15c/contracts/src/v0.8/ccip/RMNRemote.sol#L34 + ConfigDigest cciptypes.Bytes32 `json:"configDigest"` + VersionedConfig VersionedConfig `json:"versionedConfig"` +} + +// VersionedConfig mirrors RMNHome.sol's VersionedConfig struct +type VersionedConfig struct { + Version uint32 `json:"version"` + Config Config `json:"config"` +} + +// Config mirrors RMNHome.sol's Config struct +type Config struct { + Nodes []Node `json:"nodes"` + SourceChains []SourceChain `json:"sourceChains"` + OffchainConfig cciptypes.Bytes `json:"offchainConfig"` +} + +// Node mirrors RMNHome.sol's Node struct +type Node struct { + PeerID cciptypes.Bytes32 `json:"peerId"` + OffchainPublicKey cciptypes.Bytes32 `json:"offchainPublicKey"` +} + +// SourceChain mirrors RMNHome.sol's SourceChain struct +type SourceChain struct { + ChainSelector cciptypes.ChainSelector `json:"chainSelector"` + MinObservers uint64 `json:"minObservers"` + ObserverNodesBitmap cciptypes.BigInt `json:"observerNodesBitmap"` +} diff --git a/internal/reader/home_chain_test.go b/internal/reader/home_chain_test.go index 868c832d..981379a4 100644 --- a/internal/reader/home_chain_test.go +++ b/internal/reader/home_chain_test.go @@ -10,7 +10,6 @@ import ( "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" - "github.com/smartcontractkit/libocr/commontypes" libocrtypes "github.com/smartcontractkit/libocr/ragep2p/types" "github.com/smartcontractkit/chainlink-common/pkg/logger" @@ -23,15 +22,10 @@ import ( ) var ( - chainA = cciptypes.ChainSelector(1) - chainB = cciptypes.ChainSelector(2) - chainC = cciptypes.ChainSelector(3) - oracleAId = commontypes.OracleID(1) - p2pOracleAId = libocrtypes.PeerID{byte(oracleAId)} - oracleBId = commontypes.OracleID(2) - p2pOracleBId = libocrtypes.PeerID{byte(oracleBId)} - oracleCId = commontypes.OracleID(3) - p2pOracleCId = libocrtypes.PeerID{byte(oracleCId)} + ccipConfigBoundContract = types.BoundContract{ + Address: "0xCCIPConfigFakeAddress", + Name: consts.ContractNameCCIPConfig, + } ) func TestHomeChainConfigPoller_HealthReport(t *testing.T) { @@ -52,10 +46,7 @@ func TestHomeChainConfigPoller_HealthReport(t *testing.T) { homeChainReader, logger.Test(t), tickTime, - types.BoundContract{ - Address: "0xCCIPConfigFakeAddress", - Name: consts.ContractNameCCIPConfig, - }, + ccipConfigBoundContract, ) require.NoError(t, configPoller.Start(context.Background())) // Initially it's healthy @@ -151,10 +142,7 @@ func Test_PollingWorking(t *testing.T) { homeChainReader, logger.Test(t), tickTime, - types.BoundContract{ - Address: "0xCCIPConfigFakeAddress", - Name: consts.ContractNameCCIPConfig, - }, + ccipConfigBoundContract, ) require.NoError(t, configPoller.Start(context.Background())) @@ -209,10 +197,7 @@ func Test_HomeChainPoller_GetOCRConfig(t *testing.T) { homeChainReader, logger.Test(t), 10*time.Millisecond, - types.BoundContract{ - Address: "0xCCIPConfigFakeAddress", - Name: consts.ContractNameCCIPConfig, - }, + ccipConfigBoundContract, ) configs, err := configPoller.GetOCRConfigs(context.Background(), donID, pluginType) diff --git a/internal/reader/rmn_home.go b/internal/reader/rmn_home.go new file mode 100644 index 00000000..8782b9ae --- /dev/null +++ b/internal/reader/rmn_home.go @@ -0,0 +1,279 @@ +package reader + +import ( + "context" + "crypto/ed25519" + "fmt" + "math/big" + "sync" + "time" + + mapset "github.com/deckarep/golang-set/v2" + + "github.com/smartcontractkit/chainlink-ccip/pkg/consts" + + "github.com/smartcontractkit/chainlink-ccip/internal/reader/contractreader" + + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-common/pkg/services" + "github.com/smartcontractkit/chainlink-common/pkg/types" + cciptypes "github.com/smartcontractkit/chainlink-common/pkg/types/ccipocr3" + "github.com/smartcontractkit/chainlink-common/pkg/types/query/primitives" + + rmntypes "github.com/smartcontractkit/chainlink-ccip/commit/merkleroot/rmn/types" +) + +const ( + rmnMaxSizeCommittee = 256 // bitmap is 256 bits making the max committee size 256 +) + +type RMNHome interface { + // GetRMNNodesInfo gets the RMNHomeNodeInfo for the given configDigest + GetRMNNodesInfo(configDigest cciptypes.Bytes32) ([]rmntypes.RMNHomeNodeInfo, error) + // IsRMNHomeConfigDigestSet checks if the configDigest is set in the RMNHome contract + IsRMNHomeConfigDigestSet(configDigest cciptypes.Bytes32) (bool, error) + // GetMinObservers gets the minimum number of observers required for each chain in the given configDigest + GetMinObservers(configDigest cciptypes.Bytes32) (map[cciptypes.ChainSelector]uint64, error) + // GetOffChainConfig gets the offchain config for the given configDigest + GetOffChainConfig(configDigest cciptypes.Bytes32) (cciptypes.Bytes, error) + services.Service +} + +type RmnHomePoller struct { + wg sync.WaitGroup + stopCh services.StopChan + sync services.StateMachine + contractReader contractreader.ContractReaderFacade + rmnHomeBoundContract types.BoundContract + lggr logger.Logger + mutex *sync.RWMutex + rmnHomeConfig map[cciptypes.Bytes32]rmntypes.RMNHomeConfig + failedPolls uint + pollingDuration time.Duration // How frequently the poller fetches the chain configs +} + +func NewRMNHomePoller( + contractReader contractreader.ContractReaderFacade, + rmnHomeBoundContract types.BoundContract, + lggr logger.Logger, + pollingInterval time.Duration, +) RMNHome { + return &RmnHomePoller{ + stopCh: make(chan struct{}), + contractReader: contractReader, + rmnHomeBoundContract: rmnHomeBoundContract, + rmnHomeConfig: make(map[cciptypes.Bytes32]rmntypes.RMNHomeConfig), + mutex: &sync.RWMutex{}, + failedPolls: 0, + lggr: lggr, + pollingDuration: pollingInterval, + } +} + +func (r *RmnHomePoller) Start(ctx context.Context) error { + r.lggr.Infow("Start Polling RMNHome") + return r.sync.StartOnce(r.Name(), func() error { + r.wg.Add(1) + go r.poll() + return nil + }) +} + +func (r *RmnHomePoller) poll() { + defer r.wg.Done() + ctx, cancel := r.stopCh.NewCtx() + defer cancel() + // Initial fetch once poll is called before any ticks + if err := r.fetchAndSetRmnHomeConfigs(ctx); err != nil { + // Just log, don't return error as we want to keep polling + r.lggr.Errorw("Initial fetch of on-chain configs failed", "err", err) + } + + ticker := time.NewTicker(r.pollingDuration) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + r.mutex.Lock() + r.failedPolls = 0 + r.mutex.Unlock() + return + case <-ticker.C: + if err := r.fetchAndSetRmnHomeConfigs(ctx); err != nil { + r.mutex.Lock() + r.failedPolls++ + r.mutex.Unlock() + } + } + } +} + +func (r *RmnHomePoller) fetchAndSetRmnHomeConfigs(ctx context.Context) error { + var versionedConfigWithDigests []rmntypes.VersionedConfigWithDigest + err := r.contractReader.GetLatestValue( + ctx, + r.rmnHomeBoundContract.ReadIdentifier(consts.MethodNameGetVersionedConfigsWithDigests), + primitives.Unconfirmed, + map[string]interface{}{ + "offset": 0, + "limit": 2, // TODO: fetch CONFIG_RING_BUFFER_SIZE + }, + &versionedConfigWithDigests, + ) + if err != nil { + return fmt.Errorf("error fetching RMNHomeConfig: %w", err) + } + + // TODO: fetch CONFIG_RING_BUFFER_SIZE and compare with len(versionedConfigWithDigests) + if len(versionedConfigWithDigests) > 2 { + r.lggr.Errorw("more than 2 RMNHomeConfigs found", "numConfigs", len(versionedConfigWithDigests), "requestedLimit", 2) + return fmt.Errorf("more than 2 RMNHomeConfigs found") + } + + r.setRMNHomeState(convertOnChainConfigToRMNHomeChainConfig(r.lggr, versionedConfigWithDigests)) + + if len(versionedConfigWithDigests) == 0 { + // That's a legitimate case if there are no rmn configs on chain yet + r.lggr.Warnw("no on chain configs found") + return nil + } + + return nil +} + +func (r *RmnHomePoller) setRMNHomeState(rmnHomeConfig map[cciptypes.Bytes32]rmntypes.RMNHomeConfig) { + r.mutex.Lock() + defer r.mutex.Unlock() + r.rmnHomeConfig = rmnHomeConfig +} + +func (r *RmnHomePoller) GetRMNNodesInfo(configDigest cciptypes.Bytes32) ([]rmntypes.RMNHomeNodeInfo, error) { + r.mutex.RLock() + defer r.mutex.RUnlock() + return r.rmnHomeConfig[configDigest].Nodes, nil +} + +func (r *RmnHomePoller) IsRMNHomeConfigDigestSet(configDigest cciptypes.Bytes32) (bool, error) { + r.mutex.RLock() + defer r.mutex.RUnlock() + _, ok := r.rmnHomeConfig[configDigest] + return ok, nil +} + +func (r *RmnHomePoller) GetMinObservers(configDigest cciptypes.Bytes32) (map[cciptypes.ChainSelector]uint64, error) { + r.mutex.RLock() + defer r.mutex.RUnlock() + return r.rmnHomeConfig[configDigest].MinObservers, nil +} + +func (r *RmnHomePoller) GetOffChainConfig(configDigest cciptypes.Bytes32) (cciptypes.Bytes, error) { + r.mutex.RLock() + defer r.mutex.RUnlock() + return r.rmnHomeConfig[configDigest].OffchainConfig, nil +} + +func (r *RmnHomePoller) Close() error { + return r.sync.StopOnce(r.Name(), func() error { + defer r.wg.Wait() + close(r.stopCh) + return nil + }) +} + +func (r *RmnHomePoller) Ready() error { + r.mutex.RLock() + defer r.mutex.RUnlock() + return r.sync.Ready() +} + +func (r *RmnHomePoller) HealthReport() map[string]error { + r.mutex.RLock() + defer r.mutex.RUnlock() + if r.failedPolls >= MaxFailedPolls { + r.sync.SvcErrBuffer.Append(fmt.Errorf("polling failed %d times in a row", MaxFailedPolls)) + } + return map[string]error{r.Name(): r.sync.Healthy()} +} + +func (r *RmnHomePoller) Name() string { + return "RmnHomePoller" +} + +func convertOnChainConfigToRMNHomeChainConfig( + lggr logger.Logger, + versionedConfigWithDigests []rmntypes.VersionedConfigWithDigest, +) map[cciptypes.Bytes32]rmntypes.RMNHomeConfig { + if len(versionedConfigWithDigests) == 0 { + lggr.Warnw("no on chain RMNHomeConfigs found") + return map[cciptypes.Bytes32]rmntypes.RMNHomeConfig{} + } + + rmnHomeConfigs := make(map[cciptypes.Bytes32]rmntypes.RMNHomeConfig) + for _, versionedConfigWithDigest := range versionedConfigWithDigests { + config := versionedConfigWithDigest.VersionedConfig.Config + nodes := make([]rmntypes.RMNHomeNodeInfo, len(config.Nodes)) + for i, node := range config.Nodes { + pubKey := ed25519.PublicKey(node.OffchainPublicKey[:]) + + nodes[i] = rmntypes.RMNHomeNodeInfo{ + ID: rmntypes.NodeID(i), + PeerID: node.PeerID, + SignObservationsPublicKey: &pubKey, + SupportedSourceChains: mapset.NewSet[cciptypes.ChainSelector](), + } + } + + minObservers := make(map[cciptypes.ChainSelector]uint64) + + for _, chain := range config.SourceChains { + minObservers[chain.ChainSelector] = chain.MinObservers + for j := 0; j < len(nodes); j++ { + isObserver, err := IsNodeObserver(chain, j, len(nodes)) + if err != nil { + lggr.Warnw("failed to check if node is observer", "err", err) + continue + } + if isObserver { + nodes[j].SupportedSourceChains.Add(chain.ChainSelector) + } + } + } + + rmnHomeConfigs[versionedConfigWithDigest.ConfigDigest] = rmntypes.RMNHomeConfig{ + Nodes: nodes, + MinObservers: minObservers, + ConfigDigest: versionedConfigWithDigest.ConfigDigest, + OffchainConfig: config.OffchainConfig, + } + } + return rmnHomeConfigs +} + +// IsNodeObserver checks if a node is an observer for the given source chain +func IsNodeObserver(sourceChain rmntypes.SourceChain, nodeIndex int, totalNodes int) (bool, error) { + if totalNodes > rmnMaxSizeCommittee || totalNodes <= 0 { + return false, fmt.Errorf("invalid total nodes: %d", totalNodes) + } + + if nodeIndex < 0 || nodeIndex >= totalNodes { + return false, fmt.Errorf("invalid node index: %d", nodeIndex) + } + + // Validate the bitmap + maxValidBitmap := new(big.Int).Lsh(big.NewInt(1), uint(totalNodes)) + maxValidBitmap.Sub(maxValidBitmap, big.NewInt(1)) + if sourceChain.ObserverNodesBitmap.Int.Cmp(maxValidBitmap) > 0 { + return false, fmt.Errorf("invalid observer nodes bitmap") + } + + // Create a big.Int with 1 shifted left by nodeIndex + mask := new(big.Int).Lsh(big.NewInt(1), uint(nodeIndex)) + + // Perform the bitwise AND operation + result := new(big.Int).And(sourceChain.ObserverNodesBitmap.Int, mask) + + // Check if the result equals the mask + return result.Cmp(mask) == 0, nil +} + +var _ RMNHome = (*RmnHomePoller)(nil) diff --git a/internal/reader/rmn_home_test.go b/internal/reader/rmn_home_test.go new file mode 100644 index 00000000..207f3235 --- /dev/null +++ b/internal/reader/rmn_home_test.go @@ -0,0 +1,305 @@ +package reader + +import ( + "context" + "fmt" + "math/big" + "testing" + "time" + + "github.com/smartcontractkit/chainlink-ccip/pkg/consts" + + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-common/pkg/types" + cciptypes "github.com/smartcontractkit/chainlink-common/pkg/types/ccipocr3" + + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + + rmntypes "github.com/smartcontractkit/chainlink-ccip/commit/merkleroot/rmn/types" + readermock "github.com/smartcontractkit/chainlink-ccip/mocks/internal_/reader/contractreader" +) + +var ( + rmnHomeBoundContract = types.BoundContract{ + Address: "0xRMNHomeFakeAddress", + Name: consts.ContractNameRMNHome, + } +) + +func TestRMNHomeChainConfigPoller_HealthReport(t *testing.T) { + homeChainReader := readermock.NewMockContractReaderFacade(t) + homeChainReader.On( + "GetLatestValue", + mock.Anything, + mock.Anything, + mock.Anything, + mock.Anything, + mock.Anything).Return(fmt.Errorf("error")) + + var ( + tickTime = 1 * time.Millisecond + totalSleepTime = 50 * time.Millisecond // give enough time for 10 ticks + ) + configPoller := NewRMNHomePoller( + homeChainReader, + rmnHomeBoundContract, + logger.Test(t), + tickTime, + ) + require.NoError(t, configPoller.Start(context.Background())) + // Initially it's healthy + healthy := configPoller.HealthReport() + require.Equal(t, map[string]error{configPoller.Name(): error(nil)}, healthy) + // After one second it will try polling 10 times and fail + time.Sleep(totalSleepTime) + errors := configPoller.HealthReport() + require.Equal(t, 1, len(errors)) + require.Errorf(t, errors[configPoller.Name()], "polling failed %d times in a row", MaxFailedPolls) + require.NoError(t, configPoller.Close()) +} + +func Test_RMNHomePollingWorking(t *testing.T) { + rmnHomeOnChainConfigs := createTestRMNHomeConfigs() + homeChainReader := readermock.NewMockContractReaderFacade(t) + homeChainReader.On( + "GetLatestValue", + mock.Anything, + rmnHomeBoundContract.ReadIdentifier(consts.MethodNameGetVersionedConfigsWithDigests), + mock.Anything, + mock.Anything, + mock.Anything, + ).Run( + func(args mock.Arguments) { + arg := args.Get(4).(*[]rmntypes.VersionedConfigWithDigest) + *arg = rmnHomeOnChainConfigs + }).Return(nil) + + defer homeChainReader.AssertExpectations(t) + + var ( + tickTime = 2 * time.Millisecond + totalSleepTime = tickTime * 20 + ) + + configPoller := NewRMNHomePoller( + homeChainReader, + rmnHomeBoundContract, + logger.Test(t), + tickTime, + ) + + require.NoError(t, configPoller.Start(context.Background())) + // sleep to allow polling to happen + time.Sleep(totalSleepTime) + require.NoError(t, configPoller.Close()) + + calls := homeChainReader.Calls + callCount := 0 + for _, call := range calls { + if call.Method == "GetLatestValue" { + callCount++ + } + } + // called at least 4 times, one for start and one for the first tick for each contract (ccip *2 and rmn *2) + require.GreaterOrEqual(t, callCount, 4) + + for _, configs := range rmnHomeOnChainConfigs { + rmnNodes, err := configPoller.GetRMNNodesInfo(configs.ConfigDigest) + require.NoError(t, err) + require.NotNil(t, rmnNodes) + + isValid, err := configPoller.IsRMNHomeConfigDigestSet(configs.ConfigDigest) + require.NoError(t, err) + require.True(t, isValid) + + offchainConfig, err := configPoller.GetOffChainConfig(configs.ConfigDigest) + require.NoError(t, err) + require.NotNil(t, offchainConfig) + + minObs, err := configPoller.GetMinObservers(configs.ConfigDigest) + require.NoError(t, err) + require.NotNil(t, minObs) + } +} + +func TestIsNodeObserver(t *testing.T) { + tests := []struct { + name string + sourceChain rmntypes.SourceChain + nodeIndex int + totalNodes int + expectedResult bool + expectedError string + }{ + { + name: "Node is observer", + sourceChain: rmntypes.SourceChain{ + ChainSelector: cciptypes.ChainSelector(1), + MinObservers: 3, + ObserverNodesBitmap: cciptypes.NewBigInt(big.NewInt(7)), // 111 in binary + }, + nodeIndex: 1, + totalNodes: 3, + expectedResult: true, + expectedError: "", + }, + { + name: "Node is not observer", + sourceChain: rmntypes.SourceChain{ + ChainSelector: cciptypes.ChainSelector(1), + MinObservers: 3, + ObserverNodesBitmap: cciptypes.NewBigInt(big.NewInt(5)), // 101 in binary + }, + nodeIndex: 1, + totalNodes: 3, + expectedResult: false, + expectedError: "", + }, + { + name: "Node index out of range (high)", + sourceChain: rmntypes.SourceChain{ + ChainSelector: cciptypes.ChainSelector(1), + MinObservers: 3, + ObserverNodesBitmap: cciptypes.NewBigInt(big.NewInt(7)), // 111 in binary + }, + nodeIndex: 3, + totalNodes: 3, + expectedResult: false, + expectedError: "invalid node index: 3", + }, + { + name: "Negative node index", + sourceChain: rmntypes.SourceChain{ + ChainSelector: cciptypes.ChainSelector(1), + MinObservers: 3, + ObserverNodesBitmap: cciptypes.NewBigInt(big.NewInt(7)), // 111 in binary + }, + nodeIndex: -1, + totalNodes: 3, + expectedResult: false, + expectedError: "invalid node index: -1", + }, + { + name: "Invalid bitmap (out of bounds)", + sourceChain: rmntypes.SourceChain{ + ChainSelector: cciptypes.ChainSelector(1), + MinObservers: 3, + ObserverNodesBitmap: cciptypes.NewBigInt(big.NewInt(8)), // 1000 in binary + }, + nodeIndex: 0, + totalNodes: 3, + expectedResult: false, + expectedError: "invalid observer nodes bitmap", + }, + { + name: "Zero total nodes", + sourceChain: rmntypes.SourceChain{ + ChainSelector: cciptypes.ChainSelector(1), + MinObservers: 3, + ObserverNodesBitmap: cciptypes.NewBigInt(big.NewInt(1)), + }, + nodeIndex: 0, + totalNodes: 0, + expectedResult: false, + expectedError: "invalid total nodes: 0", + }, + { + name: "Total nodes exceeds 256", + sourceChain: rmntypes.SourceChain{ + ChainSelector: cciptypes.ChainSelector(1), + MinObservers: 3, + ObserverNodesBitmap: cciptypes.NewBigInt(big.NewInt(1)), + }, + nodeIndex: 0, + totalNodes: 257, + expectedResult: false, + expectedError: "invalid total nodes: 257", + }, + { + name: "Last valid node is observer", + sourceChain: rmntypes.SourceChain{ + ChainSelector: cciptypes.ChainSelector(1), + MinObservers: 1, + ObserverNodesBitmap: cciptypes.NewBigInt(new(big.Int).SetBit(big.NewInt(0), 255, 1)), // Only the 256th bit is set + }, + nodeIndex: 255, + totalNodes: 256, + expectedResult: true, + expectedError: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := IsNodeObserver(tt.sourceChain, tt.nodeIndex, tt.totalNodes) + + if tt.expectedError != "" { + require.Error(t, err) + require.Equal(t, tt.expectedError, err.Error()) + } else { + require.NoError(t, err) + } + + require.Equal(t, tt.expectedResult, result) + }) + } +} + +func createTestRMNHomeConfigs() []rmntypes.VersionedConfigWithDigest { + return []rmntypes.VersionedConfigWithDigest{ + { + ConfigDigest: cciptypes.Bytes32{1, 2, 3, 4, 5}, + VersionedConfig: rmntypes.VersionedConfig{ + Version: 1, + Config: rmntypes.Config{ + Nodes: []rmntypes.Node{ + { + PeerID: cciptypes.Bytes32{10, 11, 12, 13, 14}, + OffchainPublicKey: cciptypes.Bytes32{20, 21, 22, 23, 24}, + }, + { + PeerID: cciptypes.Bytes32{15, 16, 17, 18, 19}, + OffchainPublicKey: cciptypes.Bytes32{25, 26, 27, 28, 29}, + }, + }, + SourceChains: []rmntypes.SourceChain{ + { + ChainSelector: cciptypes.ChainSelector(1), + MinObservers: 1, + ObserverNodesBitmap: cciptypes.NewBigInt(big.NewInt(2)), // 10 in binary = 1 is observer and 0 is not + }, + { + ChainSelector: cciptypes.ChainSelector(2), + MinObservers: 2, + ObserverNodesBitmap: cciptypes.NewBigInt(big.NewInt(3)), // 11 in binary = 0 and 1 are observers + }, + }, + OffchainConfig: cciptypes.Bytes{30, 31, 32, 33, 34}, + }, + }, + }, + { + ConfigDigest: cciptypes.Bytes32{6, 7, 8, 9, 10}, + VersionedConfig: rmntypes.VersionedConfig{ + Version: 2, + Config: rmntypes.Config{ + Nodes: []rmntypes.Node{ + { + PeerID: cciptypes.Bytes32{40, 41, 42, 43, 44}, + OffchainPublicKey: cciptypes.Bytes32{50, 51, 52, 53, 54}, + }, + }, + SourceChains: []rmntypes.SourceChain{ + { + ChainSelector: cciptypes.ChainSelector(1), + MinObservers: 1, + ObserverNodesBitmap: cciptypes.NewBigInt(big.NewInt(1)), // 1 in binary = 0 is an observer + }, + }, + OffchainConfig: cciptypes.Bytes{60, 61, 62, 63, 64}, + }, + }, + }, + } +} diff --git a/internal/reader/rmn_remote.go b/internal/reader/rmn_remote.go new file mode 100644 index 00000000..45f5c270 --- /dev/null +++ b/internal/reader/rmn_remote.go @@ -0,0 +1,47 @@ +package reader + +import ( + rmntypes "github.com/smartcontractkit/chainlink-ccip/commit/merkleroot/rmn/types" + + cciptypes "github.com/smartcontractkit/chainlink-common/pkg/types/ccipocr3" +) + +type RMNRemote interface { + GetMinSigners() uint64 + GetSignersInfo() []rmntypes.RMNRemoteSignerInfo + GetRmnReportVersion() string + GetRmnRemoteContractAddress() string + GetRmnHomeConfigDigest() cciptypes.Bytes32 +} + +type RmnRemotePoller struct { + rmnRemoteConfig rmntypes.RMNRemoteConfig +} + +func NewRMNRemotePoller() RMNRemote { + return &RmnRemotePoller{ + rmnRemoteConfig: rmntypes.RMNRemoteConfig{}, + } +} + +func (r *RmnRemotePoller) GetMinSigners() uint64 { + panic("implement me") +} + +func (r *RmnRemotePoller) GetSignersInfo() []rmntypes.RMNRemoteSignerInfo { + panic("implement me") +} + +func (r *RmnRemotePoller) GetRmnReportVersion() string { + panic("implement me") +} + +func (r *RmnRemotePoller) GetRmnRemoteContractAddress() string { + panic("implement me") +} + +func (r *RmnRemotePoller) GetRmnHomeConfigDigest() cciptypes.Bytes32 { + panic("implement me") +} + +var _ RMNRemote = (*RmnRemotePoller)(nil) diff --git a/internal/reader/test_helpers.go b/internal/reader/test_helpers.go new file mode 100644 index 00000000..f793d9e8 --- /dev/null +++ b/internal/reader/test_helpers.go @@ -0,0 +1,20 @@ +package reader + +import ( + "github.com/smartcontractkit/libocr/commontypes" + libocrtypes "github.com/smartcontractkit/libocr/ragep2p/types" + + cciptypes "github.com/smartcontractkit/chainlink-common/pkg/types/ccipocr3" +) + +var ( + chainA = cciptypes.ChainSelector(1) + chainB = cciptypes.ChainSelector(2) + chainC = cciptypes.ChainSelector(3) + oracleAId = commontypes.OracleID(1) + p2pOracleAId = libocrtypes.PeerID{byte(oracleAId)} + oracleBId = commontypes.OracleID(2) + p2pOracleBId = libocrtypes.PeerID{byte(oracleBId)} + oracleCId = commontypes.OracleID(3) + p2pOracleCId = libocrtypes.PeerID{byte(oracleCId)} +) diff --git a/mocks/internal_/reader/rmn_home.go b/mocks/internal_/reader/rmn_home.go new file mode 100644 index 00000000..f1f8aa28 --- /dev/null +++ b/mocks/internal_/reader/rmn_home.go @@ -0,0 +1,498 @@ +// Code generated by mockery v2.43.2. DO NOT EDIT. + +package reader + +import ( + context "context" + + ccipocr3 "github.com/smartcontractkit/chainlink-common/pkg/types/ccipocr3" + + mock "github.com/stretchr/testify/mock" + + types "github.com/smartcontractkit/chainlink-ccip/commit/merkleroot/rmn/types" +) + +// MockRMNHome is an autogenerated mock type for the RMNHome type +type MockRMNHome struct { + mock.Mock +} + +type MockRMNHome_Expecter struct { + mock *mock.Mock +} + +func (_m *MockRMNHome) EXPECT() *MockRMNHome_Expecter { + return &MockRMNHome_Expecter{mock: &_m.Mock} +} + +// Close provides a mock function with given fields: +func (_m *MockRMNHome) Close() error { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Close") + } + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockRMNHome_Close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Close' +type MockRMNHome_Close_Call struct { + *mock.Call +} + +// Close is a helper method to define mock.On call +func (_e *MockRMNHome_Expecter) Close() *MockRMNHome_Close_Call { + return &MockRMNHome_Close_Call{Call: _e.mock.On("Close")} +} + +func (_c *MockRMNHome_Close_Call) Run(run func()) *MockRMNHome_Close_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *MockRMNHome_Close_Call) Return(_a0 error) *MockRMNHome_Close_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockRMNHome_Close_Call) RunAndReturn(run func() error) *MockRMNHome_Close_Call { + _c.Call.Return(run) + return _c +} + +// GetMinObservers provides a mock function with given fields: configDigest +func (_m *MockRMNHome) GetMinObservers(configDigest ccipocr3.Bytes32) (map[ccipocr3.ChainSelector]uint64, error) { + ret := _m.Called(configDigest) + + if len(ret) == 0 { + panic("no return value specified for GetMinObservers") + } + + var r0 map[ccipocr3.ChainSelector]uint64 + var r1 error + if rf, ok := ret.Get(0).(func(ccipocr3.Bytes32) (map[ccipocr3.ChainSelector]uint64, error)); ok { + return rf(configDigest) + } + if rf, ok := ret.Get(0).(func(ccipocr3.Bytes32) map[ccipocr3.ChainSelector]uint64); ok { + r0 = rf(configDigest) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(map[ccipocr3.ChainSelector]uint64) + } + } + + if rf, ok := ret.Get(1).(func(ccipocr3.Bytes32) error); ok { + r1 = rf(configDigest) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockRMNHome_GetMinObservers_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetMinObservers' +type MockRMNHome_GetMinObservers_Call struct { + *mock.Call +} + +// GetMinObservers is a helper method to define mock.On call +// - configDigest ccipocr3.Bytes32 +func (_e *MockRMNHome_Expecter) GetMinObservers(configDigest interface{}) *MockRMNHome_GetMinObservers_Call { + return &MockRMNHome_GetMinObservers_Call{Call: _e.mock.On("GetMinObservers", configDigest)} +} + +func (_c *MockRMNHome_GetMinObservers_Call) Run(run func(configDigest ccipocr3.Bytes32)) *MockRMNHome_GetMinObservers_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(ccipocr3.Bytes32)) + }) + return _c +} + +func (_c *MockRMNHome_GetMinObservers_Call) Return(_a0 map[ccipocr3.ChainSelector]uint64, _a1 error) *MockRMNHome_GetMinObservers_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockRMNHome_GetMinObservers_Call) RunAndReturn(run func(ccipocr3.Bytes32) (map[ccipocr3.ChainSelector]uint64, error)) *MockRMNHome_GetMinObservers_Call { + _c.Call.Return(run) + return _c +} + +// GetOffChainConfig provides a mock function with given fields: configDigest +func (_m *MockRMNHome) GetOffChainConfig(configDigest ccipocr3.Bytes32) (ccipocr3.Bytes, error) { + ret := _m.Called(configDigest) + + if len(ret) == 0 { + panic("no return value specified for GetOffChainConfig") + } + + var r0 ccipocr3.Bytes + var r1 error + if rf, ok := ret.Get(0).(func(ccipocr3.Bytes32) (ccipocr3.Bytes, error)); ok { + return rf(configDigest) + } + if rf, ok := ret.Get(0).(func(ccipocr3.Bytes32) ccipocr3.Bytes); ok { + r0 = rf(configDigest) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(ccipocr3.Bytes) + } + } + + if rf, ok := ret.Get(1).(func(ccipocr3.Bytes32) error); ok { + r1 = rf(configDigest) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockRMNHome_GetOffChainConfig_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetOffChainConfig' +type MockRMNHome_GetOffChainConfig_Call struct { + *mock.Call +} + +// GetOffChainConfig is a helper method to define mock.On call +// - configDigest ccipocr3.Bytes32 +func (_e *MockRMNHome_Expecter) GetOffChainConfig(configDigest interface{}) *MockRMNHome_GetOffChainConfig_Call { + return &MockRMNHome_GetOffChainConfig_Call{Call: _e.mock.On("GetOffChainConfig", configDigest)} +} + +func (_c *MockRMNHome_GetOffChainConfig_Call) Run(run func(configDigest ccipocr3.Bytes32)) *MockRMNHome_GetOffChainConfig_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(ccipocr3.Bytes32)) + }) + return _c +} + +func (_c *MockRMNHome_GetOffChainConfig_Call) Return(_a0 ccipocr3.Bytes, _a1 error) *MockRMNHome_GetOffChainConfig_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockRMNHome_GetOffChainConfig_Call) RunAndReturn(run func(ccipocr3.Bytes32) (ccipocr3.Bytes, error)) *MockRMNHome_GetOffChainConfig_Call { + _c.Call.Return(run) + return _c +} + +// GetRMNNodesInfo provides a mock function with given fields: configDigest +func (_m *MockRMNHome) GetRMNNodesInfo(configDigest ccipocr3.Bytes32) ([]types.RMNHomeNodeInfo, error) { + ret := _m.Called(configDigest) + + if len(ret) == 0 { + panic("no return value specified for GetRMNNodesInfo") + } + + var r0 []types.RMNHomeNodeInfo + var r1 error + if rf, ok := ret.Get(0).(func(ccipocr3.Bytes32) ([]types.RMNHomeNodeInfo, error)); ok { + return rf(configDigest) + } + if rf, ok := ret.Get(0).(func(ccipocr3.Bytes32) []types.RMNHomeNodeInfo); ok { + r0 = rf(configDigest) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]types.RMNHomeNodeInfo) + } + } + + if rf, ok := ret.Get(1).(func(ccipocr3.Bytes32) error); ok { + r1 = rf(configDigest) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockRMNHome_GetRMNNodesInfo_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetRMNNodesInfo' +type MockRMNHome_GetRMNNodesInfo_Call struct { + *mock.Call +} + +// GetRMNNodesInfo is a helper method to define mock.On call +// - configDigest ccipocr3.Bytes32 +func (_e *MockRMNHome_Expecter) GetRMNNodesInfo(configDigest interface{}) *MockRMNHome_GetRMNNodesInfo_Call { + return &MockRMNHome_GetRMNNodesInfo_Call{Call: _e.mock.On("GetRMNNodesInfo", configDigest)} +} + +func (_c *MockRMNHome_GetRMNNodesInfo_Call) Run(run func(configDigest ccipocr3.Bytes32)) *MockRMNHome_GetRMNNodesInfo_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(ccipocr3.Bytes32)) + }) + return _c +} + +func (_c *MockRMNHome_GetRMNNodesInfo_Call) Return(_a0 []types.RMNHomeNodeInfo, _a1 error) *MockRMNHome_GetRMNNodesInfo_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockRMNHome_GetRMNNodesInfo_Call) RunAndReturn(run func(ccipocr3.Bytes32) ([]types.RMNHomeNodeInfo, error)) *MockRMNHome_GetRMNNodesInfo_Call { + _c.Call.Return(run) + return _c +} + +// HealthReport provides a mock function with given fields: +func (_m *MockRMNHome) HealthReport() map[string]error { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for HealthReport") + } + + var r0 map[string]error + if rf, ok := ret.Get(0).(func() map[string]error); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(map[string]error) + } + } + + return r0 +} + +// MockRMNHome_HealthReport_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'HealthReport' +type MockRMNHome_HealthReport_Call struct { + *mock.Call +} + +// HealthReport is a helper method to define mock.On call +func (_e *MockRMNHome_Expecter) HealthReport() *MockRMNHome_HealthReport_Call { + return &MockRMNHome_HealthReport_Call{Call: _e.mock.On("HealthReport")} +} + +func (_c *MockRMNHome_HealthReport_Call) Run(run func()) *MockRMNHome_HealthReport_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *MockRMNHome_HealthReport_Call) Return(_a0 map[string]error) *MockRMNHome_HealthReport_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockRMNHome_HealthReport_Call) RunAndReturn(run func() map[string]error) *MockRMNHome_HealthReport_Call { + _c.Call.Return(run) + return _c +} + +// IsRMNHomeConfigDigestSet provides a mock function with given fields: configDigest +func (_m *MockRMNHome) IsRMNHomeConfigDigestSet(configDigest ccipocr3.Bytes32) (bool, error) { + ret := _m.Called(configDigest) + + if len(ret) == 0 { + panic("no return value specified for IsRMNHomeConfigDigestSet") + } + + var r0 bool + var r1 error + if rf, ok := ret.Get(0).(func(ccipocr3.Bytes32) (bool, error)); ok { + return rf(configDigest) + } + if rf, ok := ret.Get(0).(func(ccipocr3.Bytes32) bool); ok { + r0 = rf(configDigest) + } else { + r0 = ret.Get(0).(bool) + } + + if rf, ok := ret.Get(1).(func(ccipocr3.Bytes32) error); ok { + r1 = rf(configDigest) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockRMNHome_IsRMNHomeConfigDigestSet_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsRMNHomeConfigDigestSet' +type MockRMNHome_IsRMNHomeConfigDigestSet_Call struct { + *mock.Call +} + +// IsRMNHomeConfigDigestSet is a helper method to define mock.On call +// - configDigest ccipocr3.Bytes32 +func (_e *MockRMNHome_Expecter) IsRMNHomeConfigDigestSet(configDigest interface{}) *MockRMNHome_IsRMNHomeConfigDigestSet_Call { + return &MockRMNHome_IsRMNHomeConfigDigestSet_Call{Call: _e.mock.On("IsRMNHomeConfigDigestSet", configDigest)} +} + +func (_c *MockRMNHome_IsRMNHomeConfigDigestSet_Call) Run(run func(configDigest ccipocr3.Bytes32)) *MockRMNHome_IsRMNHomeConfigDigestSet_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(ccipocr3.Bytes32)) + }) + return _c +} + +func (_c *MockRMNHome_IsRMNHomeConfigDigestSet_Call) Return(_a0 bool, _a1 error) *MockRMNHome_IsRMNHomeConfigDigestSet_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockRMNHome_IsRMNHomeConfigDigestSet_Call) RunAndReturn(run func(ccipocr3.Bytes32) (bool, error)) *MockRMNHome_IsRMNHomeConfigDigestSet_Call { + _c.Call.Return(run) + return _c +} + +// Name provides a mock function with given fields: +func (_m *MockRMNHome) Name() string { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Name") + } + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// MockRMNHome_Name_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Name' +type MockRMNHome_Name_Call struct { + *mock.Call +} + +// Name is a helper method to define mock.On call +func (_e *MockRMNHome_Expecter) Name() *MockRMNHome_Name_Call { + return &MockRMNHome_Name_Call{Call: _e.mock.On("Name")} +} + +func (_c *MockRMNHome_Name_Call) Run(run func()) *MockRMNHome_Name_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *MockRMNHome_Name_Call) Return(_a0 string) *MockRMNHome_Name_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockRMNHome_Name_Call) RunAndReturn(run func() string) *MockRMNHome_Name_Call { + _c.Call.Return(run) + return _c +} + +// Ready provides a mock function with given fields: +func (_m *MockRMNHome) Ready() error { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Ready") + } + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockRMNHome_Ready_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Ready' +type MockRMNHome_Ready_Call struct { + *mock.Call +} + +// Ready is a helper method to define mock.On call +func (_e *MockRMNHome_Expecter) Ready() *MockRMNHome_Ready_Call { + return &MockRMNHome_Ready_Call{Call: _e.mock.On("Ready")} +} + +func (_c *MockRMNHome_Ready_Call) Run(run func()) *MockRMNHome_Ready_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *MockRMNHome_Ready_Call) Return(_a0 error) *MockRMNHome_Ready_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockRMNHome_Ready_Call) RunAndReturn(run func() error) *MockRMNHome_Ready_Call { + _c.Call.Return(run) + return _c +} + +// Start provides a mock function with given fields: _a0 +func (_m *MockRMNHome) Start(_a0 context.Context) error { + ret := _m.Called(_a0) + + if len(ret) == 0 { + panic("no return value specified for Start") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context) error); ok { + r0 = rf(_a0) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockRMNHome_Start_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Start' +type MockRMNHome_Start_Call struct { + *mock.Call +} + +// Start is a helper method to define mock.On call +// - _a0 context.Context +func (_e *MockRMNHome_Expecter) Start(_a0 interface{}) *MockRMNHome_Start_Call { + return &MockRMNHome_Start_Call{Call: _e.mock.On("Start", _a0)} +} + +func (_c *MockRMNHome_Start_Call) Run(run func(_a0 context.Context)) *MockRMNHome_Start_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context)) + }) + return _c +} + +func (_c *MockRMNHome_Start_Call) Return(_a0 error) *MockRMNHome_Start_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockRMNHome_Start_Call) RunAndReturn(run func(context.Context) error) *MockRMNHome_Start_Call { + _c.Call.Return(run) + return _c +} + +// NewMockRMNHome creates a new instance of MockRMNHome. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockRMNHome(t interface { + mock.TestingT + Cleanup(func()) +}) *MockRMNHome { + mock := &MockRMNHome{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/mocks/internal_/reader/rmn_remote.go b/mocks/internal_/reader/rmn_remote.go new file mode 100644 index 00000000..f2345e3b --- /dev/null +++ b/mocks/internal_/reader/rmn_remote.go @@ -0,0 +1,266 @@ +// Code generated by mockery v2.43.2. DO NOT EDIT. + +package reader + +import ( + ccipocr3 "github.com/smartcontractkit/chainlink-common/pkg/types/ccipocr3" + mock "github.com/stretchr/testify/mock" + + types "github.com/smartcontractkit/chainlink-ccip/commit/merkleroot/rmn/types" +) + +// MockRMNRemote is an autogenerated mock type for the RMNRemote type +type MockRMNRemote struct { + mock.Mock +} + +type MockRMNRemote_Expecter struct { + mock *mock.Mock +} + +func (_m *MockRMNRemote) EXPECT() *MockRMNRemote_Expecter { + return &MockRMNRemote_Expecter{mock: &_m.Mock} +} + +// GetMinSigners provides a mock function with given fields: +func (_m *MockRMNRemote) GetMinSigners() uint64 { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for GetMinSigners") + } + + var r0 uint64 + if rf, ok := ret.Get(0).(func() uint64); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(uint64) + } + + return r0 +} + +// MockRMNRemote_GetMinSigners_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetMinSigners' +type MockRMNRemote_GetMinSigners_Call struct { + *mock.Call +} + +// GetMinSigners is a helper method to define mock.On call +func (_e *MockRMNRemote_Expecter) GetMinSigners() *MockRMNRemote_GetMinSigners_Call { + return &MockRMNRemote_GetMinSigners_Call{Call: _e.mock.On("GetMinSigners")} +} + +func (_c *MockRMNRemote_GetMinSigners_Call) Run(run func()) *MockRMNRemote_GetMinSigners_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *MockRMNRemote_GetMinSigners_Call) Return(_a0 uint64) *MockRMNRemote_GetMinSigners_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockRMNRemote_GetMinSigners_Call) RunAndReturn(run func() uint64) *MockRMNRemote_GetMinSigners_Call { + _c.Call.Return(run) + return _c +} + +// GetRmnHomeConfigDigest provides a mock function with given fields: +func (_m *MockRMNRemote) GetRmnHomeConfigDigest() ccipocr3.Bytes32 { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for GetRmnHomeConfigDigest") + } + + var r0 ccipocr3.Bytes32 + if rf, ok := ret.Get(0).(func() ccipocr3.Bytes32); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(ccipocr3.Bytes32) + } + } + + return r0 +} + +// MockRMNRemote_GetRmnHomeConfigDigest_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetRmnHomeConfigDigest' +type MockRMNRemote_GetRmnHomeConfigDigest_Call struct { + *mock.Call +} + +// GetRmnHomeConfigDigest is a helper method to define mock.On call +func (_e *MockRMNRemote_Expecter) GetRmnHomeConfigDigest() *MockRMNRemote_GetRmnHomeConfigDigest_Call { + return &MockRMNRemote_GetRmnHomeConfigDigest_Call{Call: _e.mock.On("GetRmnHomeConfigDigest")} +} + +func (_c *MockRMNRemote_GetRmnHomeConfigDigest_Call) Run(run func()) *MockRMNRemote_GetRmnHomeConfigDigest_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *MockRMNRemote_GetRmnHomeConfigDigest_Call) Return(_a0 ccipocr3.Bytes32) *MockRMNRemote_GetRmnHomeConfigDigest_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockRMNRemote_GetRmnHomeConfigDigest_Call) RunAndReturn(run func() ccipocr3.Bytes32) *MockRMNRemote_GetRmnHomeConfigDigest_Call { + _c.Call.Return(run) + return _c +} + +// GetRmnRemoteContractAddress provides a mock function with given fields: +func (_m *MockRMNRemote) GetRmnRemoteContractAddress() string { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for GetRmnRemoteContractAddress") + } + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// MockRMNRemote_GetRmnRemoteContractAddress_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetRmnRemoteContractAddress' +type MockRMNRemote_GetRmnRemoteContractAddress_Call struct { + *mock.Call +} + +// GetRmnRemoteContractAddress is a helper method to define mock.On call +func (_e *MockRMNRemote_Expecter) GetRmnRemoteContractAddress() *MockRMNRemote_GetRmnRemoteContractAddress_Call { + return &MockRMNRemote_GetRmnRemoteContractAddress_Call{Call: _e.mock.On("GetRmnRemoteContractAddress")} +} + +func (_c *MockRMNRemote_GetRmnRemoteContractAddress_Call) Run(run func()) *MockRMNRemote_GetRmnRemoteContractAddress_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *MockRMNRemote_GetRmnRemoteContractAddress_Call) Return(_a0 string) *MockRMNRemote_GetRmnRemoteContractAddress_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockRMNRemote_GetRmnRemoteContractAddress_Call) RunAndReturn(run func() string) *MockRMNRemote_GetRmnRemoteContractAddress_Call { + _c.Call.Return(run) + return _c +} + +// GetRmnReportVersion provides a mock function with given fields: +func (_m *MockRMNRemote) GetRmnReportVersion() string { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for GetRmnReportVersion") + } + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// MockRMNRemote_GetRmnReportVersion_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetRmnReportVersion' +type MockRMNRemote_GetRmnReportVersion_Call struct { + *mock.Call +} + +// GetRmnReportVersion is a helper method to define mock.On call +func (_e *MockRMNRemote_Expecter) GetRmnReportVersion() *MockRMNRemote_GetRmnReportVersion_Call { + return &MockRMNRemote_GetRmnReportVersion_Call{Call: _e.mock.On("GetRmnReportVersion")} +} + +func (_c *MockRMNRemote_GetRmnReportVersion_Call) Run(run func()) *MockRMNRemote_GetRmnReportVersion_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *MockRMNRemote_GetRmnReportVersion_Call) Return(_a0 string) *MockRMNRemote_GetRmnReportVersion_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockRMNRemote_GetRmnReportVersion_Call) RunAndReturn(run func() string) *MockRMNRemote_GetRmnReportVersion_Call { + _c.Call.Return(run) + return _c +} + +// GetSignersInfo provides a mock function with given fields: +func (_m *MockRMNRemote) GetSignersInfo() []types.RMNRemoteSignerInfo { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for GetSignersInfo") + } + + var r0 []types.RMNRemoteSignerInfo + if rf, ok := ret.Get(0).(func() []types.RMNRemoteSignerInfo); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]types.RMNRemoteSignerInfo) + } + } + + return r0 +} + +// MockRMNRemote_GetSignersInfo_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetSignersInfo' +type MockRMNRemote_GetSignersInfo_Call struct { + *mock.Call +} + +// GetSignersInfo is a helper method to define mock.On call +func (_e *MockRMNRemote_Expecter) GetSignersInfo() *MockRMNRemote_GetSignersInfo_Call { + return &MockRMNRemote_GetSignersInfo_Call{Call: _e.mock.On("GetSignersInfo")} +} + +func (_c *MockRMNRemote_GetSignersInfo_Call) Run(run func()) *MockRMNRemote_GetSignersInfo_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *MockRMNRemote_GetSignersInfo_Call) Return(_a0 []types.RMNRemoteSignerInfo) *MockRMNRemote_GetSignersInfo_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockRMNRemote_GetSignersInfo_Call) RunAndReturn(run func() []types.RMNRemoteSignerInfo) *MockRMNRemote_GetSignersInfo_Call { + _c.Call.Return(run) + return _c +} + +// NewMockRMNRemote creates a new instance of MockRMNRemote. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockRMNRemote(t interface { + mock.TestingT + Cleanup(func()) +}) *MockRMNRemote { + mock := &MockRMNRemote{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/consts/consts.go b/pkg/consts/consts.go index 078d2f7a..256934d1 100644 --- a/pkg/consts/consts.go +++ b/pkg/consts/consts.go @@ -11,6 +11,8 @@ const ( ContractNameCCIPConfig = "CCIPConfig" ContractNamePriceAggregator = "AggregatorV3Interface" ContractNameNonceManager = "NonceManager" + ContractNameRMNHome = "RMNHome" + ContractNameRMNRemote = "RMNRemote" ) // Method Names @@ -73,6 +75,10 @@ const ( // Used by the home chain reader. MethodNameGetAllChainConfigs = "GetAllChainConfigs" MethodNameGetOCRConfig = "GetOCRConfig" + + // RMNHome.sol methods + // Used by the rmn home reader. + MethodNameGetVersionedConfigsWithDigests = "GetVersionedConfigsWithDigests" ) // Event Names From c0f3601e2e74e7a45e6f9e4712eaa135f7b9845d Mon Sep 17 00:00:00 2001 From: nogo <0xnogo@gmail.com> Date: Thu, 19 Sep 2024 11:43:07 +0400 Subject: [PATCH 02/16] remove RMNConfig struct --- commit/merkleroot/rmn/types/config.go | 6 ------ 1 file changed, 6 deletions(-) diff --git a/commit/merkleroot/rmn/types/config.go b/commit/merkleroot/rmn/types/config.go index 370fdfd2..431e791b 100644 --- a/commit/merkleroot/rmn/types/config.go +++ b/commit/merkleroot/rmn/types/config.go @@ -10,12 +10,6 @@ import ( type NodeID uint32 -// RMNConfig contains the RMN configuration required by the plugin and the RMN client in a single struct. -type RMNConfig struct { - Home RMNHomeConfig - Remote RMNRemoteConfig -} - // RMNHomeConfig contains the configuration fetched from the RMNHome contract. type RMNHomeConfig struct { Nodes []RMNHomeNodeInfo From 7a51db6d705416ed28a49bd5004b119bc5400be1 Mon Sep 17 00:00:00 2001 From: nogo <0xnogo@gmail.com> Date: Thu, 19 Sep 2024 12:13:46 +0400 Subject: [PATCH 03/16] use new getAllConfigs --- internal/reader/rmn_home.go | 68 +++++++--- internal/reader/rmn_home_test.go | 225 ++++++++++++++++++------------- pkg/consts/consts.go | 2 +- 3 files changed, 182 insertions(+), 113 deletions(-) diff --git a/internal/reader/rmn_home.go b/internal/reader/rmn_home.go index 8782b9ae..180ef48e 100644 --- a/internal/reader/rmn_home.go +++ b/internal/reader/rmn_home.go @@ -39,6 +39,12 @@ type RMNHome interface { services.Service } +type rmnHomeState struct { + primaryConfigDigest cciptypes.Bytes32 + secondaryConfigDigest cciptypes.Bytes32 + rmnHomeConfig map[cciptypes.Bytes32]rmntypes.RMNHomeConfig +} + type RmnHomePoller struct { wg sync.WaitGroup stopCh services.StopChan @@ -47,7 +53,7 @@ type RmnHomePoller struct { rmnHomeBoundContract types.BoundContract lggr logger.Logger mutex *sync.RWMutex - rmnHomeConfig map[cciptypes.Bytes32]rmntypes.RMNHomeConfig + rmnHomeState rmnHomeState failedPolls uint pollingDuration time.Duration // How frequently the poller fetches the chain configs } @@ -62,7 +68,7 @@ func NewRMNHomePoller( stopCh: make(chan struct{}), contractReader: contractReader, rmnHomeBoundContract: rmnHomeBoundContract, - rmnHomeConfig: make(map[cciptypes.Bytes32]rmntypes.RMNHomeConfig), + rmnHomeState: rmnHomeState{}, mutex: &sync.RWMutex{}, failedPolls: 0, lggr: lggr, @@ -112,25 +118,39 @@ func (r *RmnHomePoller) fetchAndSetRmnHomeConfigs(ctx context.Context) error { var versionedConfigWithDigests []rmntypes.VersionedConfigWithDigest err := r.contractReader.GetLatestValue( ctx, - r.rmnHomeBoundContract.ReadIdentifier(consts.MethodNameGetVersionedConfigsWithDigests), + r.rmnHomeBoundContract.ReadIdentifier(consts.MethodNameGetAllConfigs), primitives.Unconfirmed, - map[string]interface{}{ - "offset": 0, - "limit": 2, // TODO: fetch CONFIG_RING_BUFFER_SIZE - }, + map[string]interface{}{}, &versionedConfigWithDigests, ) if err != nil { return fmt.Errorf("error fetching RMNHomeConfig: %w", err) } - // TODO: fetch CONFIG_RING_BUFFER_SIZE and compare with len(versionedConfigWithDigests) - if len(versionedConfigWithDigests) > 2 { - r.lggr.Errorw("more than 2 RMNHomeConfigs found", "numConfigs", len(versionedConfigWithDigests), "requestedLimit", 2) - return fmt.Errorf("more than 2 RMNHomeConfigs found") + if len(versionedConfigWithDigests) != 2 { + r.lggr.Warnw("expected 2 RMNHomeConfigs, got", "count", len(versionedConfigWithDigests)) + return fmt.Errorf("expected 2 RMNHomeConfigs, got %d", len(versionedConfigWithDigests)) } - r.setRMNHomeState(convertOnChainConfigToRMNHomeChainConfig(r.lggr, versionedConfigWithDigests)) + var primaryConfigDigest, secondaryConfigDigest cciptypes.Bytes32 + + // check if the versionesconfigwithdigests are set (can be empty) + if versionedConfigWithDigests[0].ConfigDigest == (cciptypes.Bytes32{}) { + r.lggr.Warnw("primary config digest is empty") + } else { + primaryConfigDigest = versionedConfigWithDigests[0].ConfigDigest + } + + if versionedConfigWithDigests[1].ConfigDigest == (cciptypes.Bytes32{}) { + r.lggr.Warnw("secondary config digest is empty") + } else { + secondaryConfigDigest = versionedConfigWithDigests[1].ConfigDigest + } + + r.setRMNHomeState( + primaryConfigDigest, + secondaryConfigDigest, + convertOnChainConfigToRMNHomeChainConfig(r.lggr, versionedConfigWithDigests)) if len(versionedConfigWithDigests) == 0 { // That's a legitimate case if there are no rmn configs on chain yet @@ -141,35 +161,42 @@ func (r *RmnHomePoller) fetchAndSetRmnHomeConfigs(ctx context.Context) error { return nil } -func (r *RmnHomePoller) setRMNHomeState(rmnHomeConfig map[cciptypes.Bytes32]rmntypes.RMNHomeConfig) { +func (r *RmnHomePoller) setRMNHomeState( + primaryConfigDigest cciptypes.Bytes32, + secondaryConfigDigest cciptypes.Bytes32, + rmnHomeConfig map[cciptypes.Bytes32]rmntypes.RMNHomeConfig) { r.mutex.Lock() defer r.mutex.Unlock() - r.rmnHomeConfig = rmnHomeConfig + s := &r.rmnHomeState + + s.primaryConfigDigest = primaryConfigDigest + s.secondaryConfigDigest = secondaryConfigDigest + s.rmnHomeConfig = rmnHomeConfig } func (r *RmnHomePoller) GetRMNNodesInfo(configDigest cciptypes.Bytes32) ([]rmntypes.RMNHomeNodeInfo, error) { r.mutex.RLock() defer r.mutex.RUnlock() - return r.rmnHomeConfig[configDigest].Nodes, nil + return r.rmnHomeState.rmnHomeConfig[configDigest].Nodes, nil } func (r *RmnHomePoller) IsRMNHomeConfigDigestSet(configDigest cciptypes.Bytes32) (bool, error) { r.mutex.RLock() defer r.mutex.RUnlock() - _, ok := r.rmnHomeConfig[configDigest] + _, ok := r.rmnHomeState.rmnHomeConfig[configDigest] return ok, nil } func (r *RmnHomePoller) GetMinObservers(configDigest cciptypes.Bytes32) (map[cciptypes.ChainSelector]uint64, error) { r.mutex.RLock() defer r.mutex.RUnlock() - return r.rmnHomeConfig[configDigest].MinObservers, nil + return r.rmnHomeState.rmnHomeConfig[configDigest].MinObservers, nil } func (r *RmnHomePoller) GetOffChainConfig(configDigest cciptypes.Bytes32) (cciptypes.Bytes, error) { r.mutex.RLock() defer r.mutex.RUnlock() - return r.rmnHomeConfig[configDigest].OffchainConfig, nil + return r.rmnHomeState.rmnHomeConfig[configDigest].OffchainConfig, nil } func (r *RmnHomePoller) Close() error { @@ -210,6 +237,11 @@ func convertOnChainConfigToRMNHomeChainConfig( rmnHomeConfigs := make(map[cciptypes.Bytes32]rmntypes.RMNHomeConfig) for _, versionedConfigWithDigest := range versionedConfigWithDigests { + // check if the versionesconfigwithdigests are set (can be empty) + if versionedConfigWithDigest.ConfigDigest == (cciptypes.Bytes32{}) { + lggr.Warnw("config digest is empty") + continue + } config := versionedConfigWithDigest.VersionedConfig.Config nodes := make([]rmntypes.RMNHomeNodeInfo, len(config.Nodes)) for i, node := range config.Nodes { diff --git a/internal/reader/rmn_home_test.go b/internal/reader/rmn_home_test.go index 207f3235..1263b3bd 100644 --- a/internal/reader/rmn_home_test.go +++ b/internal/reader/rmn_home_test.go @@ -60,66 +60,125 @@ func TestRMNHomeChainConfigPoller_HealthReport(t *testing.T) { } func Test_RMNHomePollingWorking(t *testing.T) { - rmnHomeOnChainConfigs := createTestRMNHomeConfigs() - homeChainReader := readermock.NewMockContractReaderFacade(t) - homeChainReader.On( - "GetLatestValue", - mock.Anything, - rmnHomeBoundContract.ReadIdentifier(consts.MethodNameGetVersionedConfigsWithDigests), - mock.Anything, - mock.Anything, - mock.Anything, - ).Run( - func(args mock.Arguments) { - arg := args.Get(4).(*[]rmntypes.VersionedConfigWithDigest) - *arg = rmnHomeOnChainConfigs - }).Return(nil) + tests := []struct { + name string + primaryEmpty bool + secondaryEmpty bool + expectedCallCount int + }{ + { + name: "Both configs non-empty", + primaryEmpty: false, + secondaryEmpty: false, + expectedCallCount: 4, + }, + { + name: "Primary config empty", + primaryEmpty: true, + secondaryEmpty: false, + expectedCallCount: 4, + }, + { + name: "Secondary config empty", + primaryEmpty: false, + secondaryEmpty: true, + expectedCallCount: 4, + }, + { + name: "Both configs empty", + primaryEmpty: true, + secondaryEmpty: true, + expectedCallCount: 4, + }, + } - defer homeChainReader.AssertExpectations(t) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + primaryConfig, secondaryConfig := createTestRMNHomeConfigs(tt.primaryEmpty, tt.secondaryEmpty) + rmnHomeOnChainConfigs := []rmntypes.VersionedConfigWithDigest{primaryConfig, secondaryConfig} - var ( - tickTime = 2 * time.Millisecond - totalSleepTime = tickTime * 20 - ) + homeChainReader := readermock.NewMockContractReaderFacade(t) + homeChainReader.On( + "GetLatestValue", + mock.Anything, + rmnHomeBoundContract.ReadIdentifier(consts.MethodNameGetAllConfigs), + mock.Anything, + mock.Anything, + mock.Anything, + ).Run( + func(args mock.Arguments) { + arg := args.Get(4).(*[]rmntypes.VersionedConfigWithDigest) + *arg = rmnHomeOnChainConfigs + }).Return(nil) - configPoller := NewRMNHomePoller( - homeChainReader, - rmnHomeBoundContract, - logger.Test(t), - tickTime, - ) + defer homeChainReader.AssertExpectations(t) - require.NoError(t, configPoller.Start(context.Background())) - // sleep to allow polling to happen - time.Sleep(totalSleepTime) - require.NoError(t, configPoller.Close()) + var ( + tickTime = 2 * time.Millisecond + totalSleepTime = tickTime * 20 + ) - calls := homeChainReader.Calls - callCount := 0 - for _, call := range calls { - if call.Method == "GetLatestValue" { - callCount++ - } - } - // called at least 4 times, one for start and one for the first tick for each contract (ccip *2 and rmn *2) - require.GreaterOrEqual(t, callCount, 4) + configPoller := NewRMNHomePoller( + homeChainReader, + rmnHomeBoundContract, + logger.Test(t), + tickTime, + ) - for _, configs := range rmnHomeOnChainConfigs { - rmnNodes, err := configPoller.GetRMNNodesInfo(configs.ConfigDigest) - require.NoError(t, err) - require.NotNil(t, rmnNodes) + require.NoError(t, configPoller.Start(context.Background())) + // sleep to allow polling to happen + time.Sleep(totalSleepTime) + require.NoError(t, configPoller.Close()) - isValid, err := configPoller.IsRMNHomeConfigDigestSet(configs.ConfigDigest) - require.NoError(t, err) - require.True(t, isValid) + calls := homeChainReader.Calls + callCount := 0 + for _, call := range calls { + if call.Method == "GetLatestValue" { + callCount++ + } + } + require.GreaterOrEqual(t, callCount, tt.expectedCallCount) + + for i, config := range rmnHomeOnChainConfigs { + isEmpty := (i == 0 && tt.primaryEmpty) || (i == 1 && tt.secondaryEmpty) - offchainConfig, err := configPoller.GetOffChainConfig(configs.ConfigDigest) - require.NoError(t, err) - require.NotNil(t, offchainConfig) + rmnNodes, err := configPoller.GetRMNNodesInfo(config.ConfigDigest) + require.NoError(t, err) + if isEmpty { + require.Empty(t, rmnNodes) + } else { + require.NotEmpty(t, rmnNodes) + } + + isValid, err := configPoller.IsRMNHomeConfigDigestSet(config.ConfigDigest) + require.NoError(t, err) + if isEmpty { + require.False(t, isValid) + } else { + require.True(t, isValid) + } - minObs, err := configPoller.GetMinObservers(configs.ConfigDigest) - require.NoError(t, err) - require.NotNil(t, minObs) + offchainConfig, err := configPoller.GetOffChainConfig(config.ConfigDigest) + require.NoError(t, err) + if isEmpty { + require.Empty(t, offchainConfig) + } else { + require.NotEmpty(t, offchainConfig) + } + + minObsMap, err := configPoller.GetMinObservers(config.ConfigDigest) + require.NoError(t, err) + if isEmpty { + require.Empty(t, minObsMap) + } else { + require.Len(t, minObsMap, 1) + expectedChainSelector := cciptypes.ChainSelector(uint64(i + 1)) + minObs, exists := minObsMap[expectedChainSelector] + require.True(t, exists) + require.Equal(t, uint64(i+1), minObs) + } + } + }) } } @@ -246,60 +305,38 @@ func TestIsNodeObserver(t *testing.T) { } } -func createTestRMNHomeConfigs() []rmntypes.VersionedConfigWithDigest { - return []rmntypes.VersionedConfigWithDigest{ - { - ConfigDigest: cciptypes.Bytes32{1, 2, 3, 4, 5}, - VersionedConfig: rmntypes.VersionedConfig{ - Version: 1, - Config: rmntypes.Config{ - Nodes: []rmntypes.Node{ - { - PeerID: cciptypes.Bytes32{10, 11, 12, 13, 14}, - OffchainPublicKey: cciptypes.Bytes32{20, 21, 22, 23, 24}, - }, - { - PeerID: cciptypes.Bytes32{15, 16, 17, 18, 19}, - OffchainPublicKey: cciptypes.Bytes32{25, 26, 27, 28, 29}, - }, - }, - SourceChains: []rmntypes.SourceChain{ - { - ChainSelector: cciptypes.ChainSelector(1), - MinObservers: 1, - ObserverNodesBitmap: cciptypes.NewBigInt(big.NewInt(2)), // 10 in binary = 1 is observer and 0 is not - }, - { - ChainSelector: cciptypes.ChainSelector(2), - MinObservers: 2, - ObserverNodesBitmap: cciptypes.NewBigInt(big.NewInt(3)), // 11 in binary = 0 and 1 are observers - }, - }, - OffchainConfig: cciptypes.Bytes{30, 31, 32, 33, 34}, - }, - }, - }, - { - ConfigDigest: cciptypes.Bytes32{6, 7, 8, 9, 10}, +func createTestRMNHomeConfigs( + primaryEmpty bool, + secondaryEmpty bool) (primary, secondary rmntypes.VersionedConfigWithDigest) { + createConfig := func(id byte, isEmpty bool) rmntypes.VersionedConfigWithDigest { + if isEmpty { + return rmntypes.VersionedConfigWithDigest{} + } + return rmntypes.VersionedConfigWithDigest{ + ConfigDigest: cciptypes.Bytes32{id}, VersionedConfig: rmntypes.VersionedConfig{ - Version: 2, + Version: uint32(id), Config: rmntypes.Config{ Nodes: []rmntypes.Node{ { - PeerID: cciptypes.Bytes32{40, 41, 42, 43, 44}, - OffchainPublicKey: cciptypes.Bytes32{50, 51, 52, 53, 54}, + PeerID: cciptypes.Bytes32{10 * id}, + OffchainPublicKey: cciptypes.Bytes32{20 * id}, }, }, SourceChains: []rmntypes.SourceChain{ { - ChainSelector: cciptypes.ChainSelector(1), - MinObservers: 1, - ObserverNodesBitmap: cciptypes.NewBigInt(big.NewInt(1)), // 1 in binary = 0 is an observer + ChainSelector: cciptypes.ChainSelector(uint64(id)), + MinObservers: uint64(id), + ObserverNodesBitmap: cciptypes.NewBigInt(big.NewInt(int64(id))), }, }, - OffchainConfig: cciptypes.Bytes{60, 61, 62, 63, 64}, + OffchainConfig: cciptypes.Bytes{30 * id}, }, }, - }, + } } + + primary = createConfig(1, primaryEmpty) + secondary = createConfig(2, secondaryEmpty) + return primary, secondary } diff --git a/pkg/consts/consts.go b/pkg/consts/consts.go index 256934d1..3ab26479 100644 --- a/pkg/consts/consts.go +++ b/pkg/consts/consts.go @@ -78,7 +78,7 @@ const ( // RMNHome.sol methods // Used by the rmn home reader. - MethodNameGetVersionedConfigsWithDigests = "GetVersionedConfigsWithDigests" + MethodNameGetAllConfigs = "getAllConfigs" ) // Event Names From b34bd6759641a0e458521aecabc39c18faeee145 Mon Sep 17 00:00:00 2001 From: nogo <0xnogo@gmail.com> Date: Thu, 19 Sep 2024 14:35:36 +0400 Subject: [PATCH 04/16] addressing comments --- commit/merkleroot/rmn/types/config.go | 22 +++---- internal/reader/rmn_home.go | 87 +++++++++++++++++---------- internal/reader/rmn_home_test.go | 17 ++++++ 3 files changed, 84 insertions(+), 42 deletions(-) diff --git a/commit/merkleroot/rmn/types/config.go b/commit/merkleroot/rmn/types/config.go index 431e791b..099275d8 100644 --- a/commit/merkleroot/rmn/types/config.go +++ b/commit/merkleroot/rmn/types/config.go @@ -12,18 +12,18 @@ type NodeID uint32 // RMNHomeConfig contains the configuration fetched from the RMNHome contract. type RMNHomeConfig struct { - Nodes []RMNHomeNodeInfo - MinObservers map[cciptypes.ChainSelector]uint64 - ConfigDigest cciptypes.Bytes32 - OffchainConfig cciptypes.Bytes // Raw offchain configuration bytes + Nodes []RMNHomeNodeInfo + SourceChainMinObservers map[cciptypes.ChainSelector]uint64 + ConfigDigest cciptypes.Bytes32 + OffchainConfig cciptypes.Bytes // The raw offchain config } // RMNHomeNodeInfo contains information about a node from the RMNHome contract. type RMNHomeNodeInfo struct { - ID NodeID // ID is the index of this node in the RMN config - PeerID cciptypes.Bytes32 - SupportedSourceChains mapset.Set[cciptypes.ChainSelector] - SignObservationsPublicKey *ed25519.PublicKey // offchainPublicKey + ID NodeID // ID is the index of this node in the RMN config + PeerID cciptypes.Bytes32 // The peer ID of the node + SupportedSourceChains mapset.Set[cciptypes.ChainSelector] // Set of supported source chains by the node + OffchainPublicKey *ed25519.PublicKey // The private key is used to verify observations } // RMNRemoteConfig contains the configuration fetched from the RMNRemote contract. @@ -38,9 +38,9 @@ type RMNRemoteConfig struct { // RMNRemoteSignerInfo contains information about a signer from the RMNRemote contract. type RMNRemoteSignerInfo struct { - SignReportsAddress cciptypes.Bytes // for signing reports - NodeIndex uint64 // maps to nodes in RMNHome - SignObservationPrefix string // for signing observations + SignerOnchainAddress cciptypes.Bytes // The signer's onchain address, used to verify report signature + NodeIndex uint64 // The index of the node in the RMN config + SignObservationPrefix string // The prefix of the observation to sign } // VersionedConfigWithDigest mirrors RMNHome.sol's VersionedConfigWithDigest struct diff --git a/internal/reader/rmn_home.go b/internal/reader/rmn_home.go index 180ef48e..59c9c4a4 100644 --- a/internal/reader/rmn_home.go +++ b/internal/reader/rmn_home.go @@ -24,7 +24,8 @@ import ( ) const ( - rmnMaxSizeCommittee = 256 // bitmap is 256 bits making the max committee size 256 + expectedRMNHomeConfigs = 2 // RMNHome contract should have 2 configs, primary and secondary + rmnMaxSizeCommittee = 256 // bitmap is 256 bits making the max committee size 256 ) type RMNHome interface { @@ -45,14 +46,16 @@ type rmnHomeState struct { rmnHomeConfig map[cciptypes.Bytes32]rmntypes.RMNHomeConfig } +// RmnHomePoller polls the RMNHome contract for the latest RMNHomeConfigs +// It is running in the backdoung with a polling interval of pollingDuration type RmnHomePoller struct { wg sync.WaitGroup stopCh services.StopChan sync services.StateMachine + mutex *sync.RWMutex contractReader contractreader.ContractReaderFacade rmnHomeBoundContract types.BoundContract lggr logger.Logger - mutex *sync.RWMutex rmnHomeState rmnHomeState failedPolls uint pollingDuration time.Duration // How frequently the poller fetches the chain configs @@ -127,24 +130,19 @@ func (r *RmnHomePoller) fetchAndSetRmnHomeConfigs(ctx context.Context) error { return fmt.Errorf("error fetching RMNHomeConfig: %w", err) } - if len(versionedConfigWithDigests) != 2 { + if len(versionedConfigWithDigests) != expectedRMNHomeConfigs { r.lggr.Warnw("expected 2 RMNHomeConfigs, got", "count", len(versionedConfigWithDigests)) return fmt.Errorf("expected 2 RMNHomeConfigs, got %d", len(versionedConfigWithDigests)) } - var primaryConfigDigest, secondaryConfigDigest cciptypes.Bytes32 - - // check if the versionesconfigwithdigests are set (can be empty) - if versionedConfigWithDigests[0].ConfigDigest == (cciptypes.Bytes32{}) { + primaryConfigDigest := versionedConfigWithDigests[0].ConfigDigest + if primaryConfigDigest.IsEmpty() { r.lggr.Warnw("primary config digest is empty") - } else { - primaryConfigDigest = versionedConfigWithDigests[0].ConfigDigest } - if versionedConfigWithDigests[1].ConfigDigest == (cciptypes.Bytes32{}) { + secondaryConfigDigest := versionedConfigWithDigests[1].ConfigDigest + if secondaryConfigDigest.IsEmpty() { r.lggr.Warnw("secondary config digest is empty") - } else { - secondaryConfigDigest = versionedConfigWithDigests[1].ConfigDigest } r.setRMNHomeState( @@ -152,12 +150,6 @@ func (r *RmnHomePoller) fetchAndSetRmnHomeConfigs(ctx context.Context) error { secondaryConfigDigest, convertOnChainConfigToRMNHomeChainConfig(r.lggr, versionedConfigWithDigests)) - if len(versionedConfigWithDigests) == 0 { - // That's a legitimate case if there are no rmn configs on chain yet - r.lggr.Warnw("no on chain configs found") - return nil - } - return nil } @@ -177,6 +169,12 @@ func (r *RmnHomePoller) setRMNHomeState( func (r *RmnHomePoller) GetRMNNodesInfo(configDigest cciptypes.Bytes32) ([]rmntypes.RMNHomeNodeInfo, error) { r.mutex.RLock() defer r.mutex.RUnlock() + _, ok := r.rmnHomeState.rmnHomeConfig[configDigest] + if !ok { + if !ok { + return nil, fmt.Errorf("configDigest %s not found in RMNHomeConfig", configDigest) + } + } return r.rmnHomeState.rmnHomeConfig[configDigest].Nodes, nil } @@ -184,18 +182,35 @@ func (r *RmnHomePoller) IsRMNHomeConfigDigestSet(configDigest cciptypes.Bytes32) r.mutex.RLock() defer r.mutex.RUnlock() _, ok := r.rmnHomeState.rmnHomeConfig[configDigest] - return ok, nil + if !ok { + if !ok { + return false, fmt.Errorf("configDigest %s not found in RMNHomeConfig", configDigest) + } + } + return true, nil } func (r *RmnHomePoller) GetMinObservers(configDigest cciptypes.Bytes32) (map[cciptypes.ChainSelector]uint64, error) { r.mutex.RLock() defer r.mutex.RUnlock() - return r.rmnHomeState.rmnHomeConfig[configDigest].MinObservers, nil + _, ok := r.rmnHomeState.rmnHomeConfig[configDigest] + if !ok { + if !ok { + return nil, fmt.Errorf("configDigest %s not found in RMNHomeConfig", configDigest) + } + } + return r.rmnHomeState.rmnHomeConfig[configDigest].SourceChainMinObservers, nil } func (r *RmnHomePoller) GetOffChainConfig(configDigest cciptypes.Bytes32) (cciptypes.Bytes, error) { r.mutex.RLock() defer r.mutex.RUnlock() + _, ok := r.rmnHomeState.rmnHomeConfig[configDigest] + if !ok { + if !ok { + return nil, fmt.Errorf("configDigest %s not found in RMNHomeConfig", configDigest) + } + } return r.rmnHomeState.rmnHomeConfig[configDigest].OffchainConfig, nil } @@ -226,6 +241,14 @@ func (r *RmnHomePoller) Name() string { return "RmnHomePoller" } +func validate(config rmntypes.VersionedConfigWithDigest) error { + // check if the versionesconfigwithdigests are set (can be empty) + if config.ConfigDigest.IsEmpty() { + return fmt.Errorf("configDigest is empty") + } + return nil +} + func convertOnChainConfigToRMNHomeChainConfig( lggr logger.Logger, versionedConfigWithDigests []rmntypes.VersionedConfigWithDigest, @@ -237,21 +260,23 @@ func convertOnChainConfigToRMNHomeChainConfig( rmnHomeConfigs := make(map[cciptypes.Bytes32]rmntypes.RMNHomeConfig) for _, versionedConfigWithDigest := range versionedConfigWithDigests { - // check if the versionesconfigwithdigests are set (can be empty) - if versionedConfigWithDigest.ConfigDigest == (cciptypes.Bytes32{}) { - lggr.Warnw("config digest is empty") + err := validate(versionedConfigWithDigest) + + if err != nil { + lggr.Warnw("invalid on chain RMNHomeConfig", "err", err) continue } + config := versionedConfigWithDigest.VersionedConfig.Config nodes := make([]rmntypes.RMNHomeNodeInfo, len(config.Nodes)) for i, node := range config.Nodes { pubKey := ed25519.PublicKey(node.OffchainPublicKey[:]) nodes[i] = rmntypes.RMNHomeNodeInfo{ - ID: rmntypes.NodeID(i), - PeerID: node.PeerID, - SignObservationsPublicKey: &pubKey, - SupportedSourceChains: mapset.NewSet[cciptypes.ChainSelector](), + ID: rmntypes.NodeID(i), + PeerID: node.PeerID, + OffchainPublicKey: &pubKey, + SupportedSourceChains: mapset.NewSet[cciptypes.ChainSelector](), } } @@ -272,10 +297,10 @@ func convertOnChainConfigToRMNHomeChainConfig( } rmnHomeConfigs[versionedConfigWithDigest.ConfigDigest] = rmntypes.RMNHomeConfig{ - Nodes: nodes, - MinObservers: minObservers, - ConfigDigest: versionedConfigWithDigest.ConfigDigest, - OffchainConfig: config.OffchainConfig, + Nodes: nodes, + SourceChainMinObservers: minObservers, + ConfigDigest: versionedConfigWithDigest.ConfigDigest, + OffchainConfig: config.OffchainConfig, } } return rmnHomeConfigs diff --git a/internal/reader/rmn_home_test.go b/internal/reader/rmn_home_test.go index 1263b3bd..ee82006c 100644 --- a/internal/reader/rmn_home_test.go +++ b/internal/reader/rmn_home_test.go @@ -27,6 +27,23 @@ var ( } ) +// test the ready method +func TestRMNHomeChainConfigPoller_Ready(t *testing.T) { + homeChainReader := readermock.NewMockContractReaderFacade(t) + configPoller := NewRMNHomePoller( + homeChainReader, + rmnHomeBoundContract, + logger.Test(t), + 1*time.Millisecond, + ) + // Initially it's not ready + require.Error(t, configPoller.Ready()) + + require.NoError(t, configPoller.Start(context.Background())) + // After starting it's ready + require.NoError(t, configPoller.Ready()) +} + func TestRMNHomeChainConfigPoller_HealthReport(t *testing.T) { homeChainReader := readermock.NewMockContractReaderFacade(t) homeChainReader.On( From bd0196cfb984cf42970d57872f14e29bacc55e58 Mon Sep 17 00:00:00 2001 From: nogo <0xnogo@gmail.com> Date: Thu, 19 Sep 2024 15:28:08 +0400 Subject: [PATCH 05/16] update tests --- internal/reader/rmn_home.go | 11 +++-------- internal/reader/rmn_home_test.go | 23 ++++++++++++++++++----- 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/internal/reader/rmn_home.go b/internal/reader/rmn_home.go index 59c9c4a4..2a459014 100644 --- a/internal/reader/rmn_home.go +++ b/internal/reader/rmn_home.go @@ -32,7 +32,7 @@ type RMNHome interface { // GetRMNNodesInfo gets the RMNHomeNodeInfo for the given configDigest GetRMNNodesInfo(configDigest cciptypes.Bytes32) ([]rmntypes.RMNHomeNodeInfo, error) // IsRMNHomeConfigDigestSet checks if the configDigest is set in the RMNHome contract - IsRMNHomeConfigDigestSet(configDigest cciptypes.Bytes32) (bool, error) + IsRMNHomeConfigDigestSet(configDigest cciptypes.Bytes32) bool // GetMinObservers gets the minimum number of observers required for each chain in the given configDigest GetMinObservers(configDigest cciptypes.Bytes32) (map[cciptypes.ChainSelector]uint64, error) // GetOffChainConfig gets the offchain config for the given configDigest @@ -178,16 +178,11 @@ func (r *RmnHomePoller) GetRMNNodesInfo(configDigest cciptypes.Bytes32) ([]rmnty return r.rmnHomeState.rmnHomeConfig[configDigest].Nodes, nil } -func (r *RmnHomePoller) IsRMNHomeConfigDigestSet(configDigest cciptypes.Bytes32) (bool, error) { +func (r *RmnHomePoller) IsRMNHomeConfigDigestSet(configDigest cciptypes.Bytes32) bool { r.mutex.RLock() defer r.mutex.RUnlock() _, ok := r.rmnHomeState.rmnHomeConfig[configDigest] - if !ok { - if !ok { - return false, fmt.Errorf("configDigest %s not found in RMNHomeConfig", configDigest) - } - } - return true, nil + return ok } func (r *RmnHomePoller) GetMinObservers(configDigest cciptypes.Bytes32) (map[cciptypes.ChainSelector]uint64, error) { diff --git a/internal/reader/rmn_home_test.go b/internal/reader/rmn_home_test.go index ee82006c..185d322f 100644 --- a/internal/reader/rmn_home_test.go +++ b/internal/reader/rmn_home_test.go @@ -36,12 +36,23 @@ func TestRMNHomeChainConfigPoller_Ready(t *testing.T) { logger.Test(t), 1*time.Millisecond, ) + // Return any result as we are testing the ready method + homeChainReader.On( + "GetLatestValue", + mock.Anything, + mock.Anything, + mock.Anything, + mock.Anything, + mock.Anything).Return(fmt.Errorf("error")) + // Initially it's not ready require.Error(t, configPoller.Ready()) require.NoError(t, configPoller.Start(context.Background())) // After starting it's ready require.NoError(t, configPoller.Ready()) + + require.NoError(t, configPoller.Close()) } func TestRMNHomeChainConfigPoller_HealthReport(t *testing.T) { @@ -160,15 +171,15 @@ func Test_RMNHomePollingWorking(t *testing.T) { isEmpty := (i == 0 && tt.primaryEmpty) || (i == 1 && tt.secondaryEmpty) rmnNodes, err := configPoller.GetRMNNodesInfo(config.ConfigDigest) - require.NoError(t, err) if isEmpty { + require.Error(t, err) require.Empty(t, rmnNodes) } else { + require.NoError(t, err) require.NotEmpty(t, rmnNodes) } - isValid, err := configPoller.IsRMNHomeConfigDigestSet(config.ConfigDigest) - require.NoError(t, err) + isValid := configPoller.IsRMNHomeConfigDigestSet(config.ConfigDigest) if isEmpty { require.False(t, isValid) } else { @@ -176,18 +187,20 @@ func Test_RMNHomePollingWorking(t *testing.T) { } offchainConfig, err := configPoller.GetOffChainConfig(config.ConfigDigest) - require.NoError(t, err) if isEmpty { + require.Error(t, err) require.Empty(t, offchainConfig) } else { + require.NoError(t, err) require.NotEmpty(t, offchainConfig) } minObsMap, err := configPoller.GetMinObservers(config.ConfigDigest) - require.NoError(t, err) if isEmpty { + require.Error(t, err) require.Empty(t, minObsMap) } else { + require.NoError(t, err) require.Len(t, minObsMap, 1) expectedChainSelector := cciptypes.ChainSelector(uint64(i + 1)) minObs, exists := minObsMap[expectedChainSelector] From a83702ecb3a8f1b1776a4d6c8028cf2a37757012 Mon Sep 17 00:00:00 2001 From: nogo <0xnogo@gmail.com> Date: Thu, 19 Sep 2024 15:31:02 +0400 Subject: [PATCH 06/16] mockery --- mocks/internal_/reader/rmn_home.go | 20 +++++--------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/mocks/internal_/reader/rmn_home.go b/mocks/internal_/reader/rmn_home.go index f1f8aa28..9ab467cf 100644 --- a/mocks/internal_/reader/rmn_home.go +++ b/mocks/internal_/reader/rmn_home.go @@ -292,7 +292,7 @@ func (_c *MockRMNHome_HealthReport_Call) RunAndReturn(run func() map[string]erro } // IsRMNHomeConfigDigestSet provides a mock function with given fields: configDigest -func (_m *MockRMNHome) IsRMNHomeConfigDigestSet(configDigest ccipocr3.Bytes32) (bool, error) { +func (_m *MockRMNHome) IsRMNHomeConfigDigestSet(configDigest ccipocr3.Bytes32) bool { ret := _m.Called(configDigest) if len(ret) == 0 { @@ -300,23 +300,13 @@ func (_m *MockRMNHome) IsRMNHomeConfigDigestSet(configDigest ccipocr3.Bytes32) ( } var r0 bool - var r1 error - if rf, ok := ret.Get(0).(func(ccipocr3.Bytes32) (bool, error)); ok { - return rf(configDigest) - } if rf, ok := ret.Get(0).(func(ccipocr3.Bytes32) bool); ok { r0 = rf(configDigest) } else { r0 = ret.Get(0).(bool) } - if rf, ok := ret.Get(1).(func(ccipocr3.Bytes32) error); ok { - r1 = rf(configDigest) - } else { - r1 = ret.Error(1) - } - - return r0, r1 + return r0 } // MockRMNHome_IsRMNHomeConfigDigestSet_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsRMNHomeConfigDigestSet' @@ -337,12 +327,12 @@ func (_c *MockRMNHome_IsRMNHomeConfigDigestSet_Call) Run(run func(configDigest c return _c } -func (_c *MockRMNHome_IsRMNHomeConfigDigestSet_Call) Return(_a0 bool, _a1 error) *MockRMNHome_IsRMNHomeConfigDigestSet_Call { - _c.Call.Return(_a0, _a1) +func (_c *MockRMNHome_IsRMNHomeConfigDigestSet_Call) Return(_a0 bool) *MockRMNHome_IsRMNHomeConfigDigestSet_Call { + _c.Call.Return(_a0) return _c } -func (_c *MockRMNHome_IsRMNHomeConfigDigestSet_Call) RunAndReturn(run func(ccipocr3.Bytes32) (bool, error)) *MockRMNHome_IsRMNHomeConfigDigestSet_Call { +func (_c *MockRMNHome_IsRMNHomeConfigDigestSet_Call) RunAndReturn(run func(ccipocr3.Bytes32) bool) *MockRMNHome_IsRMNHomeConfigDigestSet_Call { _c.Call.Return(run) return _c } From 3a1233473368cd1fce6b78dc2313533794c7df14 Mon Sep 17 00:00:00 2001 From: nogo <0xnogo@gmail.com> Date: Thu, 19 Sep 2024 16:15:11 +0400 Subject: [PATCH 07/16] add rmn home reader as param --- commit/factory.go | 64 ++++++++++++++++++++++++++-------- commit/merkleroot/processor.go | 43 ++++++++++++----------- commit/plugin.go | 4 +++ 3 files changed, 77 insertions(+), 34 deletions(-) diff --git a/commit/factory.go b/commit/factory.go index 0c0ce8f8..b4f5b035 100644 --- a/commit/factory.go +++ b/commit/factory.go @@ -2,8 +2,11 @@ package commit import ( "context" + "crypto/rand" + "encoding/hex" "errors" "fmt" + "time" "google.golang.org/grpc" @@ -55,13 +58,14 @@ func (p PluginFactoryConstructor) NewValidationService(ctx context.Context) (cor // PluginFactory implements common ReportingPluginFactory and is used for (re-)initializing commit plugin instances. type PluginFactory struct { - lggr logger.Logger - ocrConfig reader.OCR3ConfigWithMeta - commitCodec cciptypes.CommitPluginCodec - msgHasher cciptypes.MessageHasher - homeChainReader reader.HomeChain - contractReaders map[cciptypes.ChainSelector]types.ContractReader - chainWriters map[cciptypes.ChainSelector]types.ChainWriter + lggr logger.Logger + ocrConfig reader.OCR3ConfigWithMeta + commitCodec cciptypes.CommitPluginCodec + msgHasher cciptypes.MessageHasher + homeChainReader reader.HomeChain + homeChainSelector cciptypes.ChainSelector + contractReaders map[cciptypes.ChainSelector]types.ContractReader + chainWriters map[cciptypes.ChainSelector]types.ChainWriter } func NewPluginFactory( @@ -70,17 +74,19 @@ func NewPluginFactory( commitCodec cciptypes.CommitPluginCodec, msgHasher cciptypes.MessageHasher, homeChainReader reader.HomeChain, + homeChainSelector cciptypes.ChainSelector, contractReaders map[cciptypes.ChainSelector]types.ContractReader, chainWriters map[cciptypes.ChainSelector]types.ChainWriter, ) *PluginFactory { return &PluginFactory{ - lggr: lggr, - ocrConfig: ocrConfig, - commitCodec: commitCodec, - msgHasher: msgHasher, - homeChainReader: homeChainReader, - contractReaders: contractReaders, - chainWriters: chainWriters, + lggr: lggr, + ocrConfig: ocrConfig, + commitCodec: commitCodec, + msgHasher: msgHasher, + homeChainReader: homeChainReader, + homeChainSelector: homeChainSelector, + contractReaders: contractReaders, + chainWriters: chainWriters, } } @@ -100,6 +106,35 @@ func (p *PluginFactory) NewReportingPlugin(config ocr3types.ReportingPluginConfi oracleIDToP2PID[commontypes.OracleID(oracleID)] = p2pID } + // Bind the RMNHome contract + // TODO: replace when RMNHome has been added in the OCR3Config + // rmnHomeAddress := p.ocrConfig.Config.RmnHomeAddress + rmnHomeAddress := make([]byte, 20) + _, err = rand.Read(rmnHomeAddress) + if err != nil { + return nil, ocr3types.ReportingPluginInfo{}, fmt.Errorf("failed to generate random address: %w", err) + } + rmnCr, ok := p.contractReaders[p.homeChainSelector] + if !ok { + return nil, + ocr3types.ReportingPluginInfo{}, + fmt.Errorf("failed to find contract reader for home chain %d", p.homeChainSelector) + } + rmnHomeBoundContract := types.BoundContract{ + Address: "0x" + hex.EncodeToString(rmnHomeAddress), // TODO: replace with actual address + Name: consts.ContractNameRMNHome, + } + + if err1 := rmnCr.Bind(context.Background(), []types.BoundContract{rmnHomeBoundContract}); err1 != nil { + return nil, ocr3types.ReportingPluginInfo{}, fmt.Errorf("failed to bind RMN contract: %w", err1) + } + rmnHomeReader := reader.NewRMNHomePoller( + rmnCr, + rmnHomeBoundContract, + p.lggr, + 100*time.Millisecond, + ) + var onChainTokenPricesReader reader.PriceReader // The node supports the chain that the token prices are on. tokenPricesCr, ok := p.contractReaders[offchainConfig.PriceFeedChainSelector] @@ -150,6 +185,7 @@ func (p *PluginFactory) NewReportingPlugin(config ocr3types.ReportingPluginConfi p.msgHasher, p.lggr, p.homeChainReader, + rmnHomeReader, config, rmn.Config{}, // todo ), ocr3types.ReportingPluginInfo{ diff --git a/commit/merkleroot/processor.go b/commit/merkleroot/processor.go index e7bd6dbb..40de3486 100644 --- a/commit/merkleroot/processor.go +++ b/commit/merkleroot/processor.go @@ -19,16 +19,17 @@ import ( // It's setup to use RMN to query which messages to include in the merkle root and ensures // the newly built merkle roots are the same as RMN roots. type Processor struct { - oracleID commontypes.OracleID - cfg pluginconfig.CommitPluginConfig - lggr logger.Logger - observer Observer - ccipReader readerpkg.CCIPReader - reportingCfg ocr3types.ReportingPluginConfig - chainSupport plugincommon.ChainSupport - rmnClient rmn.Client - rmnCrypto cciptypes.RMNCrypto - rmnConfig rmn.Config + oracleID commontypes.OracleID + cfg pluginconfig.CommitPluginConfig + lggr logger.Logger + observer Observer + ccipReader readerpkg.CCIPReader + reportingCfg ocr3types.ReportingPluginConfig + chainSupport plugincommon.ChainSupport + rmnClient rmn.Client + rmnCrypto cciptypes.RMNCrypto + rmnConfig rmn.Config + rmnHomeReader reader.RMNHome } // NewProcessor creates a new Processor @@ -44,6 +45,7 @@ func NewProcessor( rmnClient rmn.Client, rmnCrypto cciptypes.RMNCrypto, rmnConfig rmn.Config, + rmnHomeReader reader.RMNHome, ) *Processor { observer := ObserverImpl{ lggr, @@ -54,16 +56,17 @@ func NewProcessor( msgHasher, } return &Processor{ - oracleID: oracleID, - cfg: cfg, - lggr: lggr, - observer: observer, - ccipReader: ccipReader, - reportingCfg: reportingCfg, - chainSupport: chainSupport, - rmnClient: rmnClient, - rmnCrypto: rmnCrypto, - rmnConfig: rmnConfig, + oracleID: oracleID, + cfg: cfg, + lggr: lggr, + observer: observer, + ccipReader: ccipReader, + reportingCfg: reportingCfg, + chainSupport: chainSupport, + rmnClient: rmnClient, + rmnCrypto: rmnCrypto, + rmnConfig: rmnConfig, + rmnHomeReader: rmnHomeReader, } } diff --git a/commit/plugin.go b/commit/plugin.go index 2e46288d..c3bb171c 100644 --- a/commit/plugin.go +++ b/commit/plugin.go @@ -37,6 +37,7 @@ type Plugin struct { reportCodec cciptypes.CommitPluginCodec lggr logger.Logger homeChain reader.HomeChain + rmnHomeReader reader.RMNHome reportingCfg ocr3types.ReportingPluginConfig chainSupport plugincommon.ChainSupport merkleRootProcessor plugincommon.PluginProcessor[merkleroot.Query, merkleroot.Observation, merkleroot.Outcome] @@ -56,6 +57,7 @@ func NewPlugin( msgHasher cciptypes.MessageHasher, lggr logger.Logger, homeChain reader.HomeChain, + rmnHomeReader reader.RMNHome, reportingCfg ocr3types.ReportingPluginConfig, rmnConfig rmn.Config, ) *Plugin { @@ -94,6 +96,7 @@ func NewPlugin( rmn.Client(nil), // todo cciptypes.RMNCrypto(nil), // todo rmnConfig, + rmnHomeReader, ) tokenPriceProcessor := tokenprice.NewProcessor( nodeID, @@ -113,6 +116,7 @@ func NewPlugin( tokenPricesReader: tokenPricesReader, ccipReader: ccipReader, homeChain: homeChain, + rmnHomeReader: rmnHomeReader, readerSyncer: readerSyncer, reportCodec: reportCodec, reportingCfg: reportingCfg, From 475bde8469b74e147fa0ae88741be148cb12a37b Mon Sep 17 00:00:00 2001 From: nogo <0xnogo@gmail.com> Date: Thu, 19 Sep 2024 16:22:07 +0400 Subject: [PATCH 08/16] addressed comment 2 --- internal/reader/rmn_home.go | 30 ++++++++++++++---------------- internal/reader/rmn_home_test.go | 1 - 2 files changed, 14 insertions(+), 17 deletions(-) diff --git a/internal/reader/rmn_home.go b/internal/reader/rmn_home.go index 2a459014..58c4797b 100644 --- a/internal/reader/rmn_home.go +++ b/internal/reader/rmn_home.go @@ -47,7 +47,7 @@ type rmnHomeState struct { } // RmnHomePoller polls the RMNHome contract for the latest RMNHomeConfigs -// It is running in the backdoung with a polling interval of pollingDuration +// It is running in the background with a polling interval of pollingDuration type RmnHomePoller struct { wg sync.WaitGroup stopCh services.StopChan @@ -131,18 +131,21 @@ func (r *RmnHomePoller) fetchAndSetRmnHomeConfigs(ctx context.Context) error { } if len(versionedConfigWithDigests) != expectedRMNHomeConfigs { - r.lggr.Warnw("expected 2 RMNHomeConfigs, got", "count", len(versionedConfigWithDigests)) - return fmt.Errorf("expected 2 RMNHomeConfigs, got %d", len(versionedConfigWithDigests)) + r.lggr.Warnw( + "unexpected number of RMNHomeConfigs", + "numConfigs", len(versionedConfigWithDigests), + "expected", expectedRMNHomeConfigs) + return fmt.Errorf("unexpected number of RMNHomeConfigs") } primaryConfigDigest := versionedConfigWithDigests[0].ConfigDigest if primaryConfigDigest.IsEmpty() { - r.lggr.Warnw("primary config digest is empty") + r.lggr.Debugw("primary config digest is empty") } secondaryConfigDigest := versionedConfigWithDigests[1].ConfigDigest if secondaryConfigDigest.IsEmpty() { - r.lggr.Warnw("secondary config digest is empty") + r.lggr.Debugw("secondary config digest is empty") } r.setRMNHomeState( @@ -171,9 +174,8 @@ func (r *RmnHomePoller) GetRMNNodesInfo(configDigest cciptypes.Bytes32) ([]rmnty defer r.mutex.RUnlock() _, ok := r.rmnHomeState.rmnHomeConfig[configDigest] if !ok { - if !ok { - return nil, fmt.Errorf("configDigest %s not found in RMNHomeConfig", configDigest) - } + return nil, fmt.Errorf("configDigest %s not found in RMNHomeConfig", configDigest) + } return r.rmnHomeState.rmnHomeConfig[configDigest].Nodes, nil } @@ -190,9 +192,7 @@ func (r *RmnHomePoller) GetMinObservers(configDigest cciptypes.Bytes32) (map[cci defer r.mutex.RUnlock() _, ok := r.rmnHomeState.rmnHomeConfig[configDigest] if !ok { - if !ok { - return nil, fmt.Errorf("configDigest %s not found in RMNHomeConfig", configDigest) - } + return nil, fmt.Errorf("configDigest %s not found in RMNHomeConfig", configDigest) } return r.rmnHomeState.rmnHomeConfig[configDigest].SourceChainMinObservers, nil } @@ -200,13 +200,11 @@ func (r *RmnHomePoller) GetMinObservers(configDigest cciptypes.Bytes32) (map[cci func (r *RmnHomePoller) GetOffChainConfig(configDigest cciptypes.Bytes32) (cciptypes.Bytes, error) { r.mutex.RLock() defer r.mutex.RUnlock() - _, ok := r.rmnHomeState.rmnHomeConfig[configDigest] + cfg, ok := r.rmnHomeState.rmnHomeConfig[configDigest] if !ok { - if !ok { - return nil, fmt.Errorf("configDigest %s not found in RMNHomeConfig", configDigest) - } + return nil, fmt.Errorf("configDigest %s not found in RMNHomeConfig", configDigest) } - return r.rmnHomeState.rmnHomeConfig[configDigest].OffchainConfig, nil + return cfg.OffchainConfig, nil } func (r *RmnHomePoller) Close() error { diff --git a/internal/reader/rmn_home_test.go b/internal/reader/rmn_home_test.go index 185d322f..9d460c73 100644 --- a/internal/reader/rmn_home_test.go +++ b/internal/reader/rmn_home_test.go @@ -27,7 +27,6 @@ var ( } ) -// test the ready method func TestRMNHomeChainConfigPoller_Ready(t *testing.T) { homeChainReader := readermock.NewMockContractReaderFacade(t) configPoller := NewRMNHomePoller( From ae92b121090dc8b7dbba9c713469c43913750bc6 Mon Sep 17 00:00:00 2001 From: nogo <0xnogo@gmail.com> Date: Thu, 19 Sep 2024 17:13:28 +0400 Subject: [PATCH 09/16] update e2e --- commit/plugin_e2e_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/commit/plugin_e2e_test.go b/commit/plugin_e2e_test.go index 58379c6b..4d80eed7 100644 --- a/commit/plugin_e2e_test.go +++ b/commit/plugin_e2e_test.go @@ -260,6 +260,7 @@ func setupNode( reportCodec := mocks.NewCommitPluginJSONReportCodec() msgHasher := mocks.NewMessageHasher() homeChainReader := reader_mock.NewMockHomeChain(t) + rmnHomeReader := reader_mock.NewMockRMNHome(t) fChain := map[ccipocr3.ChainSelector]int{} supportedChainsForPeer := make(map[libocrtypes.PeerID]mapset.Set[ccipocr3.ChainSelector]) @@ -347,6 +348,7 @@ func setupNode( msgHasher, lggr, homeChainReader, + rmnHomeReader, reportingCfg, rmn.Config{}, ) From c484fa813df4bbfaf1cc6f9fd0f9798be964085e Mon Sep 17 00:00:00 2001 From: nogo <0xnogo@gmail.com> Date: Wed, 25 Sep 2024 12:46:22 +0400 Subject: [PATCH 10/16] change error log --- commit/factory.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commit/factory.go b/commit/factory.go index b4f5b035..1b9d2fd4 100644 --- a/commit/factory.go +++ b/commit/factory.go @@ -126,7 +126,7 @@ func (p *PluginFactory) NewReportingPlugin(config ocr3types.ReportingPluginConfi } if err1 := rmnCr.Bind(context.Background(), []types.BoundContract{rmnHomeBoundContract}); err1 != nil { - return nil, ocr3types.ReportingPluginInfo{}, fmt.Errorf("failed to bind RMN contract: %w", err1) + return nil, ocr3types.ReportingPluginInfo{}, fmt.Errorf("failed to bind RMNHome contract: %w", err1) } rmnHomeReader := reader.NewRMNHomePoller( rmnCr, From 2bcda39b2685a15eddeaf77728fb41d9796ea22c Mon Sep 17 00:00:00 2001 From: nogo <0xnogo@gmail.com> Date: Wed, 25 Sep 2024 12:46:42 +0400 Subject: [PATCH 11/16] add more logs --- commit/factory.go | 1 + 1 file changed, 1 insertion(+) diff --git a/commit/factory.go b/commit/factory.go index 1b9d2fd4..be42d0f3 100644 --- a/commit/factory.go +++ b/commit/factory.go @@ -114,6 +114,7 @@ func (p *PluginFactory) NewReportingPlugin(config ocr3types.ReportingPluginConfi if err != nil { return nil, ocr3types.ReportingPluginInfo{}, fmt.Errorf("failed to generate random address: %w", err) } + fmt.Println("home chain selector", p.homeChainSelector.String()) rmnCr, ok := p.contractReaders[p.homeChainSelector] if !ok { return nil, From fb9458a871379a433029e70f889b4497cc76fcf8 Mon Sep 17 00:00:00 2001 From: nogo <0xnogo@gmail.com> Date: Wed, 25 Sep 2024 17:23:23 +0400 Subject: [PATCH 12/16] Refactor RMNHome contract binding and reader initialization --- commit/factory.go | 51 +++++++++++++++++------------------ internal/reader/home_chain.go | 1 + 2 files changed, 25 insertions(+), 27 deletions(-) diff --git a/commit/factory.go b/commit/factory.go index be42d0f3..189cf341 100644 --- a/commit/factory.go +++ b/commit/factory.go @@ -2,7 +2,6 @@ package commit import ( "context" - "crypto/rand" "encoding/hex" "errors" "fmt" @@ -30,6 +29,7 @@ import ( const maxReportTransmissionCheckAttempts = 5 const maxQueryLength = 1024 * 1024 // 1MB +const rmnEnabled = false // PluginFactoryConstructor implements common OCR3ReportingPluginClient and is used for initializing a plugin factory // and a validation service. @@ -107,34 +107,30 @@ func (p *PluginFactory) NewReportingPlugin(config ocr3types.ReportingPluginConfi } // Bind the RMNHome contract - // TODO: replace when RMNHome has been added in the OCR3Config - // rmnHomeAddress := p.ocrConfig.Config.RmnHomeAddress - rmnHomeAddress := make([]byte, 20) - _, err = rand.Read(rmnHomeAddress) - if err != nil { - return nil, ocr3types.ReportingPluginInfo{}, fmt.Errorf("failed to generate random address: %w", err) - } - fmt.Println("home chain selector", p.homeChainSelector.String()) - rmnCr, ok := p.contractReaders[p.homeChainSelector] - if !ok { - return nil, - ocr3types.ReportingPluginInfo{}, - fmt.Errorf("failed to find contract reader for home chain %d", p.homeChainSelector) - } - rmnHomeBoundContract := types.BoundContract{ - Address: "0x" + hex.EncodeToString(rmnHomeAddress), // TODO: replace with actual address - Name: consts.ContractNameRMNHome, - } + rmnHomeReader := reader.RMNHome(nil) + if rmnEnabled { + rmnHomeAddress := p.ocrConfig.Config.RmnHomeAddress + rmnCr, ok := p.contractReaders[p.homeChainSelector] + if !ok { + return nil, + ocr3types.ReportingPluginInfo{}, + fmt.Errorf("failed to find contract reader for home chain %d", p.homeChainSelector) + } + rmnHomeBoundContract := types.BoundContract{ + Address: "0x" + hex.EncodeToString(rmnHomeAddress), + Name: consts.ContractNameRMNHome, + } - if err1 := rmnCr.Bind(context.Background(), []types.BoundContract{rmnHomeBoundContract}); err1 != nil { - return nil, ocr3types.ReportingPluginInfo{}, fmt.Errorf("failed to bind RMNHome contract: %w", err1) + if err1 := rmnCr.Bind(context.Background(), []types.BoundContract{rmnHomeBoundContract}); err1 != nil { + return nil, ocr3types.ReportingPluginInfo{}, fmt.Errorf("failed to bind RMNHome contract: %w", err1) + } + rmnHomeReader = reader.NewRMNHomePoller( + rmnCr, + rmnHomeBoundContract, + p.lggr, + 100*time.Millisecond, + ) } - rmnHomeReader := reader.NewRMNHomePoller( - rmnCr, - rmnHomeBoundContract, - p.lggr, - 100*time.Millisecond, - ) var onChainTokenPricesReader reader.PriceReader // The node supports the chain that the token prices are on. @@ -179,6 +175,7 @@ func (p *PluginFactory) NewReportingPlugin(config ocr3types.ReportingPluginConfi NewMsgScanBatchSize: merklemulti.MaxNumberTreeLeaves, MaxReportTransmissionCheckAttempts: maxReportTransmissionCheckAttempts, OffchainConfig: offchainConfig, + RMNEnabled: rmnEnabled, }, ccipReader, onChainTokenPricesReader, diff --git a/internal/reader/home_chain.go b/internal/reader/home_chain.go index 8f987777..ba4f47af 100644 --- a/internal/reader/home_chain.go +++ b/internal/reader/home_chain.go @@ -358,6 +358,7 @@ type OCR3Config struct { F uint8 `json:"F"` OffchainConfigVersion uint64 `json:"offchainConfigVersion"` OfframpAddress []byte `json:"offrampAddress"` + RmnHomeAddress []byte `json:"rmnHome"` P2PIds [][32]byte `json:"p2pIds"` Signers [][]byte `json:"signers"` Transmitters [][]byte `json:"transmitters"` From 27eedb719ed12e8510dd4c65ef3ba58312ff2462 Mon Sep 17 00:00:00 2001 From: nogo <0xnogo@gmail.com> Date: Wed, 25 Sep 2024 17:27:15 +0400 Subject: [PATCH 13/16] change the json reader for RMNHomeAddress --- internal/reader/home_chain.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/reader/home_chain.go b/internal/reader/home_chain.go index ba4f47af..dc639846 100644 --- a/internal/reader/home_chain.go +++ b/internal/reader/home_chain.go @@ -358,7 +358,7 @@ type OCR3Config struct { F uint8 `json:"F"` OffchainConfigVersion uint64 `json:"offchainConfigVersion"` OfframpAddress []byte `json:"offrampAddress"` - RmnHomeAddress []byte `json:"rmnHome"` + RmnHomeAddress []byte `json:"rmnHomeAddress"` P2PIds [][32]byte `json:"p2pIds"` Signers [][]byte `json:"signers"` Transmitters [][]byte `json:"transmitters"` From 2c073c261357f33ee51bec31c57941a0ffe62d01 Mon Sep 17 00:00:00 2001 From: nogo <0xnogo@gmail.com> Date: Wed, 25 Sep 2024 21:12:26 +0400 Subject: [PATCH 14/16] add don id --- commit/factory.go | 1 + 1 file changed, 1 insertion(+) diff --git a/commit/factory.go b/commit/factory.go index 6f26fc03..857928d4 100644 --- a/commit/factory.go +++ b/commit/factory.go @@ -83,6 +83,7 @@ func NewPluginFactory( ) *PluginFactory { return &PluginFactory{ lggr: lggr, + donID: donID, ocrConfig: ocrConfig, commitCodec: commitCodec, msgHasher: msgHasher, From 42abcf7a5253587074ee19f2827f3e56185ed3f6 Mon Sep 17 00:00:00 2001 From: nogo <0xnogo@gmail.com> Date: Wed, 25 Sep 2024 21:24:17 +0400 Subject: [PATCH 15/16] Refactor commit/factory.go: Update variable declaration for rmnHomeReader --- commit/factory.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commit/factory.go b/commit/factory.go index 857928d4..b4feabb2 100644 --- a/commit/factory.go +++ b/commit/factory.go @@ -111,7 +111,7 @@ func (p *PluginFactory) NewReportingPlugin(config ocr3types.ReportingPluginConfi } // Bind the RMNHome contract - rmnHomeReader := reader.RMNHome(nil) + var rmnHomeReader reader.RMNHome if rmnEnabled { rmnHomeAddress := p.ocrConfig.Config.RmnHomeAddress rmnCr, ok := p.contractReaders[p.homeChainSelector] From aa80e412966ba97bbc440173f476c2b2105c9ba2 Mon Sep 17 00:00:00 2001 From: nogo <0xnogo@gmail.com> Date: Thu, 26 Sep 2024 00:23:13 +0400 Subject: [PATCH 16/16] bump --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 0c4aaac6..176e75b7 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.22.5 require ( github.com/deckarep/golang-set/v2 v2.6.0 github.com/smartcontractkit/chain-selectors v1.0.23 - github.com/smartcontractkit/chainlink-common v0.2.3-0.20240919092417-53e784c2e420 + github.com/smartcontractkit/chainlink-common v0.2.3-0.20240925085218-aded1b263ecc github.com/smartcontractkit/libocr v0.0.0-20240717100443-f6226e09bee7 github.com/stretchr/testify v1.9.0 go.uber.org/zap v1.27.0 diff --git a/go.sum b/go.sum index 48aa0a5c..c67c9891 100644 --- a/go.sum +++ b/go.sum @@ -59,8 +59,8 @@ github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/smartcontractkit/chain-selectors v1.0.23 h1:D2Eaex4Cw/O7Lg3tX6WklOqnjjIQAEBnutCtksPzVDY= github.com/smartcontractkit/chain-selectors v1.0.23/go.mod h1:d4Hi+E1zqjy9HqMkjBE5q1vcG9VGgxf5VxiRHfzi2kE= -github.com/smartcontractkit/chainlink-common v0.2.3-0.20240919092417-53e784c2e420 h1:+xNnYYgkxzKUIkLCOfzfAKUxeLLtuxlalDI70kNJ8No= -github.com/smartcontractkit/chainlink-common v0.2.3-0.20240919092417-53e784c2e420/go.mod h1:zm+l8gN4LQS1+YvwQDhRz/njirVeWGNiDJKIhCGwaoQ= +github.com/smartcontractkit/chainlink-common v0.2.3-0.20240925085218-aded1b263ecc h1:ALbyaoRzUSXQ2NhGFKVOyJqO22IB5yQjhjKWbIZGbrI= +github.com/smartcontractkit/chainlink-common v0.2.3-0.20240925085218-aded1b263ecc/go.mod h1:F6WUS6N4mP5ScwpwyTyAJc9/vjR+GXbMCRUOVekQi1g= github.com/smartcontractkit/libocr v0.0.0-20240717100443-f6226e09bee7 h1:e38V5FYE7DA1JfKXeD5Buo/7lczALuVXlJ8YNTAUxcw= github.com/smartcontractkit/libocr v0.0.0-20240717100443-f6226e09bee7/go.mod h1:fb1ZDVXACvu4frX3APHZaEBp0xi1DIm34DcA0CwTsZM= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=