From 48f64d3e45acb395d836c1480b8526b5283d151c Mon Sep 17 00:00:00 2001 From: nogo <0xnogo@gmail.com> Date: Tue, 4 Feb 2025 18:54:04 +0400 Subject: [PATCH 01/18] use cache --- pkg/reader/ccip.go | 688 ++++++++++++++++++++++++++++++++------------- 1 file changed, 491 insertions(+), 197 deletions(-) diff --git a/pkg/reader/ccip.go b/pkg/reader/ccip.go index d7e4172b8..a48aea40b 100644 --- a/pkg/reader/ccip.go +++ b/pkg/reader/ccip.go @@ -7,7 +7,6 @@ import ( "errors" "fmt" "math/big" - "sort" "strconv" "sync" "time" @@ -34,6 +33,74 @@ import ( plugintypes2 "github.com/smartcontractkit/chainlink-ccip/plugintypes" ) +// Default refresh period for cache if not specified +const defaultRefreshPeriod = 30 * time.Second + +// ChainConfigSnapshot represents the complete configuration state of the chain +type ChainConfigSnapshot struct { + Offramp OfframpConfig + RMNProxy RMNProxyConfig + RMNRemote RMNRemoteConfig + FeeQuoter FeeQuoterConfig +} + +type FeeQuoterConfig struct { + StaticConfig feeQuoterStaticConfig +} + +type RMNRemoteConfig struct { + DigestHeader rmnDigestHeader + VersionedConfig versionedConfig +} + +type OfframpConfig struct { + CommitLatestOCRConfig OCRConfigResponse + ExecLatestOCRConfig OCRConfigResponse + StaticConfig offRampStaticChainConfig + DynamicConfig offRampDynamicChainConfig + SelectorsAndConf selectorsAndConfigs +} + +type RMNProxyConfig struct { + RemoteAddress []byte +} + +// chainCache represents the cache for a single chain +type chainCache struct { + sync.RWMutex + data ChainConfigSnapshot + lastRefresh time.Time +} + +// configCache handles caching of chain configuration data for multiple chains +type configCache struct { + sync.RWMutex + chainCaches map[cciptypes.ChainSelector]*chainCache + refreshPeriod time.Duration +} + +// newConfigCache creates a new multi-chain config cache +func newConfigCache(refreshPeriod time.Duration) *configCache { + return &configCache{ + chainCaches: make(map[cciptypes.ChainSelector]*chainCache), + refreshPeriod: refreshPeriod, + } +} + +// getOrCreateChainCache safely gets or creates a cache for a specific chain +func (c *configCache) getOrCreateChainCache(chainSel cciptypes.ChainSelector) *chainCache { + c.Lock() + defer c.Unlock() + + if cache, exists := c.chainCaches[chainSel]; exists { + return cache + } + + cache := &chainCache{} + c.chainCaches[chainSel] = cache + return cache +} + // TODO: unit test the implementation when the actual contract reader and writer interfaces are finalized and mocks // can be generated. type ccipChainReader struct { @@ -43,6 +110,21 @@ type ccipChainReader struct { destChain cciptypes.ChainSelector offrampAddress string extraDataCodec cciptypes.ExtraDataCodec + cache *configCache +} + +// NewCCIPChainReaderWithCache creates a new CCIPReader with specified cache refresh period +func NewCCIPChainReaderWithCache( + ctx context.Context, + lggr logger.Logger, + contractReaders map[cciptypes.ChainSelector]contractreader.ContractReaderFacade, + contractWriters map[cciptypes.ChainSelector]types.ContractWriter, + destChain cciptypes.ChainSelector, + offrampAddress []byte, + extraDataCodec cciptypes.ExtraDataCodec, + refreshPeriod time.Duration, +) *ccipChainReader { + return newCCIPChainReaderInternal(ctx, lggr, contractReaders, contractWriters, destChain, offrampAddress, extraDataCodec, refreshPeriod) } func newCCIPChainReaderInternal( @@ -53,12 +135,19 @@ func newCCIPChainReaderInternal( destChain cciptypes.ChainSelector, offrampAddress []byte, extraDataCodec cciptypes.ExtraDataCodec, + refreshPeriod ...time.Duration, // Optional refresh period ) *ccipChainReader { var crs = make(map[cciptypes.ChainSelector]contractreader.Extended) for chainSelector, cr := range contractReaders { crs[chainSelector] = contractreader.NewExtendedContractReader(cr) } + // Use provided refresh period or default + period := defaultRefreshPeriod + if len(refreshPeriod) > 0 { + period = refreshPeriod[0] + } + reader := &ccipChainReader{ lggr: lggr, contractReaders: crs, @@ -66,6 +155,7 @@ func newCCIPChainReaderInternal( destChain: destChain, offrampAddress: typeconv.AddressBytesToString(offrampAddress, uint64(destChain)), extraDataCodec: extraDataCodec, + cache: newConfigCache(period), } contracts := ContractAddresses{ @@ -119,6 +209,344 @@ type CommitReportAcceptedEvent struct { PriceUpdates PriceUpdates } +type rmnDigestHeader struct { + DigestHeader cciptypes.Bytes32 +} + +type OCRConfigResponse struct { + OCRConfig OCRConfig +} + +type OCRConfig struct { + ConfigInfo ConfigInfo + Signers [][]byte + Transmitters [][]byte +} + +type ConfigInfo struct { + ConfigDigest [32]byte + F uint8 + N uint8 + IsSignatureVerificationEnabled bool +} + +// --------------------------------------------------- +// The following functions are used for the config cache + +// getChainConfig returns the cached chain configuration for a specific chain +func (r *ccipChainReader) getChainConfig(ctx context.Context, chainSel cciptypes.ChainSelector) (ChainConfigSnapshot, error) { + chainCache := r.cache.getOrCreateChainCache(chainSel) + + chainCache.RLock() + timeSinceLastRefresh := time.Since(chainCache.lastRefresh) + if timeSinceLastRefresh < r.cache.refreshPeriod { + defer chainCache.RUnlock() + r.lggr.Infow("Cache hit", + "chain", chainSel, + "timeSinceLastRefresh", timeSinceLastRefresh, + "refreshPeriod", r.cache.refreshPeriod) + return chainCache.data, nil + } + chainCache.RUnlock() + + return r.refreshChainCache(ctx, chainSel) +} + +func (r *ccipChainReader) refreshChainCache(ctx context.Context, chainSel cciptypes.ChainSelector) (ChainConfigSnapshot, error) { + chainCache := r.cache.getOrCreateChainCache(chainSel) + + chainCache.Lock() + defer chainCache.Unlock() + + timeSinceLastRefresh := time.Since(chainCache.lastRefresh) + if timeSinceLastRefresh < r.cache.refreshPeriod { + r.lggr.Infow("Cache was refreshed by another goroutine", + "chain", chainSel, + "timeSinceLastRefresh", timeSinceLastRefresh) + return chainCache.data, nil + } + + startTime := time.Now() + newData, err := r.fetchChainConfig(ctx, chainSel) + refreshDuration := time.Since(startTime) + + if err != nil { + if !chainCache.lastRefresh.IsZero() { + r.lggr.Warnw("Failed to refresh cache, using old data", + "chain", chainSel, + "error", err, + "lastRefresh", chainCache.lastRefresh, + "refreshDuration", refreshDuration) + return chainCache.data, nil + } + r.lggr.Errorw("Failed to refresh cache, no old data available", + "chain", chainSel, + "error", err, + "refreshDuration", refreshDuration) + return ChainConfigSnapshot{}, fmt.Errorf("failed to refresh cache for chain %d: %w", chainSel, err) + } + + chainCache.data = newData + chainCache.lastRefresh = time.Now() + + return newData, nil +} + +// prepareBatchRequests creates the batch request for all configurations +func (r *ccipChainReader) prepareBatchRequests() contractreader.ExtendedBatchGetLatestValuesRequest { + var ( + commitLatestOCRConfig OCRConfigResponse + execLatestOCRConfig OCRConfigResponse + staticConfig offRampStaticChainConfig + dynamicConfig offRampDynamicChainConfig + selectorsAndConf selectorsAndConfigs + rmnRemoteAddress []byte + rmnDigestHeader rmnDigestHeader + rmnVersionConfig versionedConfig + feeQuoterConfig feeQuoterStaticConfig + ) + + return contractreader.ExtendedBatchGetLatestValuesRequest{ + consts.ContractNameOffRamp: { + { + ReadName: consts.MethodNameOffRampLatestConfigDetails, + Params: map[string]any{ + "ocrPluginType": consts.PluginTypeCommit, + }, + ReturnVal: &commitLatestOCRConfig, + }, + { + ReadName: consts.MethodNameOffRampLatestConfigDetails, + Params: map[string]any{ + "ocrPluginType": consts.PluginTypeExecute, + }, + ReturnVal: &execLatestOCRConfig, + }, + { + ReadName: consts.MethodNameOffRampGetStaticConfig, + Params: map[string]any{}, + ReturnVal: &staticConfig, + }, + { + ReadName: consts.MethodNameOffRampGetDynamicConfig, + Params: map[string]any{}, + ReturnVal: &dynamicConfig, + }, + { + ReadName: consts.MethodNameOffRampGetAllSourceChainConfigs, + Params: map[string]any{}, + ReturnVal: &selectorsAndConf, + }, + }, + consts.ContractNameRMNProxy: {{ + ReadName: consts.MethodNameGetARM, + Params: map[string]any{}, + ReturnVal: &rmnRemoteAddress, + }}, + consts.ContractNameRMNRemote: { + { + ReadName: consts.MethodNameGetReportDigestHeader, + Params: map[string]any{}, + ReturnVal: &rmnDigestHeader, + }, + { + ReadName: consts.MethodNameGetVersionedConfig, + Params: map[string]any{}, + ReturnVal: &rmnVersionConfig, + }, + }, + consts.ContractNameFeeQuoter: {{ + ReadName: consts.MethodNameFeeQuoterGetStaticConfig, + Params: map[string]any{}, + ReturnVal: &feeQuoterConfig, + }}, + } +} + +// fetchChainConfig fetches the latest configuration for a specific chain +func (r *ccipChainReader) fetchChainConfig(ctx context.Context, chainSel cciptypes.ChainSelector) (ChainConfigSnapshot, error) { + reader, exists := r.contractReaders[chainSel] + if !exists { + return ChainConfigSnapshot{}, fmt.Errorf("no contract reader for chain %d", chainSel) + } + + requests := r.prepareBatchRequests() + batchResult, skipped, err := reader.ExtendedBatchGetLatestValues(ctx, requests, true) + if err != nil { + return ChainConfigSnapshot{}, fmt.Errorf("batch get latest values for chain %d: %w", chainSel, err) + } + + if len(skipped) > 0 { + r.lggr.Infow("some contracts were skipped due to no bindings", + "chain", chainSel, + "contracts", skipped) + } + + return r.updateFromResults(batchResult) +} + +func (r *ccipChainReader) updateFromResults(batchResult types.BatchGetLatestValuesResult) (ChainConfigSnapshot, error) { + config := ChainConfigSnapshot{} + + for contract, results := range batchResult { + var err error + switch contract.Name { + case consts.ContractNameOffRamp: + config.Offramp, err = r.processOfframpResults(results) + if err != nil { + return ChainConfigSnapshot{}, fmt.Errorf("process offramp results: %w", err) + } + + case consts.ContractNameRMNProxy: + config.RMNProxy, err = r.processRMNProxyResults(results) + if err != nil { + return ChainConfigSnapshot{}, fmt.Errorf("process RMN proxy results: %w", err) + } + + case consts.ContractNameRMNRemote: + config.RMNRemote, err = r.processRMNRemoteResults(results) + if err != nil { + return ChainConfigSnapshot{}, fmt.Errorf("process RMN remote results: %w", err) + } + + case consts.ContractNameFeeQuoter: + config.FeeQuoter, err = r.processFeeQuoterResults(results) + if err != nil { + return ChainConfigSnapshot{}, fmt.Errorf("process fee quoter results: %w", err) + } + + default: + r.lggr.Warnw("Unhandled contract in batch results", "contract", contract.Name) + } + } + + return config, nil +} + +func (r *ccipChainReader) processOfframpResults(results []types.BatchReadResult) (OfframpConfig, error) { + config := OfframpConfig{} + + if len(results) != 5 { + return OfframpConfig{}, fmt.Errorf("expected 5 offramp results, got %d", len(results)) + } + + for i, result := range results { + val, err := result.GetResult() + if err != nil { + return OfframpConfig{}, fmt.Errorf("get offramp result %d: %w", i, err) + } + + switch i { + case 0: // CommitLatestOCRConfig + if typed, ok := val.(*OCRConfigResponse); ok { + config.CommitLatestOCRConfig = *typed + } else { + return OfframpConfig{}, fmt.Errorf("invalid type for CommitLatestOCRConfig: %T", val) + } + + case 1: // ExecLatestOCRConfig + if typed, ok := val.(*OCRConfigResponse); ok { + config.ExecLatestOCRConfig = *typed + } else { + return OfframpConfig{}, fmt.Errorf("invalid type for ExecLatestOCRConfig: %T", val) + } + + case 2: // StaticConfig + if typed, ok := val.(*offRampStaticChainConfig); ok { + config.StaticConfig = *typed + } else { + return OfframpConfig{}, fmt.Errorf("invalid type for StaticConfig: %T", val) + } + + case 3: // DynamicConfig + if typed, ok := val.(*offRampDynamicChainConfig); ok { + config.DynamicConfig = *typed + } else { + return OfframpConfig{}, fmt.Errorf("invalid type for DynamicConfig: %T", val) + } + + case 4: // SelectorsAndConf + if typed, ok := val.(*selectorsAndConfigs); ok { + config.SelectorsAndConf = *typed + } else { + return OfframpConfig{}, fmt.Errorf("invalid type for SelectorsAndConf: %T", val) + } + } + } + + return config, nil +} + +func (r *ccipChainReader) processRMNProxyResults(results []types.BatchReadResult) (RMNProxyConfig, error) { + if len(results) != 1 { + return RMNProxyConfig{}, fmt.Errorf("expected 1 RMN proxy result, got %d", len(results)) + } + + val, err := results[0].GetResult() + if err != nil { + return RMNProxyConfig{}, fmt.Errorf("get RMN proxy result: %w", err) + } + + if bytes, ok := val.(*[]byte); ok { + return RMNProxyConfig{ + RemoteAddress: *bytes, + }, nil + } + + return RMNProxyConfig{}, fmt.Errorf("invalid type for RMN proxy remote address: %T", val) +} + +func (r *ccipChainReader) processRMNRemoteResults(results []types.BatchReadResult) (RMNRemoteConfig, error) { + config := RMNRemoteConfig{} + + if len(results) != 2 { + return RMNRemoteConfig{}, fmt.Errorf("expected 2 RMN remote results, got %d", len(results)) + } + + // Process DigestHeader + val, err := results[0].GetResult() + if err != nil { + return RMNRemoteConfig{}, fmt.Errorf("get RMN remote digest header result: %w", err) + } + if typed, ok := val.(*rmnDigestHeader); ok { + config.DigestHeader = *typed + } else { + return RMNRemoteConfig{}, fmt.Errorf("invalid type for RMN remote digest header: %T", val) + } + + // Process VersionedConfig + val, err = results[1].GetResult() + if err != nil { + return RMNRemoteConfig{}, fmt.Errorf("get RMN remote versioned config result: %w", err) + } + if typed, ok := val.(*versionedConfig); ok { + config.VersionedConfig = *typed + } else { + return RMNRemoteConfig{}, fmt.Errorf("invalid type for RMN remote versioned config: %T", val) + } + + return config, nil +} + +func (r *ccipChainReader) processFeeQuoterResults(results []types.BatchReadResult) (FeeQuoterConfig, error) { + if len(results) != 1 { + return FeeQuoterConfig{}, fmt.Errorf("expected 1 fee quoter result, got %d", len(results)) + } + + val, err := results[0].GetResult() + if err != nil { + return FeeQuoterConfig{}, fmt.Errorf("get fee quoter result: %w", err) + } + + if typed, ok := val.(*feeQuoterStaticConfig); ok { + return FeeQuoterConfig{ + StaticConfig: *typed, + }, nil + } + + return FeeQuoterConfig{}, fmt.Errorf("invalid type for fee quoter static config: %T", val) +} + // --------------------------------------------------- func (r *ccipChainReader) CommitReportsGTETimestamp( @@ -713,74 +1141,31 @@ func (r *ccipChainReader) GetChainFeePriceUpdate(ctx context.Context, selectors return feeUpdates } -func (r *ccipChainReader) GetRMNRemoteConfig( - ctx context.Context, -) (rmntypes.RemoteConfig, error) { - lggr := logutil.WithContextValues(ctx, r.lggr) - if err := validateExtendedReaderExistence(r.contractReaders, r.destChain); err != nil { - return rmntypes.RemoteConfig{}, err - } - - // RMNRemote address stored in the offramp static config is actually the proxy contract address. - // Here we will get the RMNRemote address from the proxy contract by calling the RMNProxy contract. - proxyContractAddress, err := r.GetContractAddress(consts.ContractNameRMNRemote, r.destChain) - if err != nil { - return rmntypes.RemoteConfig{}, fmt.Errorf("get RMNRemote proxy contract address: %w", err) - } - - rmnRemoteAddress, err := r.getRMNRemoteAddress(ctx, lggr, r.destChain, proxyContractAddress) - if err != nil { - return rmntypes.RemoteConfig{}, fmt.Errorf("get RMNRemote address: %w", err) - } - lggr.Debugw("got RMNRemote address", "address", rmnRemoteAddress) - - // TODO: make the calls in parallel using errgroup - var vc versionedConfig - err = r.contractReaders[r.destChain].ExtendedGetLatestValue( - ctx, - consts.ContractNameRMNRemote, - consts.MethodNameGetVersionedConfig, - primitives.Unconfirmed, - map[string]any{}, - &vc, - ) - if err != nil { - return rmntypes.RemoteConfig{}, fmt.Errorf("get RMNRemote config: %w", err) - } - - type ret struct { - DigestHeader cciptypes.Bytes32 +// buildSigners converts internal signer representation to RMN signer info format +func (r *ccipChainReader) buildSigners(signers []signer) []rmntypes.RemoteSignerInfo { + result := make([]rmntypes.RemoteSignerInfo, 0, len(signers)) + for _, s := range signers { + result = append(result, rmntypes.RemoteSignerInfo{ + OnchainPublicKey: s.OnchainPublicKey, + NodeIndex: s.NodeIndex, + }) } - var header ret + return result +} - err = r.contractReaders[r.destChain].ExtendedGetLatestValue( - ctx, - consts.ContractNameRMNRemote, - consts.MethodNameGetReportDigestHeader, - primitives.Unconfirmed, - map[string]any{}, - &header, - ) +func (r *ccipChainReader) GetRMNRemoteConfig(ctx context.Context) (rmntypes.RemoteConfig, error) { + config, err := r.getChainConfig(ctx, r.destChain) if err != nil { - return rmntypes.RemoteConfig{}, fmt.Errorf("get RMNRemote report digest header: %w", err) - } - lggr.Infow("got RMNRemote report digest header", "digest", header.DigestHeader) - - signers := make([]rmntypes.RemoteSignerInfo, 0, len(vc.Config.Signers)) - for _, signer := range vc.Config.Signers { - signers = append(signers, rmntypes.RemoteSignerInfo{ - OnchainPublicKey: signer.OnchainPublicKey, - NodeIndex: signer.NodeIndex, - }) + return rmntypes.RemoteConfig{}, err } return rmntypes.RemoteConfig{ - ContractAddress: rmnRemoteAddress, - ConfigDigest: vc.Config.RMNHomeContractConfigDigest, - Signers: signers, - FSign: vc.Config.FSign, - ConfigVersion: vc.Version, - RmnReportVersion: header.DigestHeader, + ContractAddress: config.RMNProxy.RemoteAddress, + ConfigDigest: config.RMNRemote.VersionedConfig.Config.RMNHomeContractConfigDigest, + Signers: r.buildSigners(config.RMNRemote.VersionedConfig.Config.Signers), + FSign: config.RMNRemote.VersionedConfig.Config.FSign, + ConfigVersion: config.RMNRemote.VersionedConfig.Version, + RmnReportVersion: config.RMNRemote.DigestHeader.DigestHeader, }, nil } @@ -856,69 +1241,32 @@ func (r *ccipChainReader) discoverOffRampContracts( ctx context.Context, lggr logger.Logger, ) (ContractAddresses, error) { - // Exit without an error if we cannot read the destination. - if err := validateExtendedReaderExistence(r.contractReaders, r.destChain); err != nil { - return nil, fmt.Errorf("validate extended reader existence for destChain(%d): %w", r.destChain, err) + config, err := r.getChainConfig(ctx, r.destChain) + if err != nil { + return nil, fmt.Errorf("get chain config: %w", err) } - // build up resp as we go. resp := make(ContractAddresses) - // OnRamps are in the offRamp SourceChainConfig. - { - sourceConfigs, err := r.getAllOffRampSourceChainsConfig(ctx, lggr) - if err != nil { - return nil, fmt.Errorf("unable to get SourceChainsConfig: %w", err) + // Process offramp configs + for i, chainSel := range config.Offramp.SelectorsAndConf.Selectors { + cfg := config.Offramp.SelectorsAndConf.SourceChainConfigs[i] + if !cfg.IsEnabled { + continue } - // Iterate results in sourceChain selector order so that the router config is deterministic. - keys := maps.Keys(sourceConfigs) - sort.Slice(keys, func(i, j int) bool { return keys[i] < keys[j] }) - for _, sourceChain := range keys { - cfg := sourceConfigs[sourceChain] - resp = resp.Append(consts.ContractNameOnRamp, sourceChain, cfg.OnRamp) - // The local router is located in each source sourceChain config. Add it once. - if len(resp[consts.ContractNameRouter][r.destChain]) == 0 { - resp = resp.Append(consts.ContractNameRouter, r.destChain, cfg.Router) - lggr.Infow("appending router contract address", "address", cfg.Router) - } + resp = resp.Append(consts.ContractNameOnRamp, cciptypes.ChainSelector(chainSel), cfg.OnRamp) + if len(resp[consts.ContractNameRouter][r.destChain]) == 0 { + resp = resp.Append(consts.ContractNameRouter, r.destChain, cfg.Router) } } - // NonceManager and RMNRemote are in the offramp static config. - { - var staticConfig offRampStaticChainConfig - err := r.getDestinationData( - ctx, - r.destChain, - consts.ContractNameOffRamp, - consts.MethodNameOffRampGetStaticConfig, - &staticConfig, - ) - if err != nil { - return nil, fmt.Errorf("unable to lookup nonce manager and rmn proxy remote (offramp static config): %w", err) - } - resp = resp.Append(consts.ContractNameNonceManager, r.destChain, staticConfig.NonceManager) - resp = resp.Append(consts.ContractNameRMNRemote, r.destChain, staticConfig.RmnRemote) - lggr.Infow("appending RMN remote contract address", "address", staticConfig.RmnRemote) - } + // Add static config contracts + resp = resp.Append(consts.ContractNameNonceManager, r.destChain, config.Offramp.StaticConfig.NonceManager) + resp = resp.Append(consts.ContractNameRMNRemote, r.destChain, config.Offramp.StaticConfig.RmnRemote) - // FeeQuoter from the offRamp dynamic config. - { - var dynamicConfig offRampDynamicChainConfig - err := r.getDestinationData( - ctx, - r.destChain, - consts.ContractNameOffRamp, - consts.MethodNameOffRampGetDynamicConfig, - &dynamicConfig, - ) - if err != nil { - return nil, fmt.Errorf("unable to lookup fee quoter (offramp dynamic config): %w", err) - } - resp = resp.Append(consts.ContractNameFeeQuoter, r.destChain, dynamicConfig.FeeQuoter) - lggr.Infow("appending fee quoter contract address", "address", dynamicConfig.FeeQuoter) - } + // Add dynamic config contracts + resp = resp.Append(consts.ContractNameFeeQuoter, r.destChain, config.Offramp.DynamicConfig.FeeQuoter) return resp, nil } @@ -1055,24 +1403,16 @@ type feeQuoterStaticConfig struct { // getDestFeeQuoterStaticConfig returns the destination chain's Fee Quoter's StaticConfig func (r *ccipChainReader) getDestFeeQuoterStaticConfig(ctx context.Context) (feeQuoterStaticConfig, error) { - var staticConfig feeQuoterStaticConfig - err := r.getDestinationData( - ctx, - r.destChain, - consts.ContractNameFeeQuoter, - consts.MethodNameFeeQuoterGetStaticConfig, - &staticConfig, - ) - + config, err := r.getChainConfig(ctx, r.destChain) if err != nil { - return feeQuoterStaticConfig{}, fmt.Errorf("unable to lookup fee quoter (offramp static config): %w", err) + return feeQuoterStaticConfig{}, err } - if len(staticConfig.LinkToken) == 0 { + if len(config.FeeQuoter.StaticConfig.LinkToken) == 0 { return feeQuoterStaticConfig{}, fmt.Errorf("link token address is empty") } - return staticConfig, nil + return config.FeeQuoter.StaticConfig, nil } // getFeeQuoterTokenPriceUSD gets the token price in USD of the given token address from the FeeQuoter contract on the @@ -1215,43 +1555,27 @@ func (r *ccipChainReader) getAllOffRampSourceChainsConfig( ctx context.Context, lggr logger.Logger, ) (map[cciptypes.ChainSelector]sourceChainConfig, error) { - if err := validateExtendedReaderExistence(r.contractReaders, r.destChain); err != nil { - return nil, fmt.Errorf("validate extended reader existence: %w", err) + config, err := r.getChainConfig(ctx, r.destChain) + if err != nil { + return nil, fmt.Errorf("get chain config: %w", err) } configs := make(map[cciptypes.ChainSelector]sourceChainConfig) - var resp selectorsAndConfigs - err := r.contractReaders[r.destChain].ExtendedGetLatestValue( - ctx, - consts.ContractNameOffRamp, - consts.MethodNameOffRampGetAllSourceChainConfigs, - primitives.Unconfirmed, - map[string]any{}, - &resp, - ) - if err != nil { - return nil, fmt.Errorf("failed to get source chain configs for source chain %d: %w", - r.destChain, err) - } - - if len(resp.SourceChainConfigs) != len(resp.Selectors) { - return nil, fmt.Errorf("selectors and source chain configs length mismatch: %v", resp) + if len(config.Offramp.SelectorsAndConf.SourceChainConfigs) != len(config.Offramp.SelectorsAndConf.Selectors) { + return nil, fmt.Errorf("selectors and source chain configs length mismatch") } - lggr.Debugw("got source chain configs", "configs", resp) - - // Populate the map. - for i := range resp.Selectors { - chainSel := cciptypes.ChainSelector(resp.Selectors[i]) - cfg := resp.SourceChainConfigs[i] + // Populate the map + for i := range config.Offramp.SelectorsAndConf.Selectors { + chainSel := cciptypes.ChainSelector(config.Offramp.SelectorsAndConf.Selectors[i]) + cfg := config.Offramp.SelectorsAndConf.SourceChainConfigs[i] enabled, err := cfg.check() if err != nil { return nil, fmt.Errorf("source chain config check for chain %d failed: %w", chainSel, err) } if !enabled { - // We don't want to process disabled chains prematurely. lggr.Debugw("source chain is disabled", "chain", chainSel) continue } @@ -1481,25 +1805,19 @@ func (r *ccipChainReader) getRMNRemoteAddress( lggr logger.Logger, chain cciptypes.ChainSelector, rmnRemoteProxyAddress []byte) ([]byte, error) { + // Still need to bind the contract before accessing it _, err := bindExtendedReaderContract(ctx, lggr, r.contractReaders, chain, consts.ContractNameRMNProxy, rmnRemoteProxyAddress) if err != nil { return nil, fmt.Errorf("bind RMN proxy contract: %w", err) } - // get the RMN remote address from the proxy - var rmnRemoteAddress []byte - err = r.getDestinationData( - ctx, - chain, - consts.ContractNameRMNProxy, - consts.MethodNameGetARM, - &rmnRemoteAddress, - ) + // Get the address from cache instead of making a contract call + config, err := r.getChainConfig(ctx, chain) if err != nil { - return nil, fmt.Errorf("unable to lookup RMN remote address (RMN proxy): %w", err) + return nil, fmt.Errorf("get chain config: %w", err) } - return rmnRemoteAddress, nil + return config.RMNProxy.RemoteAddress, nil } // Get the DestChainConfig from the FeeQuoter contract on the given chain. @@ -1593,43 +1911,19 @@ func (r *ccipChainReader) GetLatestPriceSeqNr(ctx context.Context) (uint64, erro } func (r *ccipChainReader) GetOffRampConfigDigest(ctx context.Context, pluginType uint8) ([32]byte, error) { - if err := validateExtendedReaderExistence(r.contractReaders, r.destChain); err != nil { - return [32]byte{}, fmt.Errorf("validate dest=%d extended reader existence: %w", r.destChain, err) - } - - type ConfigInfo struct { - ConfigDigest [32]byte - F uint8 - N uint8 - IsSignatureVerificationEnabled bool - } - - type OCRConfig struct { - ConfigInfo ConfigInfo - Signers [][]byte - Transmitters [][]byte - } - - type OCRConfigResponse struct { - OCRConfig OCRConfig + config, err := r.getChainConfig(ctx, r.destChain) + if err != nil { + return [32]byte{}, err } - var resp OCRConfigResponse - err := r.contractReaders[r.destChain].ExtendedGetLatestValue( - ctx, - consts.ContractNameOffRamp, - consts.MethodNameOffRampLatestConfigDetails, - primitives.Unconfirmed, - map[string]any{ - "ocrPluginType": pluginType, - }, - &resp, - ) - if err != nil { - return [32]byte{}, fmt.Errorf("get latest config digest: %w", err) + var respFromBatch OCRConfigResponse + if pluginType == consts.PluginTypeCommit { + respFromBatch = config.Offramp.CommitLatestOCRConfig + } else { + respFromBatch = config.Offramp.ExecLatestOCRConfig } - return resp.OCRConfig.ConfigInfo.ConfigDigest, nil + return respFromBatch.OCRConfig.ConfigInfo.ConfigDigest, nil } func validateCommitReportAcceptedEvent(seq types.Sequence, gteTimestamp time.Time) (*CommitReportAcceptedEvent, error) { From 597fda563f446d10a9c0568b28d664bc866051b3 Mon Sep 17 00:00:00 2001 From: nogo <0xnogo@gmail.com> Date: Tue, 4 Feb 2025 20:45:50 +0400 Subject: [PATCH 02/18] disable caching --- pkg/reader/ccip.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/reader/ccip.go b/pkg/reader/ccip.go index a48aea40b..da48eb907 100644 --- a/pkg/reader/ccip.go +++ b/pkg/reader/ccip.go @@ -34,7 +34,7 @@ import ( ) // Default refresh period for cache if not specified -const defaultRefreshPeriod = 30 * time.Second +const defaultRefreshPeriod = 0 * time.Second // ChainConfigSnapshot represents the complete configuration state of the chain type ChainConfigSnapshot struct { From 97a08f85c569611ee67ec3850477aadbc90e6e86 Mon Sep 17 00:00:00 2001 From: nogo <0xnogo@gmail.com> Date: Tue, 4 Feb 2025 21:17:07 +0400 Subject: [PATCH 03/18] log all --- pkg/reader/ccip.go | 178 ++++++++++++++++++++++++++++++++++------ pkg/reader/ccip_test.go | 11 ++- 2 files changed, 161 insertions(+), 28 deletions(-) diff --git a/pkg/reader/ccip.go b/pkg/reader/ccip.go index da48eb907..be883e4f7 100644 --- a/pkg/reader/ccip.go +++ b/pkg/reader/ccip.go @@ -113,20 +113,6 @@ type ccipChainReader struct { cache *configCache } -// NewCCIPChainReaderWithCache creates a new CCIPReader with specified cache refresh period -func NewCCIPChainReaderWithCache( - ctx context.Context, - lggr logger.Logger, - contractReaders map[cciptypes.ChainSelector]contractreader.ContractReaderFacade, - contractWriters map[cciptypes.ChainSelector]types.ContractWriter, - destChain cciptypes.ChainSelector, - offrampAddress []byte, - extraDataCodec cciptypes.ExtraDataCodec, - refreshPeriod time.Duration, -) *ccipChainReader { - return newCCIPChainReaderInternal(ctx, lggr, contractReaders, contractWriters, destChain, offrampAddress, extraDataCodec, refreshPeriod) -} - func newCCIPChainReaderInternal( ctx context.Context, lggr logger.Logger, @@ -1154,11 +1140,58 @@ func (r *ccipChainReader) buildSigners(signers []signer) []rmntypes.RemoteSigner } func (r *ccipChainReader) GetRMNRemoteConfig(ctx context.Context) (rmntypes.RemoteConfig, error) { + // Get from cache config, err := r.getChainConfig(ctx, r.destChain) if err != nil { + r.lggr.Errorw("Failed to get config from cache", "err", err) + } else { + r.lggr.Infow("Cache response RMNRemoteConfig", + "contractAddress", hex.EncodeToString(config.RMNProxy.RemoteAddress), + "configDigest", hex.EncodeToString(config.RMNRemote.VersionedConfig.Config.RMNHomeContractConfigDigest[:]), + "signers", len(config.RMNRemote.VersionedConfig.Config.Signers), + "FSign", config.RMNRemote.VersionedConfig.Config.FSign, + "configVersion", config.RMNRemote.VersionedConfig.Version, + "rmnReportVersion", hex.EncodeToString(config.RMNRemote.DigestHeader.DigestHeader[:])) + } + + // Original direct call code for comparison + if err := validateExtendedReaderExistence(r.contractReaders, r.destChain); err != nil { return rmntypes.RemoteConfig{}, err } + var vc versionedConfig + err = r.contractReaders[r.destChain].ExtendedGetLatestValue( + ctx, + consts.ContractNameRMNRemote, + consts.MethodNameGetVersionedConfig, + primitives.Unconfirmed, + map[string]any{}, + &vc, + ) + if err != nil { + return rmntypes.RemoteConfig{}, fmt.Errorf("get RMNRemote config: %w", err) + } + + var header rmnDigestHeader + err = r.contractReaders[r.destChain].ExtendedGetLatestValue( + ctx, + consts.ContractNameRMNRemote, + consts.MethodNameGetReportDigestHeader, + primitives.Unconfirmed, + map[string]any{}, + &header, + ) + if err != nil { + return rmntypes.RemoteConfig{}, fmt.Errorf("get RMNRemote report digest header: %w", err) + } + + r.lggr.Infow("Direct call response RMNRemoteConfig", + "configDigest", hex.EncodeToString(vc.Config.RMNHomeContractConfigDigest[:]), + "signers", len(vc.Config.Signers), + "FSign", vc.Config.FSign, + "configVersion", vc.Version, + "rmnReportVersion", hex.EncodeToString(header.DigestHeader[:])) + return rmntypes.RemoteConfig{ ContractAddress: config.RMNProxy.RemoteAddress, ConfigDigest: config.RMNRemote.VersionedConfig.Config.RMNHomeContractConfigDigest, @@ -1241,14 +1274,49 @@ func (r *ccipChainReader) discoverOffRampContracts( ctx context.Context, lggr logger.Logger, ) (ContractAddresses, error) { + // Get from cache config, err := r.getChainConfig(ctx, r.destChain) if err != nil { - return nil, fmt.Errorf("get chain config: %w", err) + r.lggr.Errorw("Failed to get config from cache", "err", err) + } else { + r.lggr.Infow("Cache response discoverOffRampContracts", + "staticConfig", config.Offramp.StaticConfig, + "dynamicConfig", config.Offramp.DynamicConfig, + "selectorsAndConf", config.Offramp.SelectorsAndConf) } + // Original direct call for comparison + var staticConfig offRampStaticChainConfig + err = r.getDestinationData( + ctx, + r.destChain, + consts.ContractNameOffRamp, + consts.MethodNameOffRampGetStaticConfig, + &staticConfig, + ) + if err != nil { + return nil, fmt.Errorf("unable to lookup nonce manager and rmn proxy remote (offramp static config): %w", err) + } + + var dynamicConfig offRampDynamicChainConfig + err = r.getDestinationData( + ctx, + r.destChain, + consts.ContractNameOffRamp, + consts.MethodNameOffRampGetDynamicConfig, + &dynamicConfig, + ) + if err != nil { + return nil, fmt.Errorf("unable to lookup fee quoter (offramp dynamic config): %w", err) + } + + r.lggr.Infow("Direct call response discoverOffRampContracts", + "staticConfig", staticConfig, + "dynamicConfig", dynamicConfig) + resp := make(ContractAddresses) - // Process offramp configs + // Process offramp configs from cache for i, chainSel := range config.Offramp.SelectorsAndConf.Selectors { cfg := config.Offramp.SelectorsAndConf.SourceChainConfigs[i] if !cfg.IsEnabled { @@ -1403,11 +1471,35 @@ type feeQuoterStaticConfig struct { // getDestFeeQuoterStaticConfig returns the destination chain's Fee Quoter's StaticConfig func (r *ccipChainReader) getDestFeeQuoterStaticConfig(ctx context.Context) (feeQuoterStaticConfig, error) { + // Get from cache config, err := r.getChainConfig(ctx, r.destChain) if err != nil { - return feeQuoterStaticConfig{}, err + r.lggr.Errorw("Failed to get config from cache", "err", err) + } else { + r.lggr.Infow("Cache response FeeQuoterStaticConfig", + "maxFeeJuelsPerMsg", config.FeeQuoter.StaticConfig.MaxFeeJuelsPerMsg, + "linkToken", hex.EncodeToString(config.FeeQuoter.StaticConfig.LinkToken), + "stalenessThreshold", config.FeeQuoter.StaticConfig.StalenessThreshold) } + // Original direct call for comparison + var staticConfig feeQuoterStaticConfig + err = r.getDestinationData( + ctx, + r.destChain, + consts.ContractNameFeeQuoter, + consts.MethodNameFeeQuoterGetStaticConfig, + &staticConfig, + ) + if err != nil { + return feeQuoterStaticConfig{}, fmt.Errorf("unable to lookup fee quoter (offramp static config): %w", err) + } + + r.lggr.Infow("Direct call response FeeQuoterStaticConfig", + "maxFeeJuelsPerMsg", staticConfig.MaxFeeJuelsPerMsg, + "linkToken", hex.EncodeToString(staticConfig.LinkToken), + "stalenessThreshold", staticConfig.StalenessThreshold) + if len(config.FeeQuoter.StaticConfig.LinkToken) == 0 { return feeQuoterStaticConfig{}, fmt.Errorf("link token address is empty") } @@ -1911,19 +2003,57 @@ func (r *ccipChainReader) GetLatestPriceSeqNr(ctx context.Context) (uint64, erro } func (r *ccipChainReader) GetOffRampConfigDigest(ctx context.Context, pluginType uint8) ([32]byte, error) { + // Get from cache config, err := r.getChainConfig(ctx, r.destChain) if err != nil { - return [32]byte{}, err + r.lggr.Errorw("Failed to get config from cache", "err", err) + } else { + var respFromCache OCRConfigResponse + if pluginType == consts.PluginTypeCommit { + respFromCache = config.Offramp.CommitLatestOCRConfig + } else { + respFromCache = config.Offramp.ExecLatestOCRConfig + } + r.lggr.Infow("Cache response", + "pluginType", pluginType, + "configDigest", hex.EncodeToString(respFromCache.OCRConfig.ConfigInfo.ConfigDigest[:]), + "F", respFromCache.OCRConfig.ConfigInfo.F, + "N", respFromCache.OCRConfig.ConfigInfo.N, + "isSignatureVerificationEnabled", respFromCache.OCRConfig.ConfigInfo.IsSignatureVerificationEnabled, + "signers", len(respFromCache.OCRConfig.Signers), + "transmitters", len(respFromCache.OCRConfig.Transmitters)) } - var respFromBatch OCRConfigResponse - if pluginType == consts.PluginTypeCommit { - respFromBatch = config.Offramp.CommitLatestOCRConfig - } else { - respFromBatch = config.Offramp.ExecLatestOCRConfig + // Original direct call + if err := validateExtendedReaderExistence(r.contractReaders, r.destChain); err != nil { + return [32]byte{}, fmt.Errorf("validate dest=%d extended reader existence: %w", r.destChain, err) } - return respFromBatch.OCRConfig.ConfigInfo.ConfigDigest, nil + var resp OCRConfigResponse + err = r.contractReaders[r.destChain].ExtendedGetLatestValue( + ctx, + consts.ContractNameOffRamp, + consts.MethodNameOffRampLatestConfigDetails, + primitives.Unconfirmed, + map[string]any{ + "ocrPluginType": pluginType, + }, + &resp, + ) + if err != nil { + return [32]byte{}, fmt.Errorf("get latest config digest: %w", err) + } + + r.lggr.Infow("Direct call response", + "pluginType", pluginType, + "configDigest", hex.EncodeToString(resp.OCRConfig.ConfigInfo.ConfigDigest[:]), + "F", resp.OCRConfig.ConfigInfo.F, + "N", resp.OCRConfig.ConfigInfo.N, + "isSignatureVerificationEnabled", resp.OCRConfig.ConfigInfo.IsSignatureVerificationEnabled, + "signers", len(resp.OCRConfig.Signers), + "transmitters", len(resp.OCRConfig.Transmitters)) + + return resp.OCRConfig.ConfigInfo.ConfigDigest, nil } func validateCommitReportAcceptedEvent(seq types.Sequence, gteTimestamp time.Time) (*CommitReportAcceptedEvent, error) { diff --git a/pkg/reader/ccip_test.go b/pkg/reader/ccip_test.go index 1aa50dd11..0e6fe185a 100644 --- a/pkg/reader/ccip_test.go +++ b/pkg/reader/ccip_test.go @@ -956,14 +956,17 @@ func TestCCIPChainReader_LinkPriceUSD(t *testing.T) { feeQuoterAddress := []byte{0x4} contractReaders := make(map[cciptypes.ChainSelector]contractreader.Extended) contractReaders[chainC] = destCR - ccipReader := ccipChainReader{ + ccipReader := newCCIPChainReaderInternal( + context.Background(), logger.Test(t), - contractReaders, + map[cciptypes.ChainSelector]contractreader.ContractReaderFacade{ + chainC: destCR, + }, nil, chainC, - string(offrampAddress), + offrampAddress, ccipocr3.NewMockExtraDataCodec(t), - } + ) require.NoError(t, ccipReader.contractReaders[chainC].Bind( context.Background(), []types.BoundContract{{Name: "FeeQuoter", From 1d50fdc2b1d345fddc284b6960d67426a35a7147 Mon Sep 17 00:00:00 2001 From: nogo <0xnogo@gmail.com> Date: Wed, 5 Feb 2025 11:04:57 +0400 Subject: [PATCH 04/18] improving logs --- pkg/reader/ccip.go | 34 +++++++++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/pkg/reader/ccip.go b/pkg/reader/ccip.go index be883e4f7..db391ed4f 100644 --- a/pkg/reader/ccip.go +++ b/pkg/reader/ccip.go @@ -1185,7 +1185,21 @@ func (r *ccipChainReader) GetRMNRemoteConfig(ctx context.Context) (rmntypes.Remo return rmntypes.RemoteConfig{}, fmt.Errorf("get RMNRemote report digest header: %w", err) } + // get the RMN remote address from the proxy + var rmnRemoteAddress []byte + err = r.getDestinationData( + ctx, + r.destChain, + consts.ContractNameRMNProxy, + consts.MethodNameGetARM, + &rmnRemoteAddress, + ) + if err != nil { + return rmntypes.RemoteConfig{}, fmt.Errorf("unable to lookup RMN remote address (RMN proxy): %w", err) + } + r.lggr.Infow("Direct call response RMNRemoteConfig", + "contractAddress", hex.EncodeToString(rmnRemoteAddress), "configDigest", hex.EncodeToString(vc.Config.RMNHomeContractConfigDigest[:]), "signers", len(vc.Config.Signers), "FSign", vc.Config.FSign, @@ -1310,9 +1324,23 @@ func (r *ccipChainReader) discoverOffRampContracts( return nil, fmt.Errorf("unable to lookup fee quoter (offramp dynamic config): %w", err) } + var selectorsAndConfigs selectorsAndConfigs + err = r.contractReaders[r.destChain].ExtendedGetLatestValue( + ctx, + consts.ContractNameOffRamp, + consts.MethodNameOffRampGetAllSourceChainConfigs, + primitives.Unconfirmed, + map[string]any{}, + &selectorsAndConfigs, + ) + if err != nil { + return nil, fmt.Errorf("unable to lookup onRamp and router (offramp source chain configs): %w", err) + } + r.lggr.Infow("Direct call response discoverOffRampContracts", "staticConfig", staticConfig, - "dynamicConfig", dynamicConfig) + "dynamicConfig", dynamicConfig, + "selectorsAndConf", selectorsAndConfigs) resp := make(ContractAddresses) @@ -2014,7 +2042,7 @@ func (r *ccipChainReader) GetOffRampConfigDigest(ctx context.Context, pluginType } else { respFromCache = config.Offramp.ExecLatestOCRConfig } - r.lggr.Infow("Cache response", + r.lggr.Infow("Cache response OCRConfigResponse", "pluginType", pluginType, "configDigest", hex.EncodeToString(respFromCache.OCRConfig.ConfigInfo.ConfigDigest[:]), "F", respFromCache.OCRConfig.ConfigInfo.F, @@ -2044,7 +2072,7 @@ func (r *ccipChainReader) GetOffRampConfigDigest(ctx context.Context, pluginType return [32]byte{}, fmt.Errorf("get latest config digest: %w", err) } - r.lggr.Infow("Direct call response", + r.lggr.Infow("Direct call response OCRConfigResponse", "pluginType", pluginType, "configDigest", hex.EncodeToString(resp.OCRConfig.ConfigInfo.ConfigDigest[:]), "F", resp.OCRConfig.ConfigInfo.F, From 361e63cac919a9643313827f0a54673f08830b0a Mon Sep 17 00:00:00 2001 From: nogo <0xnogo@gmail.com> Date: Wed, 5 Feb 2025 11:25:11 +0400 Subject: [PATCH 05/18] fix getRMNRemoteAddress --- pkg/reader/ccip.go | 41 +++++++++++++++++++++++++++++++---------- 1 file changed, 31 insertions(+), 10 deletions(-) diff --git a/pkg/reader/ccip.go b/pkg/reader/ccip.go index db391ed4f..0678fa21e 100644 --- a/pkg/reader/ccip.go +++ b/pkg/reader/ccip.go @@ -1185,19 +1185,14 @@ func (r *ccipChainReader) GetRMNRemoteConfig(ctx context.Context) (rmntypes.Remo return rmntypes.RemoteConfig{}, fmt.Errorf("get RMNRemote report digest header: %w", err) } - // get the RMN remote address from the proxy - var rmnRemoteAddress []byte - err = r.getDestinationData( - ctx, - r.destChain, - consts.ContractNameRMNProxy, - consts.MethodNameGetARM, - &rmnRemoteAddress, - ) + proxyContractAddress, err := r.GetContractAddress(consts.ContractNameRMNRemote, r.destChain) if err != nil { - return rmntypes.RemoteConfig{}, fmt.Errorf("unable to lookup RMN remote address (RMN proxy): %w", err) + return rmntypes.RemoteConfig{}, fmt.Errorf("get RMNRemote proxy contract address: %w", err) } + // get the RMN remote address from the proxy + rmnRemoteAddress, err := r.getRMNRemoteAddress(ctx, r.lggr, r.destChain, proxyContractAddress) + r.lggr.Infow("Direct call response RMNRemoteConfig", "contractAddress", hex.EncodeToString(rmnRemoteAddress), "configDigest", hex.EncodeToString(vc.Config.RMNHomeContractConfigDigest[:]), @@ -1216,6 +1211,32 @@ func (r *ccipChainReader) GetRMNRemoteConfig(ctx context.Context) (rmntypes.Remo }, nil } +func (r *ccipChainReader) getRMNRemoteAddress( + ctx context.Context, + lggr logger.Logger, + chain cciptypes.ChainSelector, + rmnRemoteProxyAddress []byte) ([]byte, error) { + _, err := bindExtendedReaderContract(ctx, lggr, r.contractReaders, chain, consts.ContractNameRMNProxy, rmnRemoteProxyAddress) + if err != nil { + return nil, fmt.Errorf("bind RMN proxy contract: %w", err) + } + + // get the RMN remote address from the proxy + var rmnRemoteAddress []byte + err = r.getDestinationData( + ctx, + chain, + consts.ContractNameRMNProxy, + consts.MethodNameGetARM, + &rmnRemoteAddress, + ) + if err != nil { + return nil, fmt.Errorf("unable to lookup RMN remote address (RMN proxy): %w", err) + } + + return rmnRemoteAddress, nil +} + // GetRmnCurseInfo returns rmn curse/pausing information about the provided chains // from the destination chain RMN remote contract. func (r *ccipChainReader) GetRmnCurseInfo( From db1186ee9f3121454f16b26423015d13d4ea379f Mon Sep 17 00:00:00 2001 From: nogo <0xnogo@gmail.com> Date: Wed, 5 Feb 2025 11:29:11 +0400 Subject: [PATCH 06/18] fix dupes --- pkg/reader/ccip.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/reader/ccip.go b/pkg/reader/ccip.go index 0678fa21e..51e3ad6e2 100644 --- a/pkg/reader/ccip.go +++ b/pkg/reader/ccip.go @@ -1191,7 +1191,7 @@ func (r *ccipChainReader) GetRMNRemoteConfig(ctx context.Context) (rmntypes.Remo } // get the RMN remote address from the proxy - rmnRemoteAddress, err := r.getRMNRemoteAddress(ctx, r.lggr, r.destChain, proxyContractAddress) + rmnRemoteAddress, err := r.getRMNRemoteAddressDirect(ctx, r.lggr, r.destChain, proxyContractAddress) r.lggr.Infow("Direct call response RMNRemoteConfig", "contractAddress", hex.EncodeToString(rmnRemoteAddress), @@ -1211,7 +1211,7 @@ func (r *ccipChainReader) GetRMNRemoteConfig(ctx context.Context) (rmntypes.Remo }, nil } -func (r *ccipChainReader) getRMNRemoteAddress( +func (r *ccipChainReader) getRMNRemoteAddressDirect( ctx context.Context, lggr logger.Logger, chain cciptypes.ChainSelector, From 59c0c43124a820dcdb64158d5a85ad7ee0a77837 Mon Sep 17 00:00:00 2001 From: nogo <0xnogo@gmail.com> Date: Wed, 5 Feb 2025 11:44:55 +0400 Subject: [PATCH 07/18] err --- pkg/reader/ccip.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pkg/reader/ccip.go b/pkg/reader/ccip.go index 51e3ad6e2..5fb287283 100644 --- a/pkg/reader/ccip.go +++ b/pkg/reader/ccip.go @@ -1192,6 +1192,9 @@ func (r *ccipChainReader) GetRMNRemoteConfig(ctx context.Context) (rmntypes.Remo // get the RMN remote address from the proxy rmnRemoteAddress, err := r.getRMNRemoteAddressDirect(ctx, r.lggr, r.destChain, proxyContractAddress) + if err != nil { + return rmntypes.RemoteConfig{}, fmt.Errorf("get RMN remote address: %w", err) + } r.lggr.Infow("Direct call response RMNRemoteConfig", "contractAddress", hex.EncodeToString(rmnRemoteAddress), From ee15551ffba3643fd89815941babb63f1ad678c8 Mon Sep 17 00:00:00 2001 From: nogo <0xnogo@gmail.com> Date: Wed, 5 Feb 2025 14:29:38 +0400 Subject: [PATCH 08/18] rm logs --- pkg/reader/ccip.go | 212 ++------------------------------------------- 1 file changed, 9 insertions(+), 203 deletions(-) diff --git a/pkg/reader/ccip.go b/pkg/reader/ccip.go index 5fb287283..5ae3111d3 100644 --- a/pkg/reader/ccip.go +++ b/pkg/reader/ccip.go @@ -1143,67 +1143,9 @@ func (r *ccipChainReader) GetRMNRemoteConfig(ctx context.Context) (rmntypes.Remo // Get from cache config, err := r.getChainConfig(ctx, r.destChain) if err != nil { - r.lggr.Errorw("Failed to get config from cache", "err", err) - } else { - r.lggr.Infow("Cache response RMNRemoteConfig", - "contractAddress", hex.EncodeToString(config.RMNProxy.RemoteAddress), - "configDigest", hex.EncodeToString(config.RMNRemote.VersionedConfig.Config.RMNHomeContractConfigDigest[:]), - "signers", len(config.RMNRemote.VersionedConfig.Config.Signers), - "FSign", config.RMNRemote.VersionedConfig.Config.FSign, - "configVersion", config.RMNRemote.VersionedConfig.Version, - "rmnReportVersion", hex.EncodeToString(config.RMNRemote.DigestHeader.DigestHeader[:])) - } - - // Original direct call code for comparison - if err := validateExtendedReaderExistence(r.contractReaders, r.destChain); err != nil { - return rmntypes.RemoteConfig{}, err - } - - var vc versionedConfig - err = r.contractReaders[r.destChain].ExtendedGetLatestValue( - ctx, - consts.ContractNameRMNRemote, - consts.MethodNameGetVersionedConfig, - primitives.Unconfirmed, - map[string]any{}, - &vc, - ) - if err != nil { - return rmntypes.RemoteConfig{}, fmt.Errorf("get RMNRemote config: %w", err) + return rmntypes.RemoteConfig{}, fmt.Errorf("get chain config: %w", err) } - var header rmnDigestHeader - err = r.contractReaders[r.destChain].ExtendedGetLatestValue( - ctx, - consts.ContractNameRMNRemote, - consts.MethodNameGetReportDigestHeader, - primitives.Unconfirmed, - map[string]any{}, - &header, - ) - if err != nil { - return rmntypes.RemoteConfig{}, fmt.Errorf("get RMNRemote report digest header: %w", err) - } - - proxyContractAddress, err := r.GetContractAddress(consts.ContractNameRMNRemote, r.destChain) - if err != nil { - return rmntypes.RemoteConfig{}, fmt.Errorf("get RMNRemote proxy contract address: %w", err) - } - - // get the RMN remote address from the proxy - rmnRemoteAddress, err := r.getRMNRemoteAddressDirect(ctx, r.lggr, r.destChain, proxyContractAddress) - if err != nil { - return rmntypes.RemoteConfig{}, fmt.Errorf("get RMN remote address: %w", err) - } - - r.lggr.Infow("Direct call response RMNRemoteConfig", - "contractAddress", hex.EncodeToString(rmnRemoteAddress), - "configDigest", hex.EncodeToString(vc.Config.RMNHomeContractConfigDigest[:]), - "signers", len(vc.Config.Signers), - "FSign", vc.Config.FSign, - "configVersion", vc.Version, - "rmnReportVersion", hex.EncodeToString(header.DigestHeader[:])) - return rmntypes.RemoteConfig{ ContractAddress: config.RMNProxy.RemoteAddress, ConfigDigest: config.RMNRemote.VersionedConfig.Config.RMNHomeContractConfigDigest, @@ -1214,32 +1156,6 @@ func (r *ccipChainReader) GetRMNRemoteConfig(ctx context.Context) (rmntypes.Remo }, nil } -func (r *ccipChainReader) getRMNRemoteAddressDirect( - ctx context.Context, - lggr logger.Logger, - chain cciptypes.ChainSelector, - rmnRemoteProxyAddress []byte) ([]byte, error) { - _, err := bindExtendedReaderContract(ctx, lggr, r.contractReaders, chain, consts.ContractNameRMNProxy, rmnRemoteProxyAddress) - if err != nil { - return nil, fmt.Errorf("bind RMN proxy contract: %w", err) - } - - // get the RMN remote address from the proxy - var rmnRemoteAddress []byte - err = r.getDestinationData( - ctx, - chain, - consts.ContractNameRMNProxy, - consts.MethodNameGetARM, - &rmnRemoteAddress, - ) - if err != nil { - return nil, fmt.Errorf("unable to lookup RMN remote address (RMN proxy): %w", err) - } - - return rmnRemoteAddress, nil -} - // GetRmnCurseInfo returns rmn curse/pausing information about the provided chains // from the destination chain RMN remote contract. func (r *ccipChainReader) GetRmnCurseInfo( @@ -1310,62 +1226,13 @@ func chainSelectorToBytes16(chainSel cciptypes.ChainSelector) [16]byte { // discoverOffRampContracts uses the offRamp for destChain to discover the addresses of other contracts. func (r *ccipChainReader) discoverOffRampContracts( ctx context.Context, - lggr logger.Logger, ) (ContractAddresses, error) { // Get from cache config, err := r.getChainConfig(ctx, r.destChain) if err != nil { - r.lggr.Errorw("Failed to get config from cache", "err", err) - } else { - r.lggr.Infow("Cache response discoverOffRampContracts", - "staticConfig", config.Offramp.StaticConfig, - "dynamicConfig", config.Offramp.DynamicConfig, - "selectorsAndConf", config.Offramp.SelectorsAndConf) - } - - // Original direct call for comparison - var staticConfig offRampStaticChainConfig - err = r.getDestinationData( - ctx, - r.destChain, - consts.ContractNameOffRamp, - consts.MethodNameOffRampGetStaticConfig, - &staticConfig, - ) - if err != nil { - return nil, fmt.Errorf("unable to lookup nonce manager and rmn proxy remote (offramp static config): %w", err) - } - - var dynamicConfig offRampDynamicChainConfig - err = r.getDestinationData( - ctx, - r.destChain, - consts.ContractNameOffRamp, - consts.MethodNameOffRampGetDynamicConfig, - &dynamicConfig, - ) - if err != nil { - return nil, fmt.Errorf("unable to lookup fee quoter (offramp dynamic config): %w", err) - } - - var selectorsAndConfigs selectorsAndConfigs - err = r.contractReaders[r.destChain].ExtendedGetLatestValue( - ctx, - consts.ContractNameOffRamp, - consts.MethodNameOffRampGetAllSourceChainConfigs, - primitives.Unconfirmed, - map[string]any{}, - &selectorsAndConfigs, - ) - if err != nil { - return nil, fmt.Errorf("unable to lookup onRamp and router (offramp source chain configs): %w", err) + return nil, fmt.Errorf("get chain config: %w", err) } - r.lggr.Infow("Direct call response discoverOffRampContracts", - "staticConfig", staticConfig, - "dynamicConfig", dynamicConfig, - "selectorsAndConf", selectorsAndConfigs) - resp := make(ContractAddresses) // Process offramp configs from cache @@ -1397,7 +1264,7 @@ func (r *ccipChainReader) DiscoverContracts(ctx context.Context) (ContractAddres // Discover destination contracts if the dest chain is supported. if err := validateExtendedReaderExistence(r.contractReaders, r.destChain); err == nil { - resp, err = r.discoverOffRampContracts(ctx, lggr) + resp, err = r.discoverOffRampContracts(ctx) // Can't continue with discovery if the destination chain is not available. // We read source chains OnRamps from there, and onRamps are essential for feeQuoter and Router discovery. if err != nil { @@ -1526,32 +1393,9 @@ func (r *ccipChainReader) getDestFeeQuoterStaticConfig(ctx context.Context) (fee // Get from cache config, err := r.getChainConfig(ctx, r.destChain) if err != nil { - r.lggr.Errorw("Failed to get config from cache", "err", err) - } else { - r.lggr.Infow("Cache response FeeQuoterStaticConfig", - "maxFeeJuelsPerMsg", config.FeeQuoter.StaticConfig.MaxFeeJuelsPerMsg, - "linkToken", hex.EncodeToString(config.FeeQuoter.StaticConfig.LinkToken), - "stalenessThreshold", config.FeeQuoter.StaticConfig.StalenessThreshold) - } - - // Original direct call for comparison - var staticConfig feeQuoterStaticConfig - err = r.getDestinationData( - ctx, - r.destChain, - consts.ContractNameFeeQuoter, - consts.MethodNameFeeQuoterGetStaticConfig, - &staticConfig, - ) - if err != nil { - return feeQuoterStaticConfig{}, fmt.Errorf("unable to lookup fee quoter (offramp static config): %w", err) + return feeQuoterStaticConfig{}, fmt.Errorf("get chain config: %w", err) } - r.lggr.Infow("Direct call response FeeQuoterStaticConfig", - "maxFeeJuelsPerMsg", staticConfig.MaxFeeJuelsPerMsg, - "linkToken", hex.EncodeToString(staticConfig.LinkToken), - "stalenessThreshold", staticConfig.StalenessThreshold) - if len(config.FeeQuoter.StaticConfig.LinkToken) == 0 { return feeQuoterStaticConfig{}, fmt.Errorf("link token address is empty") } @@ -2055,56 +1899,18 @@ func (r *ccipChainReader) GetLatestPriceSeqNr(ctx context.Context) (uint64, erro } func (r *ccipChainReader) GetOffRampConfigDigest(ctx context.Context, pluginType uint8) ([32]byte, error) { - // Get from cache config, err := r.getChainConfig(ctx, r.destChain) if err != nil { - r.lggr.Errorw("Failed to get config from cache", "err", err) - } else { - var respFromCache OCRConfigResponse - if pluginType == consts.PluginTypeCommit { - respFromCache = config.Offramp.CommitLatestOCRConfig - } else { - respFromCache = config.Offramp.ExecLatestOCRConfig - } - r.lggr.Infow("Cache response OCRConfigResponse", - "pluginType", pluginType, - "configDigest", hex.EncodeToString(respFromCache.OCRConfig.ConfigInfo.ConfigDigest[:]), - "F", respFromCache.OCRConfig.ConfigInfo.F, - "N", respFromCache.OCRConfig.ConfigInfo.N, - "isSignatureVerificationEnabled", respFromCache.OCRConfig.ConfigInfo.IsSignatureVerificationEnabled, - "signers", len(respFromCache.OCRConfig.Signers), - "transmitters", len(respFromCache.OCRConfig.Transmitters)) - } - - // Original direct call - if err := validateExtendedReaderExistence(r.contractReaders, r.destChain); err != nil { - return [32]byte{}, fmt.Errorf("validate dest=%d extended reader existence: %w", r.destChain, err) + return [32]byte{}, fmt.Errorf("get chain config: %w", err) } var resp OCRConfigResponse - err = r.contractReaders[r.destChain].ExtendedGetLatestValue( - ctx, - consts.ContractNameOffRamp, - consts.MethodNameOffRampLatestConfigDetails, - primitives.Unconfirmed, - map[string]any{ - "ocrPluginType": pluginType, - }, - &resp, - ) - if err != nil { - return [32]byte{}, fmt.Errorf("get latest config digest: %w", err) + if pluginType == consts.PluginTypeCommit { + resp = config.Offramp.CommitLatestOCRConfig + } else { + resp = config.Offramp.ExecLatestOCRConfig } - r.lggr.Infow("Direct call response OCRConfigResponse", - "pluginType", pluginType, - "configDigest", hex.EncodeToString(resp.OCRConfig.ConfigInfo.ConfigDigest[:]), - "F", resp.OCRConfig.ConfigInfo.F, - "N", resp.OCRConfig.ConfigInfo.N, - "isSignatureVerificationEnabled", resp.OCRConfig.ConfigInfo.IsSignatureVerificationEnabled, - "signers", len(resp.OCRConfig.Signers), - "transmitters", len(resp.OCRConfig.Transmitters)) - return resp.OCRConfig.ConfigInfo.ConfigDigest, nil } From 224a772b44c02d3cbe6897b497b8aa350c665929 Mon Sep 17 00:00:00 2001 From: nogo <0xnogo@gmail.com> Date: Wed, 5 Feb 2025 14:48:31 +0400 Subject: [PATCH 09/18] use getRMNRemoteAddress --- pkg/reader/ccip.go | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/pkg/reader/ccip.go b/pkg/reader/ccip.go index 5ae3111d3..2f838a81c 100644 --- a/pkg/reader/ccip.go +++ b/pkg/reader/ccip.go @@ -1140,14 +1140,27 @@ func (r *ccipChainReader) buildSigners(signers []signer) []rmntypes.RemoteSigner } func (r *ccipChainReader) GetRMNRemoteConfig(ctx context.Context) (rmntypes.RemoteConfig, error) { - // Get from cache + lggr := logutil.WithContextValues(ctx, r.lggr) + config, err := r.getChainConfig(ctx, r.destChain) if err != nil { return rmntypes.RemoteConfig{}, fmt.Errorf("get chain config: %w", err) } + // RMNRemote address stored in the offramp static config is actually the proxy contract address. + // Here we will get the RMNRemote address from the proxy contract by calling the RMNProxy contract. + proxyContractAddress, err := r.GetContractAddress(consts.ContractNameRMNRemote, r.destChain) + if err != nil { + return rmntypes.RemoteConfig{}, fmt.Errorf("get RMNRemote proxy contract address: %w", err) + } + + rmnRemoteAddress, err := r.getRMNRemoteAddress(ctx, lggr, r.destChain, proxyContractAddress) + if err != nil { + return rmntypes.RemoteConfig{}, fmt.Errorf("get RMNRemote address: %w", err) + } + return rmntypes.RemoteConfig{ - ContractAddress: config.RMNProxy.RemoteAddress, + ContractAddress: rmnRemoteAddress, ConfigDigest: config.RMNRemote.VersionedConfig.Config.RMNHomeContractConfigDigest, Signers: r.buildSigners(config.RMNRemote.VersionedConfig.Config.Signers), FSign: config.RMNRemote.VersionedConfig.Config.FSign, From 44822b6b0557293dfd91a3c07c77a55f1f9fada7 Mon Sep 17 00:00:00 2001 From: nogo <0xnogo@gmail.com> Date: Wed, 5 Feb 2025 15:02:12 +0400 Subject: [PATCH 10/18] 30s default --- pkg/reader/ccip.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/reader/ccip.go b/pkg/reader/ccip.go index 2f838a81c..ad07a25e1 100644 --- a/pkg/reader/ccip.go +++ b/pkg/reader/ccip.go @@ -34,7 +34,7 @@ import ( ) // Default refresh period for cache if not specified -const defaultRefreshPeriod = 0 * time.Second +const defaultRefreshPeriod = 30 * time.Second // ChainConfigSnapshot represents the complete configuration state of the chain type ChainConfigSnapshot struct { From b780d8db430ab68430778b6f598f0eb591557e87 Mon Sep 17 00:00:00 2001 From: nogo <0xnogo@gmail.com> Date: Wed, 5 Feb 2025 15:22:52 +0400 Subject: [PATCH 11/18] lint + cache file --- pkg/reader/cache.go | 115 +++++++++++++++++ pkg/reader/ccip.go | 281 +++++++++++------------------------------- pkg/reader/helpers.go | 8 ++ 3 files changed, 194 insertions(+), 210 deletions(-) create mode 100644 pkg/reader/cache.go diff --git a/pkg/reader/cache.go b/pkg/reader/cache.go new file mode 100644 index 000000000..93b033135 --- /dev/null +++ b/pkg/reader/cache.go @@ -0,0 +1,115 @@ +package reader + +import ( + "context" + "fmt" + "sync" + "time" + + cciptypes "github.com/smartcontractkit/chainlink-ccip/pkg/types/ccipocr3" +) + +// configCache handles caching of chain configuration data for multiple chains. +// It is used by the ccipChainReader to store and retrieve configuration data, +// avoiding unnecessary contract calls and improving performance. +type configCache struct { + sync.RWMutex + chainCaches map[cciptypes.ChainSelector]*chainCache + refreshPeriod time.Duration +} + +// chainCache represents the cache for a single chain. +// It stores the configuration data for a specific chain and manages +// the last refresh time to determine when the data needs to be updated. +type chainCache struct { + sync.RWMutex + data ChainConfigSnapshot + lastRefresh time.Time +} + +// newConfigCache creates a new multi-chain config cache with the specified refresh period. +// The refresh period determines how often the cached data is considered stale and needs to be updated. +func newConfigCache(refreshPeriod time.Duration) *configCache { + return &configCache{ + chainCaches: make(map[cciptypes.ChainSelector]*chainCache), + refreshPeriod: refreshPeriod, + } +} + +// getOrCreateChainCache safely retrieves or creates a cache for a specific chain. +// It ensures thread safety by using locks when accessing the cache map. +func (c *configCache) getOrCreateChainCache(chainSel cciptypes.ChainSelector) *chainCache { + c.Lock() + defer c.Unlock() + + if cache, exists := c.chainCaches[chainSel]; exists { + return cache + } + + cache := &chainCache{} + c.chainCaches[chainSel] = cache + return cache +} + +// getChainConfig returns the cached chain configuration for a specific chain +func (r *ccipChainReader) getChainConfig( + ctx context.Context, + chainSel cciptypes.ChainSelector) (ChainConfigSnapshot, error) { + chainCache := r.cache.getOrCreateChainCache(chainSel) + + chainCache.RLock() + timeSinceLastRefresh := time.Since(chainCache.lastRefresh) + if timeSinceLastRefresh < r.cache.refreshPeriod { + defer chainCache.RUnlock() + r.lggr.Infow("Cache hit", + "chain", chainSel, + "timeSinceLastRefresh", timeSinceLastRefresh, + "refreshPeriod", r.cache.refreshPeriod) + return chainCache.data, nil + } + chainCache.RUnlock() + + return r.refreshChainCache(ctx, chainSel) +} + +func (r *ccipChainReader) refreshChainCache( + ctx context.Context, + chainSel cciptypes.ChainSelector) (ChainConfigSnapshot, error) { + chainCache := r.cache.getOrCreateChainCache(chainSel) + + chainCache.Lock() + defer chainCache.Unlock() + + timeSinceLastRefresh := time.Since(chainCache.lastRefresh) + if timeSinceLastRefresh < r.cache.refreshPeriod { + r.lggr.Infow("Cache was refreshed by another goroutine", + "chain", chainSel, + "timeSinceLastRefresh", timeSinceLastRefresh) + return chainCache.data, nil + } + + startTime := time.Now() + newData, err := r.fetchChainConfig(ctx, chainSel) + refreshDuration := time.Since(startTime) + + if err != nil { + if !chainCache.lastRefresh.IsZero() { + r.lggr.Warnw("Failed to refresh cache, using old data", + "chain", chainSel, + "error", err, + "lastRefresh", chainCache.lastRefresh, + "refreshDuration", refreshDuration) + return chainCache.data, nil + } + r.lggr.Errorw("Failed to refresh cache, no old data available", + "chain", chainSel, + "error", err, + "refreshDuration", refreshDuration) + return ChainConfigSnapshot{}, fmt.Errorf("failed to refresh cache for chain %d: %w", chainSel, err) + } + + chainCache.data = newData + chainCache.lastRefresh = time.Now() + + return newData, nil +} diff --git a/pkg/reader/ccip.go b/pkg/reader/ccip.go index ad07a25e1..7688c8475 100644 --- a/pkg/reader/ccip.go +++ b/pkg/reader/ccip.go @@ -65,42 +65,6 @@ type RMNProxyConfig struct { RemoteAddress []byte } -// chainCache represents the cache for a single chain -type chainCache struct { - sync.RWMutex - data ChainConfigSnapshot - lastRefresh time.Time -} - -// configCache handles caching of chain configuration data for multiple chains -type configCache struct { - sync.RWMutex - chainCaches map[cciptypes.ChainSelector]*chainCache - refreshPeriod time.Duration -} - -// newConfigCache creates a new multi-chain config cache -func newConfigCache(refreshPeriod time.Duration) *configCache { - return &configCache{ - chainCaches: make(map[cciptypes.ChainSelector]*chainCache), - refreshPeriod: refreshPeriod, - } -} - -// getOrCreateChainCache safely gets or creates a cache for a specific chain -func (c *configCache) getOrCreateChainCache(chainSel cciptypes.ChainSelector) *chainCache { - c.Lock() - defer c.Unlock() - - if cache, exists := c.chainCaches[chainSel]; exists { - return cache - } - - cache := &chainCache{} - c.chainCaches[chainSel] = cache - return cache -} - // TODO: unit test the implementation when the actual contract reader and writer interfaces are finalized and mocks // can be generated. type ccipChainReader struct { @@ -121,19 +85,12 @@ func newCCIPChainReaderInternal( destChain cciptypes.ChainSelector, offrampAddress []byte, extraDataCodec cciptypes.ExtraDataCodec, - refreshPeriod ...time.Duration, // Optional refresh period ) *ccipChainReader { var crs = make(map[cciptypes.ChainSelector]contractreader.Extended) for chainSelector, cr := range contractReaders { crs[chainSelector] = contractreader.NewExtendedContractReader(cr) } - // Use provided refresh period or default - period := defaultRefreshPeriod - if len(refreshPeriod) > 0 { - period = refreshPeriod[0] - } - reader := &ccipChainReader{ lggr: lggr, contractReaders: crs, @@ -141,7 +98,7 @@ func newCCIPChainReaderInternal( destChain: destChain, offrampAddress: typeconv.AddressBytesToString(offrampAddress, uint64(destChain)), extraDataCodec: extraDataCodec, - cache: newConfigCache(period), + cache: newConfigCache(defaultRefreshPeriod), } contracts := ContractAddresses{ @@ -217,66 +174,6 @@ type ConfigInfo struct { } // --------------------------------------------------- -// The following functions are used for the config cache - -// getChainConfig returns the cached chain configuration for a specific chain -func (r *ccipChainReader) getChainConfig(ctx context.Context, chainSel cciptypes.ChainSelector) (ChainConfigSnapshot, error) { - chainCache := r.cache.getOrCreateChainCache(chainSel) - - chainCache.RLock() - timeSinceLastRefresh := time.Since(chainCache.lastRefresh) - if timeSinceLastRefresh < r.cache.refreshPeriod { - defer chainCache.RUnlock() - r.lggr.Infow("Cache hit", - "chain", chainSel, - "timeSinceLastRefresh", timeSinceLastRefresh, - "refreshPeriod", r.cache.refreshPeriod) - return chainCache.data, nil - } - chainCache.RUnlock() - - return r.refreshChainCache(ctx, chainSel) -} - -func (r *ccipChainReader) refreshChainCache(ctx context.Context, chainSel cciptypes.ChainSelector) (ChainConfigSnapshot, error) { - chainCache := r.cache.getOrCreateChainCache(chainSel) - - chainCache.Lock() - defer chainCache.Unlock() - - timeSinceLastRefresh := time.Since(chainCache.lastRefresh) - if timeSinceLastRefresh < r.cache.refreshPeriod { - r.lggr.Infow("Cache was refreshed by another goroutine", - "chain", chainSel, - "timeSinceLastRefresh", timeSinceLastRefresh) - return chainCache.data, nil - } - - startTime := time.Now() - newData, err := r.fetchChainConfig(ctx, chainSel) - refreshDuration := time.Since(startTime) - - if err != nil { - if !chainCache.lastRefresh.IsZero() { - r.lggr.Warnw("Failed to refresh cache, using old data", - "chain", chainSel, - "error", err, - "lastRefresh", chainCache.lastRefresh, - "refreshDuration", refreshDuration) - return chainCache.data, nil - } - r.lggr.Errorw("Failed to refresh cache, no old data available", - "chain", chainSel, - "error", err, - "refreshDuration", refreshDuration) - return ChainConfigSnapshot{}, fmt.Errorf("failed to refresh cache for chain %d: %w", chainSel, err) - } - - chainCache.data = newData - chainCache.lastRefresh = time.Now() - - return newData, nil -} // prepareBatchRequests creates the batch request for all configurations func (r *ccipChainReader) prepareBatchRequests() contractreader.ExtendedBatchGetLatestValuesRequest { @@ -350,7 +247,9 @@ func (r *ccipChainReader) prepareBatchRequests() contractreader.ExtendedBatchGet } // fetchChainConfig fetches the latest configuration for a specific chain -func (r *ccipChainReader) fetchChainConfig(ctx context.Context, chainSel cciptypes.ChainSelector) (ChainConfigSnapshot, error) { +func (r *ccipChainReader) fetchChainConfig( + ctx context.Context, + chainSel cciptypes.ChainSelector) (ChainConfigSnapshot, error) { reader, exists := r.contractReaders[chainSel] if !exists { return ChainConfigSnapshot{}, fmt.Errorf("no contract reader for chain %d", chainSel) @@ -409,54 +308,76 @@ func (r *ccipChainReader) updateFromResults(batchResult types.BatchGetLatestValu return config, nil } -func (r *ccipChainReader) processOfframpResults(results []types.BatchReadResult) (OfframpConfig, error) { - config := OfframpConfig{} +// resultProcessor defines a function type for processing individual results +type resultProcessor func(interface{}) error + +func (r *ccipChainReader) processOfframpResults( + results []types.BatchReadResult) (OfframpConfig, error) { if len(results) != 5 { return OfframpConfig{}, fmt.Errorf("expected 5 offramp results, got %d", len(results)) } - for i, result := range results { - val, err := result.GetResult() - if err != nil { - return OfframpConfig{}, fmt.Errorf("get offramp result %d: %w", i, err) - } + config := OfframpConfig{} - switch i { - case 0: // CommitLatestOCRConfig - if typed, ok := val.(*OCRConfigResponse); ok { - config.CommitLatestOCRConfig = *typed - } else { - return OfframpConfig{}, fmt.Errorf("invalid type for CommitLatestOCRConfig: %T", val) + // Define processors for each expected result + processors := []resultProcessor{ + // CommitLatestOCRConfig + func(val interface{}) error { + typed, ok := val.(*OCRConfigResponse) + if !ok { + return fmt.Errorf("invalid type for CommitLatestOCRConfig: %T", val) } - - case 1: // ExecLatestOCRConfig - if typed, ok := val.(*OCRConfigResponse); ok { - config.ExecLatestOCRConfig = *typed - } else { - return OfframpConfig{}, fmt.Errorf("invalid type for ExecLatestOCRConfig: %T", val) + config.CommitLatestOCRConfig = *typed + return nil + }, + // ExecLatestOCRConfig + func(val interface{}) error { + typed, ok := val.(*OCRConfigResponse) + if !ok { + return fmt.Errorf("invalid type for ExecLatestOCRConfig: %T", val) } - - case 2: // StaticConfig - if typed, ok := val.(*offRampStaticChainConfig); ok { - config.StaticConfig = *typed - } else { - return OfframpConfig{}, fmt.Errorf("invalid type for StaticConfig: %T", val) + config.ExecLatestOCRConfig = *typed + return nil + }, + // StaticConfig + func(val interface{}) error { + typed, ok := val.(*offRampStaticChainConfig) + if !ok { + return fmt.Errorf("invalid type for StaticConfig: %T", val) } - - case 3: // DynamicConfig - if typed, ok := val.(*offRampDynamicChainConfig); ok { - config.DynamicConfig = *typed - } else { - return OfframpConfig{}, fmt.Errorf("invalid type for DynamicConfig: %T", val) + config.StaticConfig = *typed + return nil + }, + // DynamicConfig + func(val interface{}) error { + typed, ok := val.(*offRampDynamicChainConfig) + if !ok { + return fmt.Errorf("invalid type for DynamicConfig: %T", val) } - - case 4: // SelectorsAndConf - if typed, ok := val.(*selectorsAndConfigs); ok { - config.SelectorsAndConf = *typed - } else { - return OfframpConfig{}, fmt.Errorf("invalid type for SelectorsAndConf: %T", val) + config.DynamicConfig = *typed + return nil + }, + // SelectorsAndConf + func(val interface{}) error { + typed, ok := val.(*selectorsAndConfigs) + if !ok { + return fmt.Errorf("invalid type for SelectorsAndConf: %T", val) } + config.SelectorsAndConf = *typed + return nil + }, + } + + // Process each result with its corresponding processor + for i, result := range results { + val, err := result.GetResult() + if err != nil { + return OfframpConfig{}, fmt.Errorf("get offramp result %d: %w", i, err) + } + + if err := processors[i](val); err != nil { + return OfframpConfig{}, fmt.Errorf("process result %d: %w", i, err) } } @@ -494,22 +415,24 @@ func (r *ccipChainReader) processRMNRemoteResults(results []types.BatchReadResul if err != nil { return RMNRemoteConfig{}, fmt.Errorf("get RMN remote digest header result: %w", err) } - if typed, ok := val.(*rmnDigestHeader); ok { - config.DigestHeader = *typed - } else { + + typed, ok := val.(*rmnDigestHeader) + if !ok { return RMNRemoteConfig{}, fmt.Errorf("invalid type for RMN remote digest header: %T", val) } + config.DigestHeader = *typed // Process VersionedConfig val, err = results[1].GetResult() if err != nil { return RMNRemoteConfig{}, fmt.Errorf("get RMN remote versioned config result: %w", err) } - if typed, ok := val.(*versionedConfig); ok { - config.VersionedConfig = *typed - } else { + + vconf, ok := val.(*versionedConfig) + if !ok { return RMNRemoteConfig{}, fmt.Errorf("invalid type for RMN remote versioned config: %T", val) } + config.VersionedConfig = *vconf return config, nil } @@ -1551,42 +1474,6 @@ type selectorsAndConfigs struct { SourceChainConfigs []sourceChainConfig `mapstructure:"F1"` } -// getAllOffRampSourceChainsConfig get all enabled source chain configs from the offRamp for dest chain -func (r *ccipChainReader) getAllOffRampSourceChainsConfig( - ctx context.Context, - lggr logger.Logger, -) (map[cciptypes.ChainSelector]sourceChainConfig, error) { - config, err := r.getChainConfig(ctx, r.destChain) - if err != nil { - return nil, fmt.Errorf("get chain config: %w", err) - } - - configs := make(map[cciptypes.ChainSelector]sourceChainConfig) - - if len(config.Offramp.SelectorsAndConf.SourceChainConfigs) != len(config.Offramp.SelectorsAndConf.Selectors) { - return nil, fmt.Errorf("selectors and source chain configs length mismatch") - } - - // Populate the map - for i := range config.Offramp.SelectorsAndConf.Selectors { - chainSel := cciptypes.ChainSelector(config.Offramp.SelectorsAndConf.Selectors[i]) - cfg := config.Offramp.SelectorsAndConf.SourceChainConfigs[i] - - enabled, err := cfg.check() - if err != nil { - return nil, fmt.Errorf("source chain config check for chain %d failed: %w", chainSel, err) - } - if !enabled { - lggr.Debugw("source chain is disabled", "chain", chainSel) - continue - } - - configs[chainSel] = cfg - } - - return configs, nil -} - // offRampStaticChainConfig is used to parse the response from the offRamp contract's getStaticConfig method. // See: /contracts/src/v0.8/ccip/offRamp/OffRamp.sol:StaticConfig type offRampStaticChainConfig struct { @@ -1605,32 +1492,6 @@ type offRampDynamicChainConfig struct { MessageInterceptor []byte `json:"messageInterceptor"` } -// getData returns data for a single reader. -func (r *ccipChainReader) getDestinationData( - ctx context.Context, - destChain cciptypes.ChainSelector, - contract string, - method string, - returnVal any, -) error { - if err := validateExtendedReaderExistence(r.contractReaders, destChain); err != nil { - return err - } - - if destChain != r.destChain { - return fmt.Errorf("expected destination chain %d, got %d", r.destChain, destChain) - } - - return r.contractReaders[destChain].ExtendedGetLatestValue( - ctx, - contract, - method, - primitives.Unconfirmed, - map[string]any{}, - returnVal, - ) -} - // See DynamicChainConfig in OnRamp.sol type onRampDynamicConfig struct { FeeQuoter []byte `json:"feeQuoter"` diff --git a/pkg/reader/helpers.go b/pkg/reader/helpers.go index 91c35ca8a..3f0db35cf 100644 --- a/pkg/reader/helpers.go +++ b/pkg/reader/helpers.go @@ -124,3 +124,11 @@ func validateReaderExistence( } return nil } + +// Helper function to handle type assertions +func assertAndAssignConfig[T any](val interface{}, errMsg string) (*T, error) { + if typed, ok := val.(*T); ok { + return typed, nil + } + return nil, fmt.Errorf(errMsg, val) +} From d2ad888a99d67826aaeb70f1c673d0a0e06aeb0e Mon Sep 17 00:00:00 2001 From: nogo <0xnogo@gmail.com> Date: Wed, 5 Feb 2025 17:05:00 +0400 Subject: [PATCH 12/18] unit test --- .mockery.yaml | 1 + mocks/pkg/reader/config_cache.go | 154 ++++++++ pkg/reader/cache.go | 367 +++++++++++++++++-- pkg/reader/cache_test.go | 611 +++++++++++++++++++++++++++++++ pkg/reader/ccip.go | 352 ++---------------- pkg/reader/ccip_interface.go | 29 ++ pkg/reader/ccip_test.go | 326 ++++++++--------- pkg/reader/helpers.go | 8 - 8 files changed, 1334 insertions(+), 514 deletions(-) create mode 100644 mocks/pkg/reader/config_cache.go create mode 100644 pkg/reader/cache_test.go diff --git a/.mockery.yaml b/.mockery.yaml index c379cd190..52d77c5c7 100644 --- a/.mockery.yaml +++ b/.mockery.yaml @@ -22,6 +22,7 @@ packages: CCIPReader: PriceReader: RMNHome: + ConfigCache: github.com/smartcontractkit/chainlink-ccip/pkg/contractreader: interfaces: Extended: diff --git a/mocks/pkg/reader/config_cache.go b/mocks/pkg/reader/config_cache.go new file mode 100644 index 000000000..886a54a69 --- /dev/null +++ b/mocks/pkg/reader/config_cache.go @@ -0,0 +1,154 @@ +// Code generated by mockery v2.43.2. DO NOT EDIT. + +package reader + +import ( + context "context" + + ccipocr3 "github.com/smartcontractkit/chainlink-ccip/pkg/types/ccipocr3" + + mock "github.com/stretchr/testify/mock" + + reader "github.com/smartcontractkit/chainlink-ccip/pkg/reader" +) + +// MockConfigCache is an autogenerated mock type for the ConfigCache type +type MockConfigCache struct { + mock.Mock +} + +type MockConfigCache_Expecter struct { + mock *mock.Mock +} + +func (_m *MockConfigCache) EXPECT() *MockConfigCache_Expecter { + return &MockConfigCache_Expecter{mock: &_m.Mock} +} + +// GetChainConfig provides a mock function with given fields: ctx, chainSel +func (_m *MockConfigCache) GetChainConfig(ctx context.Context, chainSel ccipocr3.ChainSelector) (reader.ChainConfigSnapshot, error) { + ret := _m.Called(ctx, chainSel) + + if len(ret) == 0 { + panic("no return value specified for GetChainConfig") + } + + var r0 reader.ChainConfigSnapshot + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, ccipocr3.ChainSelector) (reader.ChainConfigSnapshot, error)); ok { + return rf(ctx, chainSel) + } + if rf, ok := ret.Get(0).(func(context.Context, ccipocr3.ChainSelector) reader.ChainConfigSnapshot); ok { + r0 = rf(ctx, chainSel) + } else { + r0 = ret.Get(0).(reader.ChainConfigSnapshot) + } + + if rf, ok := ret.Get(1).(func(context.Context, ccipocr3.ChainSelector) error); ok { + r1 = rf(ctx, chainSel) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockConfigCache_GetChainConfig_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetChainConfig' +type MockConfigCache_GetChainConfig_Call struct { + *mock.Call +} + +// GetChainConfig is a helper method to define mock.On call +// - ctx context.Context +// - chainSel ccipocr3.ChainSelector +func (_e *MockConfigCache_Expecter) GetChainConfig(ctx interface{}, chainSel interface{}) *MockConfigCache_GetChainConfig_Call { + return &MockConfigCache_GetChainConfig_Call{Call: _e.mock.On("GetChainConfig", ctx, chainSel)} +} + +func (_c *MockConfigCache_GetChainConfig_Call) Run(run func(ctx context.Context, chainSel ccipocr3.ChainSelector)) *MockConfigCache_GetChainConfig_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(ccipocr3.ChainSelector)) + }) + return _c +} + +func (_c *MockConfigCache_GetChainConfig_Call) Return(_a0 reader.ChainConfigSnapshot, _a1 error) *MockConfigCache_GetChainConfig_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockConfigCache_GetChainConfig_Call) RunAndReturn(run func(context.Context, ccipocr3.ChainSelector) (reader.ChainConfigSnapshot, error)) *MockConfigCache_GetChainConfig_Call { + _c.Call.Return(run) + return _c +} + +// RefreshChainConfig provides a mock function with given fields: ctx, chainSel +func (_m *MockConfigCache) RefreshChainConfig(ctx context.Context, chainSel ccipocr3.ChainSelector) (reader.ChainConfigSnapshot, error) { + ret := _m.Called(ctx, chainSel) + + if len(ret) == 0 { + panic("no return value specified for RefreshChainConfig") + } + + var r0 reader.ChainConfigSnapshot + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, ccipocr3.ChainSelector) (reader.ChainConfigSnapshot, error)); ok { + return rf(ctx, chainSel) + } + if rf, ok := ret.Get(0).(func(context.Context, ccipocr3.ChainSelector) reader.ChainConfigSnapshot); ok { + r0 = rf(ctx, chainSel) + } else { + r0 = ret.Get(0).(reader.ChainConfigSnapshot) + } + + if rf, ok := ret.Get(1).(func(context.Context, ccipocr3.ChainSelector) error); ok { + r1 = rf(ctx, chainSel) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockConfigCache_RefreshChainConfig_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RefreshChainConfig' +type MockConfigCache_RefreshChainConfig_Call struct { + *mock.Call +} + +// RefreshChainConfig is a helper method to define mock.On call +// - ctx context.Context +// - chainSel ccipocr3.ChainSelector +func (_e *MockConfigCache_Expecter) RefreshChainConfig(ctx interface{}, chainSel interface{}) *MockConfigCache_RefreshChainConfig_Call { + return &MockConfigCache_RefreshChainConfig_Call{Call: _e.mock.On("RefreshChainConfig", ctx, chainSel)} +} + +func (_c *MockConfigCache_RefreshChainConfig_Call) Run(run func(ctx context.Context, chainSel ccipocr3.ChainSelector)) *MockConfigCache_RefreshChainConfig_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(ccipocr3.ChainSelector)) + }) + return _c +} + +func (_c *MockConfigCache_RefreshChainConfig_Call) Return(_a0 reader.ChainConfigSnapshot, _a1 error) *MockConfigCache_RefreshChainConfig_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockConfigCache_RefreshChainConfig_Call) RunAndReturn(run func(context.Context, ccipocr3.ChainSelector) (reader.ChainConfigSnapshot, error)) *MockConfigCache_RefreshChainConfig_Call { + _c.Call.Return(run) + return _c +} + +// NewMockConfigCache creates a new instance of MockConfigCache. 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 NewMockConfigCache(t interface { + mock.TestingT + Cleanup(func()) +}) *MockConfigCache { + mock := &MockConfigCache{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/reader/cache.go b/pkg/reader/cache.go index 93b033135..e6f030dfb 100644 --- a/pkg/reader/cache.go +++ b/pkg/reader/cache.go @@ -6,16 +6,32 @@ import ( "sync" "time" + "github.com/smartcontractkit/chainlink-ccip/pkg/consts" + "github.com/smartcontractkit/chainlink-ccip/pkg/contractreader" cciptypes "github.com/smartcontractkit/chainlink-ccip/pkg/types/ccipocr3" + + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-common/pkg/types" ) +// ConfigCache defines the interface for caching chain configuration data +type ConfigCache interface { + // GetChainConfig retrieves the cached configuration for a chain + GetChainConfig(ctx context.Context, chainSel cciptypes.ChainSelector) (ChainConfigSnapshot, error) + // RefreshChainConfig forces a refresh of the chain configuration + RefreshChainConfig(ctx context.Context, chainSel cciptypes.ChainSelector) (ChainConfigSnapshot, error) +} + // configCache handles caching of chain configuration data for multiple chains. // It is used by the ccipChainReader to store and retrieve configuration data, // avoiding unnecessary contract calls and improving performance. +// configCache handles caching of chain configuration data for multiple chains type configCache struct { sync.RWMutex chainCaches map[cciptypes.ChainSelector]*chainCache refreshPeriod time.Duration + readers map[cciptypes.ChainSelector]contractreader.Extended + lggr logger.Logger } // chainCache represents the cache for a single chain. @@ -27,17 +43,21 @@ type chainCache struct { lastRefresh time.Time } -// newConfigCache creates a new multi-chain config cache with the specified refresh period. -// The refresh period determines how often the cached data is considered stale and needs to be updated. -func newConfigCache(refreshPeriod time.Duration) *configCache { +// newConfigCache creates a new config cache instance +func newConfigCache( + lggr logger.Logger, + readers map[cciptypes.ChainSelector]contractreader.Extended, + refreshPeriod time.Duration, +) *configCache { return &configCache{ chainCaches: make(map[cciptypes.ChainSelector]*chainCache), refreshPeriod: refreshPeriod, + readers: readers, + lggr: lggr, } } -// getOrCreateChainCache safely retrieves or creates a cache for a specific chain. -// It ensures thread safety by using locks when accessing the cache map. +// getOrCreateChainCache safely retrieves or creates a cache for a specific chain func (c *configCache) getOrCreateChainCache(chainSel cciptypes.ChainSelector) *chainCache { c.Lock() defer c.Unlock() @@ -46,62 +66,79 @@ func (c *configCache) getOrCreateChainCache(chainSel cciptypes.ChainSelector) *c return cache } + // verify we have the reader for this chain + if _, exists := c.readers[chainSel]; !exists { + c.lggr.Errorw("No contract reader for chain", "chain", chainSel) + return nil + } + cache := &chainCache{} c.chainCaches[chainSel] = cache return cache } -// getChainConfig returns the cached chain configuration for a specific chain -func (r *ccipChainReader) getChainConfig( +// GetChainConfig retrieves the cached configuration for a chain +func (c *configCache) GetChainConfig( ctx context.Context, - chainSel cciptypes.ChainSelector) (ChainConfigSnapshot, error) { - chainCache := r.cache.getOrCreateChainCache(chainSel) + chainSel cciptypes.ChainSelector, +) (ChainConfigSnapshot, error) { + // Check if we have a reader for this chain + reader, exists := c.readers[chainSel] + if !exists || reader == nil { + c.lggr.Errorw("No contract reader for chain", "chain", chainSel) + return ChainConfigSnapshot{}, fmt.Errorf("no contract reader for chain %d", chainSel) + } + + chainCache := c.getOrCreateChainCache(chainSel) chainCache.RLock() timeSinceLastRefresh := time.Since(chainCache.lastRefresh) - if timeSinceLastRefresh < r.cache.refreshPeriod { + if timeSinceLastRefresh < c.refreshPeriod { defer chainCache.RUnlock() - r.lggr.Infow("Cache hit", + c.lggr.Infow("Cache hit", "chain", chainSel, "timeSinceLastRefresh", timeSinceLastRefresh, - "refreshPeriod", r.cache.refreshPeriod) + "refreshPeriod", c.refreshPeriod) return chainCache.data, nil } chainCache.RUnlock() - return r.refreshChainCache(ctx, chainSel) + return c.RefreshChainConfig(ctx, chainSel) } -func (r *ccipChainReader) refreshChainCache( +// RefreshChainConfig forces a refresh of the chain configuration +func (c *configCache) RefreshChainConfig( ctx context.Context, - chainSel cciptypes.ChainSelector) (ChainConfigSnapshot, error) { - chainCache := r.cache.getOrCreateChainCache(chainSel) + chainSel cciptypes.ChainSelector, +) (ChainConfigSnapshot, error) { + chainCache := c.getOrCreateChainCache(chainSel) chainCache.Lock() defer chainCache.Unlock() + // Double check if another goroutine has already refreshed timeSinceLastRefresh := time.Since(chainCache.lastRefresh) - if timeSinceLastRefresh < r.cache.refreshPeriod { - r.lggr.Infow("Cache was refreshed by another goroutine", + if timeSinceLastRefresh < c.refreshPeriod { + c.lggr.Infow("Cache was refreshed by another goroutine", "chain", chainSel, "timeSinceLastRefresh", timeSinceLastRefresh) return chainCache.data, nil } startTime := time.Now() - newData, err := r.fetchChainConfig(ctx, chainSel) + newData, err := c.fetchChainConfig(ctx, chainSel) refreshDuration := time.Since(startTime) if err != nil { if !chainCache.lastRefresh.IsZero() { - r.lggr.Warnw("Failed to refresh cache, using old data", + c.lggr.Warnw("Failed to refresh cache, using old data", "chain", chainSel, "error", err, "lastRefresh", chainCache.lastRefresh, "refreshDuration", refreshDuration) return chainCache.data, nil } - r.lggr.Errorw("Failed to refresh cache, no old data available", + c.lggr.Errorw("Failed to refresh cache, no old data available", "chain", chainSel, "error", err, "refreshDuration", refreshDuration) @@ -111,5 +148,293 @@ func (r *ccipChainReader) refreshChainCache( chainCache.data = newData chainCache.lastRefresh = time.Now() + c.lggr.Infow("Successfully refreshed cache", + "chain", chainSel, + "refreshDuration", refreshDuration) + return newData, nil } + +// prepareBatchRequests creates the batch request for all configurations +func (c *configCache) prepareBatchRequests() contractreader.ExtendedBatchGetLatestValuesRequest { + var ( + commitLatestOCRConfig OCRConfigResponse + execLatestOCRConfig OCRConfigResponse + staticConfig offRampStaticChainConfig + dynamicConfig offRampDynamicChainConfig + selectorsAndConf selectorsAndConfigs + rmnRemoteAddress []byte + rmnDigestHeader rmnDigestHeader + rmnVersionConfig versionedConfig + feeQuoterConfig feeQuoterStaticConfig + ) + + return contractreader.ExtendedBatchGetLatestValuesRequest{ + consts.ContractNameOffRamp: { + { + ReadName: consts.MethodNameOffRampLatestConfigDetails, + Params: map[string]any{ + "ocrPluginType": consts.PluginTypeCommit, + }, + ReturnVal: &commitLatestOCRConfig, + }, + { + ReadName: consts.MethodNameOffRampLatestConfigDetails, + Params: map[string]any{ + "ocrPluginType": consts.PluginTypeExecute, + }, + ReturnVal: &execLatestOCRConfig, + }, + { + ReadName: consts.MethodNameOffRampGetStaticConfig, + Params: map[string]any{}, + ReturnVal: &staticConfig, + }, + { + ReadName: consts.MethodNameOffRampGetDynamicConfig, + Params: map[string]any{}, + ReturnVal: &dynamicConfig, + }, + { + ReadName: consts.MethodNameOffRampGetAllSourceChainConfigs, + Params: map[string]any{}, + ReturnVal: &selectorsAndConf, + }, + }, + consts.ContractNameRMNProxy: {{ + ReadName: consts.MethodNameGetARM, + Params: map[string]any{}, + ReturnVal: &rmnRemoteAddress, + }}, + consts.ContractNameRMNRemote: { + { + ReadName: consts.MethodNameGetReportDigestHeader, + Params: map[string]any{}, + ReturnVal: &rmnDigestHeader, + }, + { + ReadName: consts.MethodNameGetVersionedConfig, + Params: map[string]any{}, + ReturnVal: &rmnVersionConfig, + }, + }, + consts.ContractNameFeeQuoter: {{ + ReadName: consts.MethodNameFeeQuoterGetStaticConfig, + Params: map[string]any{}, + ReturnVal: &feeQuoterConfig, + }}, + } +} + +// fetchChainConfig fetches the latest configuration for a specific chain +func (c *configCache) fetchChainConfig( + ctx context.Context, + chainSel cciptypes.ChainSelector) (ChainConfigSnapshot, error) { + reader, exists := c.readers[chainSel] + if !exists { + return ChainConfigSnapshot{}, fmt.Errorf("no contract reader for chain %d", chainSel) + } + + requests := c.prepareBatchRequests() + batchResult, skipped, err := reader.ExtendedBatchGetLatestValues(ctx, requests, true) + if err != nil { + return ChainConfigSnapshot{}, fmt.Errorf("batch get latest values for chain %d: %w", chainSel, err) + } + + if len(skipped) > 0 { + c.lggr.Infow("some contracts were skipped due to no bindings", + "chain", chainSel, + "contracts", skipped) + } + + return c.updateFromResults(batchResult) +} + +func (c *configCache) updateFromResults(batchResult types.BatchGetLatestValuesResult) (ChainConfigSnapshot, error) { + config := ChainConfigSnapshot{} + + for contract, results := range batchResult { + var err error + switch contract.Name { + case consts.ContractNameOffRamp: + config.Offramp, err = c.processOfframpResults(results) + if err != nil { + return ChainConfigSnapshot{}, fmt.Errorf("process offramp results: %w", err) + } + + case consts.ContractNameRMNProxy: + config.RMNProxy, err = c.processRMNProxyResults(results) + if err != nil { + return ChainConfigSnapshot{}, fmt.Errorf("process RMN proxy results: %w", err) + } + + case consts.ContractNameRMNRemote: + config.RMNRemote, err = c.processRMNRemoteResults(results) + if err != nil { + return ChainConfigSnapshot{}, fmt.Errorf("process RMN remote results: %w", err) + } + + case consts.ContractNameFeeQuoter: + config.FeeQuoter, err = c.processFeeQuoterResults(results) + if err != nil { + return ChainConfigSnapshot{}, fmt.Errorf("process fee quoter results: %w", err) + } + + default: + c.lggr.Warnw("Unhandled contract in batch results", "contract", contract.Name) + } + } + + return config, nil +} + +// resultProcessor defines a function type for processing individual results +type resultProcessor func(interface{}) error + +func (c *configCache) processOfframpResults( + results []types.BatchReadResult) (OfframpConfig, error) { + + if len(results) != 5 { + return OfframpConfig{}, fmt.Errorf("expected 5 offramp results, got %d", len(results)) + } + + config := OfframpConfig{} + + // Define processors for each expected result + processors := []resultProcessor{ + // CommitLatestOCRConfig + func(val interface{}) error { + typed, ok := val.(*OCRConfigResponse) + if !ok { + return fmt.Errorf("invalid type for CommitLatestOCRConfig: %T", val) + } + config.CommitLatestOCRConfig = *typed + return nil + }, + // ExecLatestOCRConfig + func(val interface{}) error { + typed, ok := val.(*OCRConfigResponse) + if !ok { + return fmt.Errorf("invalid type for ExecLatestOCRConfig: %T", val) + } + config.ExecLatestOCRConfig = *typed + return nil + }, + // StaticConfig + func(val interface{}) error { + typed, ok := val.(*offRampStaticChainConfig) + if !ok { + return fmt.Errorf("invalid type for StaticConfig: %T", val) + } + config.StaticConfig = *typed + return nil + }, + // DynamicConfig + func(val interface{}) error { + typed, ok := val.(*offRampDynamicChainConfig) + if !ok { + return fmt.Errorf("invalid type for DynamicConfig: %T", val) + } + config.DynamicConfig = *typed + return nil + }, + // SelectorsAndConf + func(val interface{}) error { + typed, ok := val.(*selectorsAndConfigs) + if !ok { + return fmt.Errorf("invalid type for SelectorsAndConf: %T", val) + } + config.SelectorsAndConf = *typed + return nil + }, + } + + // Process each result with its corresponding processor + for i, result := range results { + val, err := result.GetResult() + if err != nil { + return OfframpConfig{}, fmt.Errorf("get offramp result %d: %w", i, err) + } + + if err := processors[i](val); err != nil { + return OfframpConfig{}, fmt.Errorf("process result %d: %w", i, err) + } + } + + return config, nil +} + +func (c *configCache) processRMNProxyResults(results []types.BatchReadResult) (RMNProxyConfig, error) { + if len(results) != 1 { + return RMNProxyConfig{}, fmt.Errorf("expected 1 RMN proxy result, got %d", len(results)) + } + + val, err := results[0].GetResult() + if err != nil { + return RMNProxyConfig{}, fmt.Errorf("get RMN proxy result: %w", err) + } + + if bytes, ok := val.(*[]byte); ok { + return RMNProxyConfig{ + RemoteAddress: *bytes, + }, nil + } + + return RMNProxyConfig{}, fmt.Errorf("invalid type for RMN proxy remote address: %T", val) +} + +func (c *configCache) processRMNRemoteResults(results []types.BatchReadResult) (RMNRemoteConfig, error) { + config := RMNRemoteConfig{} + + if len(results) != 2 { + return RMNRemoteConfig{}, fmt.Errorf("expected 2 RMN remote results, got %d", len(results)) + } + + // Process DigestHeader + val, err := results[0].GetResult() + if err != nil { + return RMNRemoteConfig{}, fmt.Errorf("get RMN remote digest header result: %w", err) + } + + typed, ok := val.(*rmnDigestHeader) + if !ok { + return RMNRemoteConfig{}, fmt.Errorf("invalid type for RMN remote digest header: %T", val) + } + config.DigestHeader = *typed + + // Process VersionedConfig + val, err = results[1].GetResult() + if err != nil { + return RMNRemoteConfig{}, fmt.Errorf("get RMN remote versioned config result: %w", err) + } + + vconf, ok := val.(*versionedConfig) + if !ok { + return RMNRemoteConfig{}, fmt.Errorf("invalid type for RMN remote versioned config: %T", val) + } + config.VersionedConfig = *vconf + + return config, nil +} + +func (c *configCache) processFeeQuoterResults(results []types.BatchReadResult) (FeeQuoterConfig, error) { + if len(results) != 1 { + return FeeQuoterConfig{}, fmt.Errorf("expected 1 fee quoter result, got %d", len(results)) + } + + val, err := results[0].GetResult() + if err != nil { + return FeeQuoterConfig{}, fmt.Errorf("get fee quoter result: %w", err) + } + + if typed, ok := val.(*feeQuoterStaticConfig); ok { + return FeeQuoterConfig{ + StaticConfig: *typed, + }, nil + } + + return FeeQuoterConfig{}, fmt.Errorf("invalid type for fee quoter static config: %T", val) +} + +// Ensure configCache implements ConfigCache +var _ ConfigCache = (*configCache)(nil) diff --git a/pkg/reader/cache_test.go b/pkg/reader/cache_test.go new file mode 100644 index 000000000..5a7db984c --- /dev/null +++ b/pkg/reader/cache_test.go @@ -0,0 +1,611 @@ +package reader + +import ( + "errors" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + + reader_mocks "github.com/smartcontractkit/chainlink-ccip/mocks/pkg/contractreader" + "github.com/smartcontractkit/chainlink-ccip/pkg/consts" + "github.com/smartcontractkit/chainlink-ccip/pkg/contractreader" + cciptypes "github.com/smartcontractkit/chainlink-ccip/pkg/types/ccipocr3" + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-common/pkg/types" + "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" +) + +func setupBasicCache(t *testing.T) (*configCache, *reader_mocks.MockExtended) { + reader := reader_mocks.NewMockExtended(t) + readers := map[cciptypes.ChainSelector]contractreader.Extended{ + chainA: reader, + } + cache := newConfigCache(logger.Test(t), readers, 1*time.Second) + return cache, reader +} + +func TestConfigCache_GetChainConfig_CacheHit(t *testing.T) { + cache, reader := setupBasicCache(t) + ctx := tests.Context(t) + + // Setup mock for initial fetch + mockCommitOCRConfig := OCRConfigResponse{ + OCRConfig: OCRConfig{ + ConfigInfo: ConfigInfo{F: 1, N: 4}, + }, + } + mockExecOCRConfig := OCRConfigResponse{ + OCRConfig: OCRConfig{ + ConfigInfo: ConfigInfo{F: 2, N: 6}, + }, + } + + // Setup batch results + result1 := &types.BatchReadResult{ReadName: consts.MethodNameOffRampLatestConfigDetails} + result1.SetResult(&mockCommitOCRConfig, nil) + result2 := &types.BatchReadResult{ReadName: consts.MethodNameOffRampLatestConfigDetails} + result2.SetResult(&mockExecOCRConfig, nil) + result3 := &types.BatchReadResult{ReadName: consts.MethodNameOffRampGetStaticConfig} + result3.SetResult(&offRampStaticChainConfig{}, nil) + result4 := &types.BatchReadResult{ReadName: consts.MethodNameOffRampGetDynamicConfig} + result4.SetResult(&offRampDynamicChainConfig{}, nil) + result5 := &types.BatchReadResult{ReadName: consts.MethodNameOffRampGetAllSourceChainConfigs} + result5.SetResult(&selectorsAndConfigs{}, nil) + + responses := types.BatchGetLatestValuesResult{ + types.BoundContract{Name: consts.ContractNameOffRamp}: { + *result1, *result2, *result3, *result4, *result5, + }, + } + + reader.On("ExtendedBatchGetLatestValues", + mock.Anything, + mock.Anything, + true, + ).Return(responses, []string{}, nil).Once() + + // First call should fetch + config1, err := cache.GetChainConfig(ctx, chainA) + require.NoError(t, err) + assert.Equal(t, uint8(1), config1.Offramp.CommitLatestOCRConfig.OCRConfig.ConfigInfo.F) + assert.Equal(t, uint8(4), config1.Offramp.CommitLatestOCRConfig.OCRConfig.ConfigInfo.N) + + // Second call within refresh period should hit cache + config2, err := cache.GetChainConfig(ctx, chainA) + require.NoError(t, err) + assert.Equal(t, config1, config2) + + // Verify the mock was called exactly once + reader.AssertNumberOfCalls(t, "ExtendedBatchGetLatestValues", 1) +} + +func TestConfigCache_GetChainConfig_CacheMiss(t *testing.T) { + cache, reader := setupBasicCache(t) + ctx := tests.Context(t) + + // Setup mock responses for two different fetches + setupMockBatchResponse := func(f uint8, n uint8) types.BatchGetLatestValuesResult { + mockConfig := OCRConfigResponse{ + OCRConfig: OCRConfig{ + ConfigInfo: ConfigInfo{F: f, N: n}, + }, + } + + result1 := &types.BatchReadResult{ReadName: consts.MethodNameOffRampLatestConfigDetails} + result1.SetResult(&mockConfig, nil) + result2 := &types.BatchReadResult{ReadName: consts.MethodNameOffRampLatestConfigDetails} + result2.SetResult(&mockConfig, nil) + result3 := &types.BatchReadResult{ReadName: consts.MethodNameOffRampGetStaticConfig} + result3.SetResult(&offRampStaticChainConfig{}, nil) + result4 := &types.BatchReadResult{ReadName: consts.MethodNameOffRampGetDynamicConfig} + result4.SetResult(&offRampDynamicChainConfig{}, nil) + result5 := &types.BatchReadResult{ReadName: consts.MethodNameOffRampGetAllSourceChainConfigs} + result5.SetResult(&selectorsAndConfigs{}, nil) + + return types.BatchGetLatestValuesResult{ + types.BoundContract{Name: consts.ContractNameOffRamp}: { + *result1, *result2, *result3, *result4, *result5, + }, + } + } + + reader.On("ExtendedBatchGetLatestValues", + mock.Anything, + mock.Anything, + true, + ).Return(setupMockBatchResponse(1, 4), []string{}, nil).Once() + + // First call should fetch initial config + config1, err := cache.GetChainConfig(ctx, chainA) + require.NoError(t, err) + assert.Equal(t, uint8(1), config1.Offramp.CommitLatestOCRConfig.OCRConfig.ConfigInfo.F) + + // Wait for cache to expire + time.Sleep(1100 * time.Millisecond) + + reader.On("ExtendedBatchGetLatestValues", + mock.Anything, + mock.Anything, + true, + ).Return(setupMockBatchResponse(2, 6), []string{}, nil).Once() + + // Second call after refresh period should fetch new config + config2, err := cache.GetChainConfig(ctx, chainA) + require.NoError(t, err) + assert.Equal(t, uint8(2), config2.Offramp.CommitLatestOCRConfig.OCRConfig.ConfigInfo.F) + assert.NotEqual(t, config1, config2) + + reader.AssertNumberOfCalls(t, "ExtendedBatchGetLatestValues", 2) +} + +func TestConfigCache_GetChainConfig_Error(t *testing.T) { + cache, reader := setupBasicCache(t) + ctx := tests.Context(t) + + expectedErr := errors.New("fetch error") + reader.On("ExtendedBatchGetLatestValues", + mock.Anything, + mock.Anything, + true, + ).Return(nil, nil, expectedErr) + + // First call with no cached data should return error + _, err := cache.GetChainConfig(ctx, chainA) + require.Error(t, err) + assert.ErrorIs(t, err, expectedErr) +} + +func TestConfigCache_GetChainConfig_ErrorWithCachedData(t *testing.T) { + cache, reader := setupBasicCache(t) + ctx := tests.Context(t) + + // Setup initial successful fetch + mockConfig := OCRConfigResponse{ + OCRConfig: OCRConfig{ + ConfigInfo: ConfigInfo{F: 1, N: 4}, + }, + } + + result1 := &types.BatchReadResult{ReadName: consts.MethodNameOffRampLatestConfigDetails} + result1.SetResult(&mockConfig, nil) + result2 := &types.BatchReadResult{ReadName: consts.MethodNameOffRampLatestConfigDetails} + result2.SetResult(&mockConfig, nil) + result3 := &types.BatchReadResult{ReadName: consts.MethodNameOffRampGetStaticConfig} + result3.SetResult(&offRampStaticChainConfig{}, nil) + result4 := &types.BatchReadResult{ReadName: consts.MethodNameOffRampGetDynamicConfig} + result4.SetResult(&offRampDynamicChainConfig{}, nil) + result5 := &types.BatchReadResult{ReadName: consts.MethodNameOffRampGetAllSourceChainConfigs} + result5.SetResult(&selectorsAndConfigs{}, nil) + + responses := types.BatchGetLatestValuesResult{ + types.BoundContract{Name: consts.ContractNameOffRamp}: { + *result1, *result2, *result3, *result4, *result5, + }, + } + + reader.On("ExtendedBatchGetLatestValues", + mock.Anything, + mock.Anything, + true, + ).Return(responses, []string{}, nil).Once() + + // First call should succeed and cache data + config1, err := cache.GetChainConfig(ctx, chainA) + require.NoError(t, err) + + // Wait for cache to expire + time.Sleep(1100 * time.Millisecond) + + // Setup error for second fetch + reader.On("ExtendedBatchGetLatestValues", + mock.Anything, + mock.Anything, + true, + ).Return(nil, nil, errors.New("fetch error")) + + // Second call should return cached data despite fetch error + config2, err := cache.GetChainConfig(ctx, chainA) + require.NoError(t, err) + assert.Equal(t, config1, config2) +} + +func TestConfigCache_RefreshChainConfig(t *testing.T) { + cache, reader := setupBasicCache(t) + ctx := tests.Context(t) + + // Setup mock response + mockConfig := OCRConfigResponse{ + OCRConfig: OCRConfig{ + ConfigInfo: ConfigInfo{F: 1, N: 4}, + }, + } + + result1 := &types.BatchReadResult{ReadName: consts.MethodNameOffRampLatestConfigDetails} + result1.SetResult(&mockConfig, nil) + result2 := &types.BatchReadResult{ReadName: consts.MethodNameOffRampLatestConfigDetails} + result2.SetResult(&mockConfig, nil) + result3 := &types.BatchReadResult{ReadName: consts.MethodNameOffRampGetStaticConfig} + result3.SetResult(&offRampStaticChainConfig{}, nil) + result4 := &types.BatchReadResult{ReadName: consts.MethodNameOffRampGetDynamicConfig} + result4.SetResult(&offRampDynamicChainConfig{}, nil) + result5 := &types.BatchReadResult{ReadName: consts.MethodNameOffRampGetAllSourceChainConfigs} + result5.SetResult(&selectorsAndConfigs{}, nil) + + responses := types.BatchGetLatestValuesResult{ + types.BoundContract{Name: consts.ContractNameOffRamp}: { + *result1, *result2, *result3, *result4, *result5, + }, + } + + reader.On("ExtendedBatchGetLatestValues", + mock.Anything, + mock.Anything, + true, + ).Return(responses, []string{}, nil).Once() + + // Force refresh should fetch regardless of cache state + config, err := cache.RefreshChainConfig(ctx, chainA) + require.NoError(t, err) + assert.Equal(t, uint8(1), config.Offramp.CommitLatestOCRConfig.OCRConfig.ConfigInfo.F) + + reader.AssertNumberOfCalls(t, "ExtendedBatchGetLatestValues", 1) +} + +func TestConfigCache_ConcurrentAccess(t *testing.T) { + cache, reader := setupBasicCache(t) + ctx := tests.Context(t) + + // Setup mock response + mockConfig := OCRConfigResponse{ + OCRConfig: OCRConfig{ + ConfigInfo: ConfigInfo{F: 1, N: 4}, + }, + } + + result1 := &types.BatchReadResult{ReadName: consts.MethodNameOffRampLatestConfigDetails} + result1.SetResult(&mockConfig, nil) + result2 := &types.BatchReadResult{ReadName: consts.MethodNameOffRampLatestConfigDetails} + result2.SetResult(&mockConfig, nil) + result3 := &types.BatchReadResult{ReadName: consts.MethodNameOffRampGetStaticConfig} + result3.SetResult(&offRampStaticChainConfig{}, nil) + result4 := &types.BatchReadResult{ReadName: consts.MethodNameOffRampGetDynamicConfig} + result4.SetResult(&offRampDynamicChainConfig{}, nil) + result5 := &types.BatchReadResult{ReadName: consts.MethodNameOffRampGetAllSourceChainConfigs} + result5.SetResult(&selectorsAndConfigs{}, nil) + + responses := types.BatchGetLatestValuesResult{ + types.BoundContract{Name: consts.ContractNameOffRamp}: { + *result1, *result2, *result3, *result4, *result5, + }, + } + + reader.On("ExtendedBatchGetLatestValues", + mock.Anything, + mock.Anything, + true, + ).Return(responses, []string{}, nil) + + // Run concurrent fetches + const numGoroutines = 10 + errCh := make(chan error, numGoroutines) + for i := 0; i < numGoroutines; i++ { + go func() { + _, err := cache.GetChainConfig(ctx, chainA) + errCh <- err + }() + } + + // Collect results + for i := 0; i < numGoroutines; i++ { + err := <-errCh + require.NoError(t, err) + } + + // Should only fetch once despite concurrent access + reader.AssertNumberOfCalls(t, "ExtendedBatchGetLatestValues", 1) +} + +func TestConfigCache_Initialization(t *testing.T) { + testCases := []struct { + name string + readers map[cciptypes.ChainSelector]contractreader.Extended + refreshPeriod time.Duration + chainToTest cciptypes.ChainSelector + expectedErr string + }{ + { + name: "nil readers map", + readers: nil, + refreshPeriod: time.Second, + chainToTest: chainA, + expectedErr: "no contract reader for chain", + }, + { + name: "empty readers map", + readers: make(map[cciptypes.ChainSelector]contractreader.Extended), + refreshPeriod: time.Second, + chainToTest: chainA, + expectedErr: "no contract reader for chain", + }, + { + name: "missing specific chain", + readers: map[cciptypes.ChainSelector]contractreader.Extended{ + chainB: nil, // Different chain than we'll test + }, + refreshPeriod: time.Second, + chainToTest: chainA, + expectedErr: "no contract reader for chain", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + lggr := logger.Test(t) + ctx := tests.Context(t) + + cache := newConfigCache(lggr, tc.readers, tc.refreshPeriod) + require.NotNil(t, cache, "cache should never be nil after initialization") + + // Verify the cache's internal state + require.NotNil(t, cache.chainCaches, "chainCaches map should never be nil") + assert.Equal(t, tc.refreshPeriod, cache.refreshPeriod) + + // Test getting config for a chain + _, err := cache.GetChainConfig(ctx, tc.chainToTest) + require.Error(t, err) + assert.Contains(t, err.Error(), tc.expectedErr) + }) + } +} + +func TestConfigCache_GetChainConfig_SkippedContracts(t *testing.T) { + cache, reader := setupBasicCache(t) + ctx := tests.Context(t) + + // Setup mock response with skipped contracts + mockConfig := OCRConfigResponse{ + OCRConfig: OCRConfig{ + ConfigInfo: ConfigInfo{F: 1, N: 4}, + }, + } + + result1 := &types.BatchReadResult{ReadName: consts.MethodNameOffRampLatestConfigDetails} + result1.SetResult(&mockConfig, nil) + result2 := &types.BatchReadResult{ReadName: consts.MethodNameOffRampLatestConfigDetails} + result2.SetResult(&mockConfig, nil) + result3 := &types.BatchReadResult{ReadName: consts.MethodNameOffRampGetStaticConfig} + result3.SetResult(&offRampStaticChainConfig{}, nil) + result4 := &types.BatchReadResult{ReadName: consts.MethodNameOffRampGetDynamicConfig} + result4.SetResult(&offRampDynamicChainConfig{}, nil) + result5 := &types.BatchReadResult{ReadName: consts.MethodNameOffRampGetAllSourceChainConfigs} + result5.SetResult(&selectorsAndConfigs{}, nil) + + responses := types.BatchGetLatestValuesResult{ + types.BoundContract{Name: consts.ContractNameOffRamp}: { + *result1, *result2, *result3, *result4, *result5, + }, + } + skippedContracts := []string{consts.ContractNameRMNProxy} + + reader.On("ExtendedBatchGetLatestValues", + mock.Anything, + mock.Anything, + true, + ).Return(responses, skippedContracts, nil).Once() + + // Should succeed even with skipped contracts + config, err := cache.GetChainConfig(ctx, chainA) + require.NoError(t, err) + assert.Equal(t, uint8(1), config.Offramp.CommitLatestOCRConfig.OCRConfig.ConfigInfo.F) +} + +func TestConfigCache_InvalidResults(t *testing.T) { + cache, reader := setupBasicCache(t) + ctx := tests.Context(t) + + // Test cases for different invalid results + testCases := []struct { + name string + setupMock func() types.BatchGetLatestValuesResult + expectedErr string + }{ + { + name: "missing offramp results", + setupMock: func() types.BatchGetLatestValuesResult { + return types.BatchGetLatestValuesResult{ + types.BoundContract{Name: consts.ContractNameOffRamp}: {}, + } + }, + expectedErr: "expected 5 offramp results", + }, + { + name: "invalid commit config type", + setupMock: func() types.BatchGetLatestValuesResult { + // Setup all 5 required results, but make the first one invalid + result1 := &types.BatchReadResult{ReadName: consts.MethodNameOffRampLatestConfigDetails} + result1.SetResult("invalid type", nil) + + result2 := &types.BatchReadResult{ReadName: consts.MethodNameOffRampLatestConfigDetails} + result2.SetResult(&OCRConfigResponse{}, nil) + + result3 := &types.BatchReadResult{ReadName: consts.MethodNameOffRampGetStaticConfig} + result3.SetResult(&offRampStaticChainConfig{}, nil) + + result4 := &types.BatchReadResult{ReadName: consts.MethodNameOffRampGetDynamicConfig} + result4.SetResult(&offRampDynamicChainConfig{}, nil) + + result5 := &types.BatchReadResult{ReadName: consts.MethodNameOffRampGetAllSourceChainConfigs} + result5.SetResult(&selectorsAndConfigs{}, nil) + + return types.BatchGetLatestValuesResult{ + types.BoundContract{Name: consts.ContractNameOffRamp}: { + *result1, *result2, *result3, *result4, *result5, + }, + } + }, + expectedErr: "invalid type for CommitLatestOCRConfig", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + reader.On("ExtendedBatchGetLatestValues", + mock.Anything, + mock.Anything, + true, + ).Return(tc.setupMock(), []string{}, nil).Once() + + _, err := cache.GetChainConfig(ctx, chainA) + require.Error(t, err) + assert.Contains(t, err.Error(), tc.expectedErr) + }) + } +} + +func TestConfigCache_MultipleChains(t *testing.T) { + readerA := reader_mocks.NewMockExtended(t) + readerB := reader_mocks.NewMockExtended(t) + readers := map[cciptypes.ChainSelector]contractreader.Extended{ + chainA: readerA, + chainB: readerB, + } + cache := newConfigCache(logger.Test(t), readers, 1*time.Second) + ctx := tests.Context(t) + + // Setup mock response for both chains + setupMockResponse := func(f uint8) types.BatchGetLatestValuesResult { + mockConfig := OCRConfigResponse{ + OCRConfig: OCRConfig{ + ConfigInfo: ConfigInfo{F: f, N: 4}, + }, + } + + result1 := &types.BatchReadResult{ReadName: consts.MethodNameOffRampLatestConfigDetails} + result1.SetResult(&mockConfig, nil) + result2 := &types.BatchReadResult{ReadName: consts.MethodNameOffRampLatestConfigDetails} + result2.SetResult(&mockConfig, nil) + result3 := &types.BatchReadResult{ReadName: consts.MethodNameOffRampGetStaticConfig} + result3.SetResult(&offRampStaticChainConfig{}, nil) + result4 := &types.BatchReadResult{ReadName: consts.MethodNameOffRampGetDynamicConfig} + result4.SetResult(&offRampDynamicChainConfig{}, nil) + result5 := &types.BatchReadResult{ReadName: consts.MethodNameOffRampGetAllSourceChainConfigs} + result5.SetResult(&selectorsAndConfigs{}, nil) + + return types.BatchGetLatestValuesResult{ + types.BoundContract{Name: consts.ContractNameOffRamp}: { + *result1, *result2, *result3, *result4, *result5, + }, + } + } + + readerA.On("ExtendedBatchGetLatestValues", + mock.Anything, + mock.Anything, + true, + ).Return(setupMockResponse(1), []string{}, nil).Once() + + readerB.On("ExtendedBatchGetLatestValues", + mock.Anything, + mock.Anything, + true, + ).Return(setupMockResponse(2), []string{}, nil).Once() + + // Get configs for both chains + configA, err := cache.GetChainConfig(ctx, chainA) + require.NoError(t, err) + assert.Equal(t, uint8(1), configA.Offramp.CommitLatestOCRConfig.OCRConfig.ConfigInfo.F) + + configB, err := cache.GetChainConfig(ctx, chainB) + require.NoError(t, err) + assert.Equal(t, uint8(2), configB.Offramp.CommitLatestOCRConfig.OCRConfig.ConfigInfo.F) + + // Each reader should be called exactly once + readerA.AssertNumberOfCalls(t, "ExtendedBatchGetLatestValues", 1) + readerB.AssertNumberOfCalls(t, "ExtendedBatchGetLatestValues", 1) +} + +func TestConfigCache_RefreshPeriod(t *testing.T) { + // Test with different refresh periods + testCases := []struct { + name string + refreshPeriod time.Duration + sleepTime time.Duration + expectRefresh bool + }{ + { + name: "short refresh period", + refreshPeriod: 100 * time.Millisecond, + sleepTime: 150 * time.Millisecond, + expectRefresh: true, + }, + { + name: "long refresh period", + refreshPeriod: 1 * time.Second, + sleepTime: 500 * time.Millisecond, + expectRefresh: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + reader := reader_mocks.NewMockExtended(t) + readers := map[cciptypes.ChainSelector]contractreader.Extended{ + chainA: reader, + } + cache := newConfigCache(logger.Test(t), readers, tc.refreshPeriod) + ctx := tests.Context(t) + + mockConfig := OCRConfigResponse{ + OCRConfig: OCRConfig{ + ConfigInfo: ConfigInfo{F: 1, N: 4}, + }, + } + + result1 := &types.BatchReadResult{ReadName: consts.MethodNameOffRampLatestConfigDetails} + result1.SetResult(&mockConfig, nil) + result2 := &types.BatchReadResult{ReadName: consts.MethodNameOffRampLatestConfigDetails} + result2.SetResult(&mockConfig, nil) + result3 := &types.BatchReadResult{ReadName: consts.MethodNameOffRampGetStaticConfig} + result3.SetResult(&offRampStaticChainConfig{}, nil) + result4 := &types.BatchReadResult{ReadName: consts.MethodNameOffRampGetDynamicConfig} + result4.SetResult(&offRampDynamicChainConfig{}, nil) + result5 := &types.BatchReadResult{ReadName: consts.MethodNameOffRampGetAllSourceChainConfigs} + result5.SetResult(&selectorsAndConfigs{}, nil) + + responses := types.BatchGetLatestValuesResult{ + types.BoundContract{Name: consts.ContractNameOffRamp}: { + *result1, *result2, *result3, *result4, *result5, + }, + } + + // Setup expected number of calls + expectedCalls := 1 + if tc.expectRefresh { + expectedCalls = 2 + } + + reader.On("ExtendedBatchGetLatestValues", + mock.Anything, + mock.Anything, + true, + ).Return(responses, []string{}, nil).Times(expectedCalls) + + // First call + _, err := cache.GetChainConfig(ctx, chainA) + require.NoError(t, err) + + // Wait + time.Sleep(tc.sleepTime) + + // Second call + _, err = cache.GetChainConfig(ctx, chainA) + require.NoError(t, err) + + // Verify number of calls + reader.AssertNumberOfCalls(t, "ExtendedBatchGetLatestValues", expectedCalls) + }) + } +} diff --git a/pkg/reader/ccip.go b/pkg/reader/ccip.go index 7688c8475..80175edbc 100644 --- a/pkg/reader/ccip.go +++ b/pkg/reader/ccip.go @@ -36,35 +36,6 @@ import ( // Default refresh period for cache if not specified const defaultRefreshPeriod = 30 * time.Second -// ChainConfigSnapshot represents the complete configuration state of the chain -type ChainConfigSnapshot struct { - Offramp OfframpConfig - RMNProxy RMNProxyConfig - RMNRemote RMNRemoteConfig - FeeQuoter FeeQuoterConfig -} - -type FeeQuoterConfig struct { - StaticConfig feeQuoterStaticConfig -} - -type RMNRemoteConfig struct { - DigestHeader rmnDigestHeader - VersionedConfig versionedConfig -} - -type OfframpConfig struct { - CommitLatestOCRConfig OCRConfigResponse - ExecLatestOCRConfig OCRConfigResponse - StaticConfig offRampStaticChainConfig - DynamicConfig offRampDynamicChainConfig - SelectorsAndConf selectorsAndConfigs -} - -type RMNProxyConfig struct { - RemoteAddress []byte -} - // TODO: unit test the implementation when the actual contract reader and writer interfaces are finalized and mocks // can be generated. type ccipChainReader struct { @@ -74,7 +45,7 @@ type ccipChainReader struct { destChain cciptypes.ChainSelector offrampAddress string extraDataCodec cciptypes.ExtraDataCodec - cache *configCache + cache ConfigCache } func newCCIPChainReaderInternal( @@ -98,9 +69,11 @@ func newCCIPChainReaderInternal( destChain: destChain, offrampAddress: typeconv.AddressBytesToString(offrampAddress, uint64(destChain)), extraDataCodec: extraDataCodec, - cache: newConfigCache(defaultRefreshPeriod), } + // Initialize cache with readers + reader.cache = newConfigCache(lggr, crs, defaultRefreshPeriod) + contracts := ContractAddresses{ consts.ContractNameOffRamp: { destChain: offrampAddress, @@ -175,289 +148,6 @@ type ConfigInfo struct { // --------------------------------------------------- -// prepareBatchRequests creates the batch request for all configurations -func (r *ccipChainReader) prepareBatchRequests() contractreader.ExtendedBatchGetLatestValuesRequest { - var ( - commitLatestOCRConfig OCRConfigResponse - execLatestOCRConfig OCRConfigResponse - staticConfig offRampStaticChainConfig - dynamicConfig offRampDynamicChainConfig - selectorsAndConf selectorsAndConfigs - rmnRemoteAddress []byte - rmnDigestHeader rmnDigestHeader - rmnVersionConfig versionedConfig - feeQuoterConfig feeQuoterStaticConfig - ) - - return contractreader.ExtendedBatchGetLatestValuesRequest{ - consts.ContractNameOffRamp: { - { - ReadName: consts.MethodNameOffRampLatestConfigDetails, - Params: map[string]any{ - "ocrPluginType": consts.PluginTypeCommit, - }, - ReturnVal: &commitLatestOCRConfig, - }, - { - ReadName: consts.MethodNameOffRampLatestConfigDetails, - Params: map[string]any{ - "ocrPluginType": consts.PluginTypeExecute, - }, - ReturnVal: &execLatestOCRConfig, - }, - { - ReadName: consts.MethodNameOffRampGetStaticConfig, - Params: map[string]any{}, - ReturnVal: &staticConfig, - }, - { - ReadName: consts.MethodNameOffRampGetDynamicConfig, - Params: map[string]any{}, - ReturnVal: &dynamicConfig, - }, - { - ReadName: consts.MethodNameOffRampGetAllSourceChainConfigs, - Params: map[string]any{}, - ReturnVal: &selectorsAndConf, - }, - }, - consts.ContractNameRMNProxy: {{ - ReadName: consts.MethodNameGetARM, - Params: map[string]any{}, - ReturnVal: &rmnRemoteAddress, - }}, - consts.ContractNameRMNRemote: { - { - ReadName: consts.MethodNameGetReportDigestHeader, - Params: map[string]any{}, - ReturnVal: &rmnDigestHeader, - }, - { - ReadName: consts.MethodNameGetVersionedConfig, - Params: map[string]any{}, - ReturnVal: &rmnVersionConfig, - }, - }, - consts.ContractNameFeeQuoter: {{ - ReadName: consts.MethodNameFeeQuoterGetStaticConfig, - Params: map[string]any{}, - ReturnVal: &feeQuoterConfig, - }}, - } -} - -// fetchChainConfig fetches the latest configuration for a specific chain -func (r *ccipChainReader) fetchChainConfig( - ctx context.Context, - chainSel cciptypes.ChainSelector) (ChainConfigSnapshot, error) { - reader, exists := r.contractReaders[chainSel] - if !exists { - return ChainConfigSnapshot{}, fmt.Errorf("no contract reader for chain %d", chainSel) - } - - requests := r.prepareBatchRequests() - batchResult, skipped, err := reader.ExtendedBatchGetLatestValues(ctx, requests, true) - if err != nil { - return ChainConfigSnapshot{}, fmt.Errorf("batch get latest values for chain %d: %w", chainSel, err) - } - - if len(skipped) > 0 { - r.lggr.Infow("some contracts were skipped due to no bindings", - "chain", chainSel, - "contracts", skipped) - } - - return r.updateFromResults(batchResult) -} - -func (r *ccipChainReader) updateFromResults(batchResult types.BatchGetLatestValuesResult) (ChainConfigSnapshot, error) { - config := ChainConfigSnapshot{} - - for contract, results := range batchResult { - var err error - switch contract.Name { - case consts.ContractNameOffRamp: - config.Offramp, err = r.processOfframpResults(results) - if err != nil { - return ChainConfigSnapshot{}, fmt.Errorf("process offramp results: %w", err) - } - - case consts.ContractNameRMNProxy: - config.RMNProxy, err = r.processRMNProxyResults(results) - if err != nil { - return ChainConfigSnapshot{}, fmt.Errorf("process RMN proxy results: %w", err) - } - - case consts.ContractNameRMNRemote: - config.RMNRemote, err = r.processRMNRemoteResults(results) - if err != nil { - return ChainConfigSnapshot{}, fmt.Errorf("process RMN remote results: %w", err) - } - - case consts.ContractNameFeeQuoter: - config.FeeQuoter, err = r.processFeeQuoterResults(results) - if err != nil { - return ChainConfigSnapshot{}, fmt.Errorf("process fee quoter results: %w", err) - } - - default: - r.lggr.Warnw("Unhandled contract in batch results", "contract", contract.Name) - } - } - - return config, nil -} - -// resultProcessor defines a function type for processing individual results -type resultProcessor func(interface{}) error - -func (r *ccipChainReader) processOfframpResults( - results []types.BatchReadResult) (OfframpConfig, error) { - - if len(results) != 5 { - return OfframpConfig{}, fmt.Errorf("expected 5 offramp results, got %d", len(results)) - } - - config := OfframpConfig{} - - // Define processors for each expected result - processors := []resultProcessor{ - // CommitLatestOCRConfig - func(val interface{}) error { - typed, ok := val.(*OCRConfigResponse) - if !ok { - return fmt.Errorf("invalid type for CommitLatestOCRConfig: %T", val) - } - config.CommitLatestOCRConfig = *typed - return nil - }, - // ExecLatestOCRConfig - func(val interface{}) error { - typed, ok := val.(*OCRConfigResponse) - if !ok { - return fmt.Errorf("invalid type for ExecLatestOCRConfig: %T", val) - } - config.ExecLatestOCRConfig = *typed - return nil - }, - // StaticConfig - func(val interface{}) error { - typed, ok := val.(*offRampStaticChainConfig) - if !ok { - return fmt.Errorf("invalid type for StaticConfig: %T", val) - } - config.StaticConfig = *typed - return nil - }, - // DynamicConfig - func(val interface{}) error { - typed, ok := val.(*offRampDynamicChainConfig) - if !ok { - return fmt.Errorf("invalid type for DynamicConfig: %T", val) - } - config.DynamicConfig = *typed - return nil - }, - // SelectorsAndConf - func(val interface{}) error { - typed, ok := val.(*selectorsAndConfigs) - if !ok { - return fmt.Errorf("invalid type for SelectorsAndConf: %T", val) - } - config.SelectorsAndConf = *typed - return nil - }, - } - - // Process each result with its corresponding processor - for i, result := range results { - val, err := result.GetResult() - if err != nil { - return OfframpConfig{}, fmt.Errorf("get offramp result %d: %w", i, err) - } - - if err := processors[i](val); err != nil { - return OfframpConfig{}, fmt.Errorf("process result %d: %w", i, err) - } - } - - return config, nil -} - -func (r *ccipChainReader) processRMNProxyResults(results []types.BatchReadResult) (RMNProxyConfig, error) { - if len(results) != 1 { - return RMNProxyConfig{}, fmt.Errorf("expected 1 RMN proxy result, got %d", len(results)) - } - - val, err := results[0].GetResult() - if err != nil { - return RMNProxyConfig{}, fmt.Errorf("get RMN proxy result: %w", err) - } - - if bytes, ok := val.(*[]byte); ok { - return RMNProxyConfig{ - RemoteAddress: *bytes, - }, nil - } - - return RMNProxyConfig{}, fmt.Errorf("invalid type for RMN proxy remote address: %T", val) -} - -func (r *ccipChainReader) processRMNRemoteResults(results []types.BatchReadResult) (RMNRemoteConfig, error) { - config := RMNRemoteConfig{} - - if len(results) != 2 { - return RMNRemoteConfig{}, fmt.Errorf("expected 2 RMN remote results, got %d", len(results)) - } - - // Process DigestHeader - val, err := results[0].GetResult() - if err != nil { - return RMNRemoteConfig{}, fmt.Errorf("get RMN remote digest header result: %w", err) - } - - typed, ok := val.(*rmnDigestHeader) - if !ok { - return RMNRemoteConfig{}, fmt.Errorf("invalid type for RMN remote digest header: %T", val) - } - config.DigestHeader = *typed - - // Process VersionedConfig - val, err = results[1].GetResult() - if err != nil { - return RMNRemoteConfig{}, fmt.Errorf("get RMN remote versioned config result: %w", err) - } - - vconf, ok := val.(*versionedConfig) - if !ok { - return RMNRemoteConfig{}, fmt.Errorf("invalid type for RMN remote versioned config: %T", val) - } - config.VersionedConfig = *vconf - - return config, nil -} - -func (r *ccipChainReader) processFeeQuoterResults(results []types.BatchReadResult) (FeeQuoterConfig, error) { - if len(results) != 1 { - return FeeQuoterConfig{}, fmt.Errorf("expected 1 fee quoter result, got %d", len(results)) - } - - val, err := results[0].GetResult() - if err != nil { - return FeeQuoterConfig{}, fmt.Errorf("get fee quoter result: %w", err) - } - - if typed, ok := val.(*feeQuoterStaticConfig); ok { - return FeeQuoterConfig{ - StaticConfig: *typed, - }, nil - } - - return FeeQuoterConfig{}, fmt.Errorf("invalid type for fee quoter static config: %T", val) -} - -// --------------------------------------------------- - func (r *ccipChainReader) CommitReportsGTETimestamp( ctx context.Context, ts time.Time, limit int, ) ([]plugintypes2.CommitPluginReportWithMeta, error) { @@ -1065,7 +755,7 @@ func (r *ccipChainReader) buildSigners(signers []signer) []rmntypes.RemoteSigner func (r *ccipChainReader) GetRMNRemoteConfig(ctx context.Context) (rmntypes.RemoteConfig, error) { lggr := logutil.WithContextValues(ctx, r.lggr) - config, err := r.getChainConfig(ctx, r.destChain) + config, err := r.cache.GetChainConfig(ctx, r.destChain) if err != nil { return rmntypes.RemoteConfig{}, fmt.Errorf("get chain config: %w", err) } @@ -1163,8 +853,10 @@ func chainSelectorToBytes16(chainSel cciptypes.ChainSelector) [16]byte { func (r *ccipChainReader) discoverOffRampContracts( ctx context.Context, ) (ContractAddresses, error) { + lggr := logutil.WithContextValues(ctx, r.lggr) + // Get from cache - config, err := r.getChainConfig(ctx, r.destChain) + config, err := r.cache.GetChainConfig(ctx, r.destChain) if err != nil { return nil, fmt.Errorf("get chain config: %w", err) } @@ -1180,16 +872,32 @@ func (r *ccipChainReader) discoverOffRampContracts( resp = resp.Append(consts.ContractNameOnRamp, cciptypes.ChainSelector(chainSel), cfg.OnRamp) if len(resp[consts.ContractNameRouter][r.destChain]) == 0 { + lggr.Infow("appending router contract address", + "address", hex.EncodeToString(cfg.Router), + "chain", r.destChain) resp = resp.Append(consts.ContractNameRouter, r.destChain, cfg.Router) } } // Add static config contracts - resp = resp.Append(consts.ContractNameNonceManager, r.destChain, config.Offramp.StaticConfig.NonceManager) - resp = resp.Append(consts.ContractNameRMNRemote, r.destChain, config.Offramp.StaticConfig.RmnRemote) + if len(config.Offramp.StaticConfig.RmnRemote) > 0 { + lggr.Infow("appending RMN remote contract address", + "address", hex.EncodeToString(config.Offramp.StaticConfig.RmnRemote), + "chain", r.destChain) + resp = resp.Append(consts.ContractNameRMNRemote, r.destChain, config.Offramp.StaticConfig.RmnRemote) + } + + if len(config.Offramp.StaticConfig.NonceManager) > 0 { + resp = resp.Append(consts.ContractNameNonceManager, r.destChain, config.Offramp.StaticConfig.NonceManager) + } // Add dynamic config contracts - resp = resp.Append(consts.ContractNameFeeQuoter, r.destChain, config.Offramp.DynamicConfig.FeeQuoter) + if len(config.Offramp.DynamicConfig.FeeQuoter) > 0 { + lggr.Infow("appending fee quoter contract address", + "address", hex.EncodeToString(config.Offramp.DynamicConfig.FeeQuoter), + "chain", r.destChain) + resp = resp.Append(consts.ContractNameFeeQuoter, r.destChain, config.Offramp.DynamicConfig.FeeQuoter) + } return resp, nil } @@ -1327,7 +1035,7 @@ type feeQuoterStaticConfig struct { // getDestFeeQuoterStaticConfig returns the destination chain's Fee Quoter's StaticConfig func (r *ccipChainReader) getDestFeeQuoterStaticConfig(ctx context.Context) (feeQuoterStaticConfig, error) { // Get from cache - config, err := r.getChainConfig(ctx, r.destChain) + config, err := r.cache.GetChainConfig(ctx, r.destChain) if err != nil { return feeQuoterStaticConfig{}, fmt.Errorf("get chain config: %w", err) } @@ -1674,7 +1382,7 @@ func (r *ccipChainReader) getRMNRemoteAddress( } // Get the address from cache instead of making a contract call - config, err := r.getChainConfig(ctx, chain) + config, err := r.cache.GetChainConfig(ctx, chain) if err != nil { return nil, fmt.Errorf("get chain config: %w", err) } @@ -1773,7 +1481,7 @@ func (r *ccipChainReader) GetLatestPriceSeqNr(ctx context.Context) (uint64, erro } func (r *ccipChainReader) GetOffRampConfigDigest(ctx context.Context, pluginType uint8) ([32]byte, error) { - config, err := r.getChainConfig(ctx, r.destChain) + config, err := r.cache.GetChainConfig(ctx, r.destChain) if err != nil { return [32]byte{}, fmt.Errorf("get chain config: %w", err) } diff --git a/pkg/reader/ccip_interface.go b/pkg/reader/ccip_interface.go index 4a8fb3d90..c559a0ecb 100644 --- a/pkg/reader/ccip_interface.go +++ b/pkg/reader/ccip_interface.go @@ -25,6 +25,35 @@ var ( // Currently only one contract per chain per name is supported. type ContractAddresses map[string]map[cciptypes.ChainSelector]cciptypes.UnknownAddress +// ChainConfigSnapshot represents the complete configuration state of the chain +type ChainConfigSnapshot struct { + Offramp OfframpConfig + RMNProxy RMNProxyConfig + RMNRemote RMNRemoteConfig + FeeQuoter FeeQuoterConfig +} + +type FeeQuoterConfig struct { + StaticConfig feeQuoterStaticConfig +} + +type RMNRemoteConfig struct { + DigestHeader rmnDigestHeader + VersionedConfig versionedConfig +} + +type OfframpConfig struct { + CommitLatestOCRConfig OCRConfigResponse + ExecLatestOCRConfig OCRConfigResponse + StaticConfig offRampStaticChainConfig + DynamicConfig offRampDynamicChainConfig + SelectorsAndConf selectorsAndConfigs +} + +type RMNProxyConfig struct { + RemoteAddress []byte +} + func (ca ContractAddresses) Append(contract string, chain cciptypes.ChainSelector, address []byte) ContractAddresses { resp := ca if resp == nil { diff --git a/pkg/reader/ccip_test.go b/pkg/reader/ccip_test.go index 0e6fe185a..52eadbc11 100644 --- a/pkg/reader/ccip_test.go +++ b/pkg/reader/ccip_test.go @@ -432,12 +432,9 @@ func TestCCIPChainReader_DiscoverContracts_HappyPath_Round1(t *testing.T) { destRMNRemote := []byte{0x4} destFeeQuoter := []byte{0x5} destRouter := []byte{0x6} - //srcRouters := []byte{0x7, 0x8} - //srcFeeQuoters := [2][]byte{{0x7}, {0x8}} // Build expected addresses. var expectedContractAddresses ContractAddresses - // Source FeeQuoter's and destRouter are missing. for i := range onramps { expectedContractAddresses = expectedContractAddresses.Append( consts.ContractNameOnRamp, sourceChain[i], onramps[i]) @@ -448,33 +445,37 @@ func TestCCIPChainReader_DiscoverContracts_HappyPath_Round1(t *testing.T) { expectedContractAddresses = expectedContractAddresses.Append(consts.ContractNameNonceManager, destChain, destNonceMgr) mockReaders := make(map[cciptypes.ChainSelector]*reader_mocks.MockExtended) - mockReaders[destChain] = reader_mocks.NewMockExtended(t) - addDestinationContractAssertions(mockReaders[destChain], destNonceMgr, destRMNRemote, destFeeQuoter) - mockReaders[destChain].EXPECT().ExtendedGetLatestValue( - mock.Anything, - consts.ContractNameOffRamp, - consts.MethodNameOffRampGetAllSourceChainConfigs, - primitives.Unconfirmed, - map[string]any{}, - mock.Anything, - ).Return(nil).Run(withReturnValueOverridden(func(returnVal interface{}) { - v := returnVal.(*selectorsAndConfigs) - v.Selectors = []uint64{uint64(sourceChain[0]), uint64(sourceChain[1])} - v.SourceChainConfigs = []sourceChainConfig{ - { - OnRamp: onramps[0], - Router: destRouter, - IsEnabled: true, + // Setup cache mock and configuration + mockCache := new(mockConfigCache) + chainConfig := ChainConfigSnapshot{ + Offramp: OfframpConfig{ + StaticConfig: offRampStaticChainConfig{ + NonceManager: destNonceMgr, + RmnRemote: destRMNRemote, }, - { - OnRamp: onramps[1], - Router: destRouter, - IsEnabled: true, + DynamicConfig: offRampDynamicChainConfig{ + FeeQuoter: destFeeQuoter, }, - } - })) + SelectorsAndConf: selectorsAndConfigs{ + Selectors: []uint64{uint64(sourceChain[0]), uint64(sourceChain[1])}, + SourceChainConfigs: []sourceChainConfig{ + { + OnRamp: onramps[0], + Router: destRouter, + IsEnabled: true, + }, + { + OnRamp: onramps[1], + Router: destRouter, + IsEnabled: true, + }, + }, + }, + }, + } + mockCache.On("GetChainConfig", mock.Anything, destChain).Return(chainConfig, nil) // mock calls to get fee quoter from onramps and source chain config from offramp. for _, selector := range sourceChain { @@ -488,7 +489,10 @@ func TestCCIPChainReader_DiscoverContracts_HappyPath_Round1(t *testing.T) { primitives.Unconfirmed, map[string]any{}, mock.Anything, - ).Return(contractreader.ErrNoBindings) + ).Return(contractreader.ErrNoBindings).Run(withReturnValueOverridden(func(returnVal interface{}) { + v := returnVal.(*getOnRampDynamicConfigResponse) + v.DynamicConfig = onRampDynamicConfig{} + })) mockReaders[selector].EXPECT().ExtendedGetLatestValue( mock.Anything, @@ -508,11 +512,13 @@ func TestCCIPChainReader_DiscoverContracts_HappyPath_Round1(t *testing.T) { } lggr, hook := logger.TestObserved(t, zapcore.InfoLevel) - // create the reader + + // create the reader with cache ccipChainReader := &ccipChainReader{ destChain: destChain, contractReaders: castToExtended, lggr: lggr, + cache: mockCache, } contractAddresses, err := ccipChainReader.DiscoverContracts(ctx) @@ -556,6 +562,8 @@ func TestCCIPChainReader_DiscoverContracts_HappyPath_Round1(t *testing.T) { "unable to lookup source routers (onRamp dest chain config), this is expected during initialization", hook.All()[6].Message, ) + + mockCache.AssertExpectations(t) } // The round2 version includes calls to the onRamp contracts. @@ -591,33 +599,37 @@ func TestCCIPChainReader_DiscoverContracts_HappyPath_Round2(t *testing.T) { expectedContractAddresses = expectedContractAddresses.Append(consts.ContractNameRouter, destChain, destRouter[0]) mockReaders := make(map[cciptypes.ChainSelector]*reader_mocks.MockExtended) - mockReaders[destChain] = reader_mocks.NewMockExtended(t) - addDestinationContractAssertions(mockReaders[destChain], destNonceMgr, destRMNRemote, destFeeQuoter) - mockReaders[destChain].EXPECT().ExtendedGetLatestValue( - mock.Anything, - consts.ContractNameOffRamp, - consts.MethodNameOffRampGetAllSourceChainConfigs, - primitives.Unconfirmed, - map[string]any{}, - mock.Anything, - ).Return(nil).Run(withReturnValueOverridden(func(returnVal interface{}) { - v := returnVal.(*selectorsAndConfigs) - v.Selectors = []uint64{uint64(sourceChain[0]), uint64(sourceChain[1])} - v.SourceChainConfigs = []sourceChainConfig{ - { - OnRamp: onramps[0], - Router: destRouter[0], - IsEnabled: true, + // Setup cache mock and configuration + mockCache := new(mockConfigCache) + chainConfig := ChainConfigSnapshot{ + Offramp: OfframpConfig{ + StaticConfig: offRampStaticChainConfig{ + NonceManager: destNonceMgr, + RmnRemote: destRMNRemote, }, - { - OnRamp: onramps[1], - Router: destRouter[1], - IsEnabled: true, + DynamicConfig: offRampDynamicChainConfig{ + FeeQuoter: destFeeQuoter, }, - } - })) + SelectorsAndConf: selectorsAndConfigs{ + Selectors: []uint64{uint64(sourceChain[0]), uint64(sourceChain[1])}, + SourceChainConfigs: []sourceChainConfig{ + { + OnRamp: onramps[0], + Router: destRouter[0], + IsEnabled: true, + }, + { + OnRamp: onramps[1], + Router: destRouter[1], + IsEnabled: true, + }, + }, + }, + }, + } + mockCache.On("GetChainConfig", mock.Anything, destChain).Return(chainConfig, nil) // mock calls to get fee quoter from onramps and source chain config from offramp. for i, selector := range sourceChain { @@ -655,17 +667,18 @@ func TestCCIPChainReader_DiscoverContracts_HappyPath_Round2(t *testing.T) { castToExtended[sel] = v } - // create the reader + // create the reader with cache ccipChainReader := &ccipChainReader{ destChain: destChain, contractReaders: castToExtended, lggr: logger.Test(t), + cache: mockCache, } contractAddresses, err := ccipChainReader.DiscoverContracts(ctx) require.NoError(t, err) - require.Equal(t, expectedContractAddresses, contractAddresses) + mockCache.AssertExpectations(t) } func TestCCIPChainReader_DiscoverContracts_GetAllSourceChainConfig_Errors(t *testing.T) { @@ -673,38 +686,31 @@ func TestCCIPChainReader_DiscoverContracts_GetAllSourceChainConfig_Errors(t *tes destChain := cciptypes.ChainSelector(1) sourceChain1 := cciptypes.ChainSelector(2) sourceChain2 := cciptypes.ChainSelector(3) - destExtended := reader_mocks.NewMockExtended(t) - // mock the call for sourceChain2 - failure + // Setup mock cache to return an error getLatestValueErr := errors.New("some error") - destExtended.EXPECT().ExtendedGetLatestValue( - mock.Anything, - consts.ContractNameOffRamp, - consts.MethodNameOffRampGetAllSourceChainConfigs, - primitives.Unconfirmed, - map[string]any{}, - mock.Anything, - ).Return(getLatestValueErr) + mockCache := new(mockConfigCache) + mockCache.On("GetChainConfig", mock.Anything, destChain).Return(ChainConfigSnapshot{}, getLatestValueErr) - // get static config call won't occur because the source chain config call failed. - - // create the reader + // create the reader with cache ccipChainReader := &ccipChainReader{ destChain: destChain, contractReaders: map[cciptypes.ChainSelector]contractreader.Extended{ - destChain: destExtended, + destChain: reader_mocks.NewMockExtended(t), // these won't be used in this test, but are needed because // we determine the source chain selectors to query from the chains // that we have readers for. sourceChain1: reader_mocks.NewMockExtended(t), sourceChain2: reader_mocks.NewMockExtended(t), }, - lggr: logger.Test(t), + lggr: logger.Test(t), + cache: mockCache, } _, err := ccipChainReader.DiscoverContracts(ctx) require.Error(t, err) require.ErrorIs(t, err, getLatestValueErr) + mockCache.AssertExpectations(t) } func TestCCIPChainReader_DiscoverContracts_GetOfframpStaticConfig_Errors(t *testing.T) { @@ -712,45 +718,31 @@ func TestCCIPChainReader_DiscoverContracts_GetOfframpStaticConfig_Errors(t *test destChain := cciptypes.ChainSelector(1) sourceChain1 := cciptypes.ChainSelector(2) sourceChain2 := cciptypes.ChainSelector(3) - destExtended := reader_mocks.NewMockExtended(t) - // mock the call for source chain configs - destExtended.EXPECT().ExtendedGetLatestValue( - mock.Anything, - consts.ContractNameOffRamp, - consts.MethodNameOffRampGetAllSourceChainConfigs, - primitives.Unconfirmed, - map[string]any{}, - mock.Anything, - ).Return(nil) // doesn't matter for this test - // mock the call to get the nonce manager - failure + // Setup mock cache to return a config with missing static config data getLatestValueErr := errors.New("some error") - destExtended.EXPECT().ExtendedGetLatestValue( - mock.Anything, - consts.ContractNameOffRamp, - consts.MethodNameOffRampGetStaticConfig, - primitives.Unconfirmed, - map[string]any{}, - mock.Anything, - ).Return(getLatestValueErr) + mockCache := new(mockConfigCache) + mockCache.On("GetChainConfig", mock.Anything, destChain).Return(ChainConfigSnapshot{}, getLatestValueErr) - // create the reader + // create the reader with cache ccipChainReader := &ccipChainReader{ destChain: destChain, contractReaders: map[cciptypes.ChainSelector]contractreader.Extended{ - destChain: destExtended, + destChain: reader_mocks.NewMockExtended(t), // these won't be used in this test, but are needed because // we determine the source chain selectors to query from the chains // that we have readers for. sourceChain1: reader_mocks.NewMockExtended(t), sourceChain2: reader_mocks.NewMockExtended(t), }, - lggr: logger.Test(t), + lggr: logger.Test(t), + cache: mockCache, } _, err := ccipChainReader.DiscoverContracts(ctx) require.Error(t, err) require.ErrorIs(t, err, getLatestValueErr) + mockCache.AssertExpectations(t) } // withReturnValueOverridden is a helper function to override the return value of a mocked out @@ -772,49 +764,40 @@ func withReturnValueOverridden(mapper func(returnVal interface{})) func(ctx cont } func TestCCIPChainReader_getDestFeeQuoterStaticConfig(t *testing.T) { - destCR := reader_mocks.NewMockContractReaderFacade(t) - destCR.EXPECT().Bind(mock.Anything, mock.Anything).Return(nil) - destCR.EXPECT().HealthReport().Return(nil) - destCR.EXPECT().GetLatestValue( - mock.Anything, - mock.Anything, - mock.Anything, - mock.Anything, - mock.Anything, - ).Run(func( - ctx context.Context, - readIdentifier string, - confidenceLevel primitives.ConfidenceLevel, - params interface{}, - returnVal interface{}, - ) { - cfg := returnVal.(*feeQuoterStaticConfig) - cfg.MaxFeeJuelsPerMsg = cciptypes.NewBigIntFromInt64(10) - cfg.LinkToken = []byte{0x3, 0x4} - cfg.StalenessThreshold = 12 - }).Return(nil) + ctx := context.Background() + // Setup expected values offrampAddress := []byte{0x3} - feeQuoterAddress := []byte{0x4} - ccipReader := newCCIPChainReaderInternal( - tests.Context(t), - logger.Test(t), - map[cciptypes.ChainSelector]contractreader.ContractReaderFacade{ - chainC: destCR, - }, nil, chainC, offrampAddress, - ccipocr3.NewMockExtraDataCodec(t), - ) + expectedConfig := feeQuoterStaticConfig{ + MaxFeeJuelsPerMsg: cciptypes.NewBigIntFromInt64(10), + LinkToken: []byte{0x3, 0x4}, + StalenessThreshold: 12, + } - require.NoError(t, ccipReader.contractReaders[chainC].Bind( - context.Background(), []types.BoundContract{{Name: "FeeQuoter", - Address: typeconv.AddressBytesToString(feeQuoterAddress, 111_111)}})) + // Setup cache with the expected config + mockCache := new(mockConfigCache) + chainConfig := ChainConfigSnapshot{ + FeeQuoter: FeeQuoterConfig{ + StaticConfig: expectedConfig, + }, + } + mockCache.On("GetChainConfig", mock.Anything, chainC).Return(chainConfig, nil) + + ccipReader := &ccipChainReader{ + lggr: logger.Test(t), + destChain: chainC, + cache: mockCache, + offrampAddress: typeconv.AddressBytesToString(offrampAddress, uint64(chainC)), + } - ctx := context.Background() cfg, err := ccipReader.getDestFeeQuoterStaticConfig(ctx) - assert.NoError(t, err) - assert.Equal(t, cciptypes.NewBigIntFromInt64(10), cfg.MaxFeeJuelsPerMsg) - assert.Equal(t, []byte{0x3, 0x4}, cfg.LinkToken) - assert.Equal(t, uint32(12), cfg.StalenessThreshold) + require.NoError(t, err) + + assert.Equal(t, expectedConfig.MaxFeeJuelsPerMsg, cfg.MaxFeeJuelsPerMsg) + assert.Equal(t, expectedConfig.LinkToken, cfg.LinkToken) + assert.Equal(t, expectedConfig.StalenessThreshold, cfg.StalenessThreshold) + + mockCache.AssertExpectations(t) } func TestCCIPChainReader_getFeeQuoterTokenPriceUSD(t *testing.T) { @@ -921,25 +904,27 @@ func TestCCIPFeeComponents_NotFoundErrors(t *testing.T) { } func TestCCIPChainReader_LinkPriceUSD(t *testing.T) { + ctx := context.Background() tokenAddr := []byte{0x3, 0x4} - destCR := reader_mocks.NewMockExtended(t) - destCR.EXPECT().Bind(mock.Anything, mock.Anything).Return(nil) + offrampAddress := []byte{0x3} - destCR.EXPECT().ExtendedGetLatestValue( - mock.Anything, - consts.ContractNameFeeQuoter, - consts.MethodNameFeeQuoterGetStaticConfig, - primitives.Unconfirmed, - map[string]any{}, - mock.Anything, - ).Return(nil).Run(withReturnValueOverridden(func(returnVal interface{}) { - cfg := returnVal.(*feeQuoterStaticConfig) - cfg.MaxFeeJuelsPerMsg = cciptypes.NewBigIntFromInt64(10) - cfg.LinkToken = []byte{0x3, 0x4} - cfg.StalenessThreshold = 12 - })) + // Setup mock cache with the fee quoter static config + mockCache := new(mockConfigCache) + chainConfig := ChainConfigSnapshot{ + FeeQuoter: FeeQuoterConfig{ + StaticConfig: feeQuoterStaticConfig{ + MaxFeeJuelsPerMsg: cciptypes.NewBigIntFromInt64(10), + LinkToken: tokenAddr, + StalenessThreshold: 12, + }, + }, + } + mockCache.On("GetChainConfig", mock.Anything, chainC).Return(chainConfig, nil) - // mock the call to get the fee quoter + // Setup contract reader for getting token price + destCR := reader_mocks.NewMockExtended(t) + + // mock the call to get the token price destCR.EXPECT().ExtendedGetLatestValue( mock.Anything, consts.ContractNameFeeQuoter, @@ -952,30 +937,22 @@ func TestCCIPChainReader_LinkPriceUSD(t *testing.T) { price.Value = big.NewInt(145) })) - offrampAddress := []byte{0x3} - feeQuoterAddress := []byte{0x4} - contractReaders := make(map[cciptypes.ChainSelector]contractreader.Extended) - contractReaders[chainC] = destCR - ccipReader := newCCIPChainReaderInternal( - context.Background(), - logger.Test(t), - map[cciptypes.ChainSelector]contractreader.ContractReaderFacade{ + // Setup ccipReader with both cache and contract readers + ccipReader := &ccipChainReader{ + lggr: logger.Test(t), + destChain: chainC, + cache: mockCache, + offrampAddress: typeconv.AddressBytesToString(offrampAddress, uint64(chainC)), + contractReaders: map[cciptypes.ChainSelector]contractreader.Extended{ chainC: destCR, }, - nil, - chainC, - offrampAddress, - ccipocr3.NewMockExtraDataCodec(t), - ) - - require.NoError(t, ccipReader.contractReaders[chainC].Bind( - context.Background(), []types.BoundContract{{Name: "FeeQuoter", - Address: typeconv.AddressBytesToString(feeQuoterAddress, 111_111)}})) + } - ctx := context.Background() price, err := ccipReader.LinkPriceUSD(ctx) - assert.NoError(t, err) + require.NoError(t, err) assert.Equal(t, cciptypes.NewBigIntFromInt64(145), price) + + mockCache.AssertExpectations(t) } func TestCCIPChainReader_GetMedianDataAvailabilityGasConfig(t *testing.T) { @@ -1334,3 +1311,26 @@ func TestCCIPChainReader_Nonces(t *testing.T) { addr2: 10, }, nonces) } + +// MockConfigCache is an autogenerated mock type for the ConfigCache type +type MockConfigCache struct { + mock.Mock +} + +type mockConfigCache struct { + mock.Mock +} + +func (m *mockConfigCache) GetChainConfig( + ctx context.Context, + chainSel cciptypes.ChainSelector) (ChainConfigSnapshot, error) { + args := m.Called(ctx, chainSel) + return args.Get(0).(ChainConfigSnapshot), args.Error(1) +} + +func (m *mockConfigCache) RefreshChainConfig( + ctx context.Context, + chainSel cciptypes.ChainSelector) (ChainConfigSnapshot, error) { + args := m.Called(ctx, chainSel) + return args.Get(0).(ChainConfigSnapshot), args.Error(1) +} diff --git a/pkg/reader/helpers.go b/pkg/reader/helpers.go index 3f0db35cf..91c35ca8a 100644 --- a/pkg/reader/helpers.go +++ b/pkg/reader/helpers.go @@ -124,11 +124,3 @@ func validateReaderExistence( } return nil } - -// Helper function to handle type assertions -func assertAndAssignConfig[T any](val interface{}, errMsg string) (*T, error) { - if typed, ok := val.(*T); ok { - return typed, nil - } - return nil, fmt.Errorf(errMsg, val) -} From e34321098fbe916bfc9e345652b7ffc50a80627d Mon Sep 17 00:00:00 2001 From: nogo <0xnogo@gmail.com> Date: Wed, 5 Feb 2025 17:06:09 +0400 Subject: [PATCH 13/18] lint --- pkg/reader/cache_test.go | 7 ++++--- pkg/reader/ccip_test.go | 31 ------------------------------- 2 files changed, 4 insertions(+), 34 deletions(-) diff --git a/pkg/reader/cache_test.go b/pkg/reader/cache_test.go index 5a7db984c..fe2640e67 100644 --- a/pkg/reader/cache_test.go +++ b/pkg/reader/cache_test.go @@ -9,13 +9,14 @@ import ( "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-common/pkg/types" + "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" + reader_mocks "github.com/smartcontractkit/chainlink-ccip/mocks/pkg/contractreader" "github.com/smartcontractkit/chainlink-ccip/pkg/consts" "github.com/smartcontractkit/chainlink-ccip/pkg/contractreader" cciptypes "github.com/smartcontractkit/chainlink-ccip/pkg/types/ccipocr3" - "github.com/smartcontractkit/chainlink-common/pkg/logger" - "github.com/smartcontractkit/chainlink-common/pkg/types" - "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" ) func setupBasicCache(t *testing.T) (*configCache, *reader_mocks.MockExtended) { diff --git a/pkg/reader/ccip_test.go b/pkg/reader/ccip_test.go index 52eadbc11..840fdfc1a 100644 --- a/pkg/reader/ccip_test.go +++ b/pkg/reader/ccip_test.go @@ -390,37 +390,6 @@ func TestCCIPChainReader_Sync_BindError(t *testing.T) { require.ErrorIs(t, err, expectedErr) } -func addDestinationContractAssertions( - extended *reader_mocks.MockExtended, - destNonceMgr, destRMNRemote, destFeeQuoter []byte, -) { - // mock the call to get the nonce manager - extended.EXPECT().ExtendedGetLatestValue( - mock.Anything, - consts.ContractNameOffRamp, - consts.MethodNameOffRampGetStaticConfig, - primitives.Unconfirmed, - map[string]any{}, - mock.Anything, - ).Return(nil).Run(withReturnValueOverridden(func(returnVal interface{}) { - v := returnVal.(*offRampStaticChainConfig) - v.NonceManager = destNonceMgr - v.RmnRemote = destRMNRemote - })) - // mock the call to get the fee quoter - extended.EXPECT().ExtendedGetLatestValue( - mock.Anything, - consts.ContractNameOffRamp, - consts.MethodNameOffRampGetDynamicConfig, - primitives.Unconfirmed, - map[string]any{}, - mock.Anything, - ).Return(nil).Run(withReturnValueOverridden(func(returnVal interface{}) { - v := returnVal.(*offRampDynamicChainConfig) - v.FeeQuoter = destFeeQuoter - })) -} - // The round1 version returns NoBindingFound errors for onramp contracts to simulate // the two-phase approach to discovering those contracts. func TestCCIPChainReader_DiscoverContracts_HappyPath_Round1(t *testing.T) { From eaa7b7f45aa906aadfeba1a9bd5b05646568b101 Mon Sep 17 00:00:00 2001 From: nogo <0xnogo@gmail.com> Date: Wed, 5 Feb 2025 19:08:28 +0400 Subject: [PATCH 14/18] minor --- pkg/reader/ccip.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pkg/reader/ccip.go b/pkg/reader/ccip.go index 80175edbc..fd9abbf7a 100644 --- a/pkg/reader/ccip.go +++ b/pkg/reader/ccip.go @@ -858,7 +858,7 @@ func (r *ccipChainReader) discoverOffRampContracts( // Get from cache config, err := r.cache.GetChainConfig(ctx, r.destChain) if err != nil { - return nil, fmt.Errorf("get chain config: %w", err) + return nil, fmt.Errorf("unable to lookup RMN remote address (RMN proxy): %w", err) } resp := make(ContractAddresses) @@ -1375,7 +1375,6 @@ func (r *ccipChainReader) getRMNRemoteAddress( lggr logger.Logger, chain cciptypes.ChainSelector, rmnRemoteProxyAddress []byte) ([]byte, error) { - // Still need to bind the contract before accessing it _, err := bindExtendedReaderContract(ctx, lggr, r.contractReaders, chain, consts.ContractNameRMNProxy, rmnRemoteProxyAddress) if err != nil { return nil, fmt.Errorf("bind RMN proxy contract: %w", err) From abc7d2901825e7f2db94cdcf892d79cc1ae41745 Mon Sep 17 00:00:00 2001 From: nogo <0xnogo@gmail.com> Date: Thu, 6 Feb 2025 12:25:02 +0400 Subject: [PATCH 15/18] rename to config poller --- mocks/pkg/reader/config_cache.go | 154 ------------------ pkg/reader/ccip.go | 14 +- pkg/reader/ccip_test.go | 16 +- pkg/reader/{cache.go => config_poller.go} | 40 ++--- .../{cache_test.go => config_poller_test.go} | 10 +- 5 files changed, 40 insertions(+), 194 deletions(-) delete mode 100644 mocks/pkg/reader/config_cache.go rename pkg/reader/{cache.go => config_poller.go} (90%) rename pkg/reader/{cache_test.go => config_poller_test.go} (98%) diff --git a/mocks/pkg/reader/config_cache.go b/mocks/pkg/reader/config_cache.go deleted file mode 100644 index 886a54a69..000000000 --- a/mocks/pkg/reader/config_cache.go +++ /dev/null @@ -1,154 +0,0 @@ -// Code generated by mockery v2.43.2. DO NOT EDIT. - -package reader - -import ( - context "context" - - ccipocr3 "github.com/smartcontractkit/chainlink-ccip/pkg/types/ccipocr3" - - mock "github.com/stretchr/testify/mock" - - reader "github.com/smartcontractkit/chainlink-ccip/pkg/reader" -) - -// MockConfigCache is an autogenerated mock type for the ConfigCache type -type MockConfigCache struct { - mock.Mock -} - -type MockConfigCache_Expecter struct { - mock *mock.Mock -} - -func (_m *MockConfigCache) EXPECT() *MockConfigCache_Expecter { - return &MockConfigCache_Expecter{mock: &_m.Mock} -} - -// GetChainConfig provides a mock function with given fields: ctx, chainSel -func (_m *MockConfigCache) GetChainConfig(ctx context.Context, chainSel ccipocr3.ChainSelector) (reader.ChainConfigSnapshot, error) { - ret := _m.Called(ctx, chainSel) - - if len(ret) == 0 { - panic("no return value specified for GetChainConfig") - } - - var r0 reader.ChainConfigSnapshot - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, ccipocr3.ChainSelector) (reader.ChainConfigSnapshot, error)); ok { - return rf(ctx, chainSel) - } - if rf, ok := ret.Get(0).(func(context.Context, ccipocr3.ChainSelector) reader.ChainConfigSnapshot); ok { - r0 = rf(ctx, chainSel) - } else { - r0 = ret.Get(0).(reader.ChainConfigSnapshot) - } - - if rf, ok := ret.Get(1).(func(context.Context, ccipocr3.ChainSelector) error); ok { - r1 = rf(ctx, chainSel) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// MockConfigCache_GetChainConfig_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetChainConfig' -type MockConfigCache_GetChainConfig_Call struct { - *mock.Call -} - -// GetChainConfig is a helper method to define mock.On call -// - ctx context.Context -// - chainSel ccipocr3.ChainSelector -func (_e *MockConfigCache_Expecter) GetChainConfig(ctx interface{}, chainSel interface{}) *MockConfigCache_GetChainConfig_Call { - return &MockConfigCache_GetChainConfig_Call{Call: _e.mock.On("GetChainConfig", ctx, chainSel)} -} - -func (_c *MockConfigCache_GetChainConfig_Call) Run(run func(ctx context.Context, chainSel ccipocr3.ChainSelector)) *MockConfigCache_GetChainConfig_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(ccipocr3.ChainSelector)) - }) - return _c -} - -func (_c *MockConfigCache_GetChainConfig_Call) Return(_a0 reader.ChainConfigSnapshot, _a1 error) *MockConfigCache_GetChainConfig_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *MockConfigCache_GetChainConfig_Call) RunAndReturn(run func(context.Context, ccipocr3.ChainSelector) (reader.ChainConfigSnapshot, error)) *MockConfigCache_GetChainConfig_Call { - _c.Call.Return(run) - return _c -} - -// RefreshChainConfig provides a mock function with given fields: ctx, chainSel -func (_m *MockConfigCache) RefreshChainConfig(ctx context.Context, chainSel ccipocr3.ChainSelector) (reader.ChainConfigSnapshot, error) { - ret := _m.Called(ctx, chainSel) - - if len(ret) == 0 { - panic("no return value specified for RefreshChainConfig") - } - - var r0 reader.ChainConfigSnapshot - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, ccipocr3.ChainSelector) (reader.ChainConfigSnapshot, error)); ok { - return rf(ctx, chainSel) - } - if rf, ok := ret.Get(0).(func(context.Context, ccipocr3.ChainSelector) reader.ChainConfigSnapshot); ok { - r0 = rf(ctx, chainSel) - } else { - r0 = ret.Get(0).(reader.ChainConfigSnapshot) - } - - if rf, ok := ret.Get(1).(func(context.Context, ccipocr3.ChainSelector) error); ok { - r1 = rf(ctx, chainSel) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// MockConfigCache_RefreshChainConfig_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RefreshChainConfig' -type MockConfigCache_RefreshChainConfig_Call struct { - *mock.Call -} - -// RefreshChainConfig is a helper method to define mock.On call -// - ctx context.Context -// - chainSel ccipocr3.ChainSelector -func (_e *MockConfigCache_Expecter) RefreshChainConfig(ctx interface{}, chainSel interface{}) *MockConfigCache_RefreshChainConfig_Call { - return &MockConfigCache_RefreshChainConfig_Call{Call: _e.mock.On("RefreshChainConfig", ctx, chainSel)} -} - -func (_c *MockConfigCache_RefreshChainConfig_Call) Run(run func(ctx context.Context, chainSel ccipocr3.ChainSelector)) *MockConfigCache_RefreshChainConfig_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(ccipocr3.ChainSelector)) - }) - return _c -} - -func (_c *MockConfigCache_RefreshChainConfig_Call) Return(_a0 reader.ChainConfigSnapshot, _a1 error) *MockConfigCache_RefreshChainConfig_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *MockConfigCache_RefreshChainConfig_Call) RunAndReturn(run func(context.Context, ccipocr3.ChainSelector) (reader.ChainConfigSnapshot, error)) *MockConfigCache_RefreshChainConfig_Call { - _c.Call.Return(run) - return _c -} - -// NewMockConfigCache creates a new instance of MockConfigCache. 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 NewMockConfigCache(t interface { - mock.TestingT - Cleanup(func()) -}) *MockConfigCache { - mock := &MockConfigCache{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/pkg/reader/ccip.go b/pkg/reader/ccip.go index fd9abbf7a..5df54aa04 100644 --- a/pkg/reader/ccip.go +++ b/pkg/reader/ccip.go @@ -45,7 +45,7 @@ type ccipChainReader struct { destChain cciptypes.ChainSelector offrampAddress string extraDataCodec cciptypes.ExtraDataCodec - cache ConfigCache + configPoller ConfigPoller } func newCCIPChainReaderInternal( @@ -72,7 +72,7 @@ func newCCIPChainReaderInternal( } // Initialize cache with readers - reader.cache = newConfigCache(lggr, crs, defaultRefreshPeriod) + reader.configPoller = newConfigPoller(lggr, crs, defaultRefreshPeriod) contracts := ContractAddresses{ consts.ContractNameOffRamp: { @@ -755,7 +755,7 @@ func (r *ccipChainReader) buildSigners(signers []signer) []rmntypes.RemoteSigner func (r *ccipChainReader) GetRMNRemoteConfig(ctx context.Context) (rmntypes.RemoteConfig, error) { lggr := logutil.WithContextValues(ctx, r.lggr) - config, err := r.cache.GetChainConfig(ctx, r.destChain) + config, err := r.configPoller.GetChainConfig(ctx, r.destChain) if err != nil { return rmntypes.RemoteConfig{}, fmt.Errorf("get chain config: %w", err) } @@ -856,7 +856,7 @@ func (r *ccipChainReader) discoverOffRampContracts( lggr := logutil.WithContextValues(ctx, r.lggr) // Get from cache - config, err := r.cache.GetChainConfig(ctx, r.destChain) + config, err := r.configPoller.GetChainConfig(ctx, r.destChain) if err != nil { return nil, fmt.Errorf("unable to lookup RMN remote address (RMN proxy): %w", err) } @@ -1035,7 +1035,7 @@ type feeQuoterStaticConfig struct { // getDestFeeQuoterStaticConfig returns the destination chain's Fee Quoter's StaticConfig func (r *ccipChainReader) getDestFeeQuoterStaticConfig(ctx context.Context) (feeQuoterStaticConfig, error) { // Get from cache - config, err := r.cache.GetChainConfig(ctx, r.destChain) + config, err := r.configPoller.GetChainConfig(ctx, r.destChain) if err != nil { return feeQuoterStaticConfig{}, fmt.Errorf("get chain config: %w", err) } @@ -1381,7 +1381,7 @@ func (r *ccipChainReader) getRMNRemoteAddress( } // Get the address from cache instead of making a contract call - config, err := r.cache.GetChainConfig(ctx, chain) + config, err := r.configPoller.GetChainConfig(ctx, chain) if err != nil { return nil, fmt.Errorf("get chain config: %w", err) } @@ -1480,7 +1480,7 @@ func (r *ccipChainReader) GetLatestPriceSeqNr(ctx context.Context) (uint64, erro } func (r *ccipChainReader) GetOffRampConfigDigest(ctx context.Context, pluginType uint8) ([32]byte, error) { - config, err := r.cache.GetChainConfig(ctx, r.destChain) + config, err := r.configPoller.GetChainConfig(ctx, r.destChain) if err != nil { return [32]byte{}, fmt.Errorf("get chain config: %w", err) } diff --git a/pkg/reader/ccip_test.go b/pkg/reader/ccip_test.go index 840fdfc1a..59e8207e0 100644 --- a/pkg/reader/ccip_test.go +++ b/pkg/reader/ccip_test.go @@ -487,7 +487,7 @@ func TestCCIPChainReader_DiscoverContracts_HappyPath_Round1(t *testing.T) { destChain: destChain, contractReaders: castToExtended, lggr: lggr, - cache: mockCache, + configPoller: mockCache, } contractAddresses, err := ccipChainReader.DiscoverContracts(ctx) @@ -641,7 +641,7 @@ func TestCCIPChainReader_DiscoverContracts_HappyPath_Round2(t *testing.T) { destChain: destChain, contractReaders: castToExtended, lggr: logger.Test(t), - cache: mockCache, + configPoller: mockCache, } contractAddresses, err := ccipChainReader.DiscoverContracts(ctx) @@ -672,8 +672,8 @@ func TestCCIPChainReader_DiscoverContracts_GetAllSourceChainConfig_Errors(t *tes sourceChain1: reader_mocks.NewMockExtended(t), sourceChain2: reader_mocks.NewMockExtended(t), }, - lggr: logger.Test(t), - cache: mockCache, + lggr: logger.Test(t), + configPoller: mockCache, } _, err := ccipChainReader.DiscoverContracts(ctx) @@ -704,8 +704,8 @@ func TestCCIPChainReader_DiscoverContracts_GetOfframpStaticConfig_Errors(t *test sourceChain1: reader_mocks.NewMockExtended(t), sourceChain2: reader_mocks.NewMockExtended(t), }, - lggr: logger.Test(t), - cache: mockCache, + lggr: logger.Test(t), + configPoller: mockCache, } _, err := ccipChainReader.DiscoverContracts(ctx) @@ -755,7 +755,7 @@ func TestCCIPChainReader_getDestFeeQuoterStaticConfig(t *testing.T) { ccipReader := &ccipChainReader{ lggr: logger.Test(t), destChain: chainC, - cache: mockCache, + configPoller: mockCache, offrampAddress: typeconv.AddressBytesToString(offrampAddress, uint64(chainC)), } @@ -910,7 +910,7 @@ func TestCCIPChainReader_LinkPriceUSD(t *testing.T) { ccipReader := &ccipChainReader{ lggr: logger.Test(t), destChain: chainC, - cache: mockCache, + configPoller: mockCache, offrampAddress: typeconv.AddressBytesToString(offrampAddress, uint64(chainC)), contractReaders: map[cciptypes.ChainSelector]contractreader.Extended{ chainC: destCR, diff --git a/pkg/reader/cache.go b/pkg/reader/config_poller.go similarity index 90% rename from pkg/reader/cache.go rename to pkg/reader/config_poller.go index e6f030dfb..59e095a41 100644 --- a/pkg/reader/cache.go +++ b/pkg/reader/config_poller.go @@ -14,19 +14,19 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/types" ) -// ConfigCache defines the interface for caching chain configuration data -type ConfigCache interface { +// ConfigPoller defines the interface for caching chain configuration data +type ConfigPoller interface { // GetChainConfig retrieves the cached configuration for a chain GetChainConfig(ctx context.Context, chainSel cciptypes.ChainSelector) (ChainConfigSnapshot, error) // RefreshChainConfig forces a refresh of the chain configuration RefreshChainConfig(ctx context.Context, chainSel cciptypes.ChainSelector) (ChainConfigSnapshot, error) } -// configCache handles caching of chain configuration data for multiple chains. +// configPoller handles caching of chain configuration data for multiple chains. // It is used by the ccipChainReader to store and retrieve configuration data, // avoiding unnecessary contract calls and improving performance. -// configCache handles caching of chain configuration data for multiple chains -type configCache struct { +// configPoller handles caching of chain configuration data for multiple chains +type configPoller struct { sync.RWMutex chainCaches map[cciptypes.ChainSelector]*chainCache refreshPeriod time.Duration @@ -43,13 +43,13 @@ type chainCache struct { lastRefresh time.Time } -// newConfigCache creates a new config cache instance -func newConfigCache( +// newConfigPoller creates a new config cache instance +func newConfigPoller( lggr logger.Logger, readers map[cciptypes.ChainSelector]contractreader.Extended, refreshPeriod time.Duration, -) *configCache { - return &configCache{ +) *configPoller { + return &configPoller{ chainCaches: make(map[cciptypes.ChainSelector]*chainCache), refreshPeriod: refreshPeriod, readers: readers, @@ -58,7 +58,7 @@ func newConfigCache( } // getOrCreateChainCache safely retrieves or creates a cache for a specific chain -func (c *configCache) getOrCreateChainCache(chainSel cciptypes.ChainSelector) *chainCache { +func (c *configPoller) getOrCreateChainCache(chainSel cciptypes.ChainSelector) *chainCache { c.Lock() defer c.Unlock() @@ -78,7 +78,7 @@ func (c *configCache) getOrCreateChainCache(chainSel cciptypes.ChainSelector) *c } // GetChainConfig retrieves the cached configuration for a chain -func (c *configCache) GetChainConfig( +func (c *configPoller) GetChainConfig( ctx context.Context, chainSel cciptypes.ChainSelector, ) (ChainConfigSnapshot, error) { @@ -107,7 +107,7 @@ func (c *configCache) GetChainConfig( } // RefreshChainConfig forces a refresh of the chain configuration -func (c *configCache) RefreshChainConfig( +func (c *configPoller) RefreshChainConfig( ctx context.Context, chainSel cciptypes.ChainSelector, ) (ChainConfigSnapshot, error) { @@ -156,7 +156,7 @@ func (c *configCache) RefreshChainConfig( } // prepareBatchRequests creates the batch request for all configurations -func (c *configCache) prepareBatchRequests() contractreader.ExtendedBatchGetLatestValuesRequest { +func (c *configPoller) prepareBatchRequests() contractreader.ExtendedBatchGetLatestValuesRequest { var ( commitLatestOCRConfig OCRConfigResponse execLatestOCRConfig OCRConfigResponse @@ -227,7 +227,7 @@ func (c *configCache) prepareBatchRequests() contractreader.ExtendedBatchGetLate } // fetchChainConfig fetches the latest configuration for a specific chain -func (c *configCache) fetchChainConfig( +func (c *configPoller) fetchChainConfig( ctx context.Context, chainSel cciptypes.ChainSelector) (ChainConfigSnapshot, error) { reader, exists := c.readers[chainSel] @@ -250,7 +250,7 @@ func (c *configCache) fetchChainConfig( return c.updateFromResults(batchResult) } -func (c *configCache) updateFromResults(batchResult types.BatchGetLatestValuesResult) (ChainConfigSnapshot, error) { +func (c *configPoller) updateFromResults(batchResult types.BatchGetLatestValuesResult) (ChainConfigSnapshot, error) { config := ChainConfigSnapshot{} for contract, results := range batchResult { @@ -291,7 +291,7 @@ func (c *configCache) updateFromResults(batchResult types.BatchGetLatestValuesRe // resultProcessor defines a function type for processing individual results type resultProcessor func(interface{}) error -func (c *configCache) processOfframpResults( +func (c *configPoller) processOfframpResults( results []types.BatchReadResult) (OfframpConfig, error) { if len(results) != 5 { @@ -364,7 +364,7 @@ func (c *configCache) processOfframpResults( return config, nil } -func (c *configCache) processRMNProxyResults(results []types.BatchReadResult) (RMNProxyConfig, error) { +func (c *configPoller) processRMNProxyResults(results []types.BatchReadResult) (RMNProxyConfig, error) { if len(results) != 1 { return RMNProxyConfig{}, fmt.Errorf("expected 1 RMN proxy result, got %d", len(results)) } @@ -383,7 +383,7 @@ func (c *configCache) processRMNProxyResults(results []types.BatchReadResult) (R return RMNProxyConfig{}, fmt.Errorf("invalid type for RMN proxy remote address: %T", val) } -func (c *configCache) processRMNRemoteResults(results []types.BatchReadResult) (RMNRemoteConfig, error) { +func (c *configPoller) processRMNRemoteResults(results []types.BatchReadResult) (RMNRemoteConfig, error) { config := RMNRemoteConfig{} if len(results) != 2 { @@ -417,7 +417,7 @@ func (c *configCache) processRMNRemoteResults(results []types.BatchReadResult) ( return config, nil } -func (c *configCache) processFeeQuoterResults(results []types.BatchReadResult) (FeeQuoterConfig, error) { +func (c *configPoller) processFeeQuoterResults(results []types.BatchReadResult) (FeeQuoterConfig, error) { if len(results) != 1 { return FeeQuoterConfig{}, fmt.Errorf("expected 1 fee quoter result, got %d", len(results)) } @@ -437,4 +437,4 @@ func (c *configCache) processFeeQuoterResults(results []types.BatchReadResult) ( } // Ensure configCache implements ConfigCache -var _ ConfigCache = (*configCache)(nil) +var _ ConfigPoller = (*configPoller)(nil) diff --git a/pkg/reader/cache_test.go b/pkg/reader/config_poller_test.go similarity index 98% rename from pkg/reader/cache_test.go rename to pkg/reader/config_poller_test.go index fe2640e67..3526ad091 100644 --- a/pkg/reader/cache_test.go +++ b/pkg/reader/config_poller_test.go @@ -19,12 +19,12 @@ import ( cciptypes "github.com/smartcontractkit/chainlink-ccip/pkg/types/ccipocr3" ) -func setupBasicCache(t *testing.T) (*configCache, *reader_mocks.MockExtended) { +func setupBasicCache(t *testing.T) (*configPoller, *reader_mocks.MockExtended) { reader := reader_mocks.NewMockExtended(t) readers := map[cciptypes.ChainSelector]contractreader.Extended{ chainA: reader, } - cache := newConfigCache(logger.Test(t), readers, 1*time.Second) + cache := newConfigPoller(logger.Test(t), readers, 1*time.Second) return cache, reader } @@ -347,7 +347,7 @@ func TestConfigCache_Initialization(t *testing.T) { lggr := logger.Test(t) ctx := tests.Context(t) - cache := newConfigCache(lggr, tc.readers, tc.refreshPeriod) + cache := newConfigPoller(lggr, tc.readers, tc.refreshPeriod) require.NotNil(t, cache, "cache should never be nil after initialization") // Verify the cache's internal state @@ -473,7 +473,7 @@ func TestConfigCache_MultipleChains(t *testing.T) { chainA: readerA, chainB: readerB, } - cache := newConfigCache(logger.Test(t), readers, 1*time.Second) + cache := newConfigPoller(logger.Test(t), readers, 1*time.Second) ctx := tests.Context(t) // Setup mock response for both chains @@ -556,7 +556,7 @@ func TestConfigCache_RefreshPeriod(t *testing.T) { readers := map[cciptypes.ChainSelector]contractreader.Extended{ chainA: reader, } - cache := newConfigCache(logger.Test(t), readers, tc.refreshPeriod) + cache := newConfigPoller(logger.Test(t), readers, tc.refreshPeriod) ctx := tests.Context(t) mockConfig := OCRConfigResponse{ From 025fef73a2de544754b21e237d425cf71e72f834 Mon Sep 17 00:00:00 2001 From: nogo <0xnogo@gmail.com> Date: Fri, 7 Feb 2025 11:50:36 +0400 Subject: [PATCH 16/18] rm get all configs --- pkg/reader/ccip_interface.go | 1 - pkg/reader/ccip_test.go | 30 ++++++++++++-------------- pkg/reader/config_poller.go | 15 ------------- pkg/reader/config_poller_test.go | 37 ++++++++------------------------ 4 files changed, 23 insertions(+), 60 deletions(-) diff --git a/pkg/reader/ccip_interface.go b/pkg/reader/ccip_interface.go index 85c8168eb..454ae7043 100644 --- a/pkg/reader/ccip_interface.go +++ b/pkg/reader/ccip_interface.go @@ -47,7 +47,6 @@ type OfframpConfig struct { ExecLatestOCRConfig OCRConfigResponse StaticConfig offRampStaticChainConfig DynamicConfig offRampDynamicChainConfig - SelectorsAndConf selectorsAndConfigs } type RMNProxyConfig struct { diff --git a/pkg/reader/ccip_test.go b/pkg/reader/ccip_test.go index 34ed50733..0ad1257c0 100644 --- a/pkg/reader/ccip_test.go +++ b/pkg/reader/ccip_test.go @@ -673,15 +673,24 @@ func TestCCIPChainReader_DiscoverContracts_HappyPath_Round2(t *testing.T) { func TestCCIPChainReader_DiscoverContracts_GetAllSourceChainConfig_Errors(t *testing.T) { ctx := tests.Context(t) - destExtended := reader_mocks.NewMockExtended(t) destChain := cciptypes.ChainSelector(1) sourceChain1 := cciptypes.ChainSelector(2) sourceChain2 := cciptypes.ChainSelector(3) + // Setup cache mock and configuration + mockCache := new(mockConfigCache) + chainConfig := ChainConfigSnapshot{ + Offramp: OfframpConfig{ + // We can leave the configs empty since we just need GetChainConfig to succeed + StaticConfig: offRampStaticChainConfig{}, + DynamicConfig: offRampDynamicChainConfig{}, + }, + } + mockCache.On("GetChainConfig", mock.Anything, destChain).Return(chainConfig, nil) + // Setup mock cache to return an error + destExtended := reader_mocks.NewMockExtended(t) getLatestValueErr := errors.New("some error") - mockCache := new(mockConfigCache) - mockCache.On("GetChainConfig", mock.Anything, destChain).Return(ChainConfigSnapshot{}, getLatestValueErr) destExtended.EXPECT().ExtendedBatchGetLatestValues( mock.Anything, mock.Anything, @@ -692,7 +701,7 @@ func TestCCIPChainReader_DiscoverContracts_GetAllSourceChainConfig_Errors(t *tes ccipChainReader := &ccipChainReader{ destChain: destChain, contractReaders: map[cciptypes.ChainSelector]contractreader.Extended{ - destChain: reader_mocks.NewMockExtended(t), + destChain: destExtended, // these won't be used in this test, but are needed because // we determine the source chain selectors to query from the chains // that we have readers for. @@ -715,18 +724,7 @@ func TestCCIPChainReader_DiscoverContracts_GetOfframpStaticConfig_Errors(t *test sourceChain1 := cciptypes.ChainSelector(2) sourceChain2 := cciptypes.ChainSelector(3) - destExtended := reader_mocks.NewMockExtended(t) - // mock the call for source chain configs - destExtended.EXPECT().ExtendedBatchGetLatestValues( - mock.Anything, - mock.Anything, - mock.Anything, - ).RunAndReturn(withBatchGetLatestValuesRetValues(t, - "0x1234567890123456789012345678901234567890", - []any{&sourceChainConfig{}, &sourceChainConfig{}})) - - // Setup mock cache to return a config with missing static config data - // mock the call to get the static config - failure + // Setup mock cache to return error getLatestValueErr := errors.New("some error") mockCache := new(mockConfigCache) mockCache.On("GetChainConfig", mock.Anything, destChain).Return(ChainConfigSnapshot{}, getLatestValueErr) diff --git a/pkg/reader/config_poller.go b/pkg/reader/config_poller.go index 59e095a41..ba2f052e8 100644 --- a/pkg/reader/config_poller.go +++ b/pkg/reader/config_poller.go @@ -162,7 +162,6 @@ func (c *configPoller) prepareBatchRequests() contractreader.ExtendedBatchGetLat execLatestOCRConfig OCRConfigResponse staticConfig offRampStaticChainConfig dynamicConfig offRampDynamicChainConfig - selectorsAndConf selectorsAndConfigs rmnRemoteAddress []byte rmnDigestHeader rmnDigestHeader rmnVersionConfig versionedConfig @@ -195,11 +194,6 @@ func (c *configPoller) prepareBatchRequests() contractreader.ExtendedBatchGetLat Params: map[string]any{}, ReturnVal: &dynamicConfig, }, - { - ReadName: consts.MethodNameOffRampGetAllSourceChainConfigs, - Params: map[string]any{}, - ReturnVal: &selectorsAndConf, - }, }, consts.ContractNameRMNProxy: {{ ReadName: consts.MethodNameGetARM, @@ -338,15 +332,6 @@ func (c *configPoller) processOfframpResults( config.DynamicConfig = *typed return nil }, - // SelectorsAndConf - func(val interface{}) error { - typed, ok := val.(*selectorsAndConfigs) - if !ok { - return fmt.Errorf("invalid type for SelectorsAndConf: %T", val) - } - config.SelectorsAndConf = *typed - return nil - }, } // Process each result with its corresponding processor diff --git a/pkg/reader/config_poller_test.go b/pkg/reader/config_poller_test.go index 3526ad091..e7e838183 100644 --- a/pkg/reader/config_poller_test.go +++ b/pkg/reader/config_poller_test.go @@ -53,12 +53,10 @@ func TestConfigCache_GetChainConfig_CacheHit(t *testing.T) { result3.SetResult(&offRampStaticChainConfig{}, nil) result4 := &types.BatchReadResult{ReadName: consts.MethodNameOffRampGetDynamicConfig} result4.SetResult(&offRampDynamicChainConfig{}, nil) - result5 := &types.BatchReadResult{ReadName: consts.MethodNameOffRampGetAllSourceChainConfigs} - result5.SetResult(&selectorsAndConfigs{}, nil) responses := types.BatchGetLatestValuesResult{ types.BoundContract{Name: consts.ContractNameOffRamp}: { - *result1, *result2, *result3, *result4, *result5, + *result1, *result2, *result3, *result4, }, } @@ -103,12 +101,10 @@ func TestConfigCache_GetChainConfig_CacheMiss(t *testing.T) { result3.SetResult(&offRampStaticChainConfig{}, nil) result4 := &types.BatchReadResult{ReadName: consts.MethodNameOffRampGetDynamicConfig} result4.SetResult(&offRampDynamicChainConfig{}, nil) - result5 := &types.BatchReadResult{ReadName: consts.MethodNameOffRampGetAllSourceChainConfigs} - result5.SetResult(&selectorsAndConfigs{}, nil) return types.BatchGetLatestValuesResult{ types.BoundContract{Name: consts.ContractNameOffRamp}: { - *result1, *result2, *result3, *result4, *result5, + *result1, *result2, *result3, *result4, }, } } @@ -178,12 +174,10 @@ func TestConfigCache_GetChainConfig_ErrorWithCachedData(t *testing.T) { result3.SetResult(&offRampStaticChainConfig{}, nil) result4 := &types.BatchReadResult{ReadName: consts.MethodNameOffRampGetDynamicConfig} result4.SetResult(&offRampDynamicChainConfig{}, nil) - result5 := &types.BatchReadResult{ReadName: consts.MethodNameOffRampGetAllSourceChainConfigs} - result5.SetResult(&selectorsAndConfigs{}, nil) responses := types.BatchGetLatestValuesResult{ types.BoundContract{Name: consts.ContractNameOffRamp}: { - *result1, *result2, *result3, *result4, *result5, + *result1, *result2, *result3, *result4, }, } @@ -232,12 +226,10 @@ func TestConfigCache_RefreshChainConfig(t *testing.T) { result3.SetResult(&offRampStaticChainConfig{}, nil) result4 := &types.BatchReadResult{ReadName: consts.MethodNameOffRampGetDynamicConfig} result4.SetResult(&offRampDynamicChainConfig{}, nil) - result5 := &types.BatchReadResult{ReadName: consts.MethodNameOffRampGetAllSourceChainConfigs} - result5.SetResult(&selectorsAndConfigs{}, nil) responses := types.BatchGetLatestValuesResult{ types.BoundContract{Name: consts.ContractNameOffRamp}: { - *result1, *result2, *result3, *result4, *result5, + *result1, *result2, *result3, *result4, }, } @@ -274,12 +266,10 @@ func TestConfigCache_ConcurrentAccess(t *testing.T) { result3.SetResult(&offRampStaticChainConfig{}, nil) result4 := &types.BatchReadResult{ReadName: consts.MethodNameOffRampGetDynamicConfig} result4.SetResult(&offRampDynamicChainConfig{}, nil) - result5 := &types.BatchReadResult{ReadName: consts.MethodNameOffRampGetAllSourceChainConfigs} - result5.SetResult(&selectorsAndConfigs{}, nil) responses := types.BatchGetLatestValuesResult{ types.BoundContract{Name: consts.ContractNameOffRamp}: { - *result1, *result2, *result3, *result4, *result5, + *result1, *result2, *result3, *result4, }, } @@ -381,12 +371,10 @@ func TestConfigCache_GetChainConfig_SkippedContracts(t *testing.T) { result3.SetResult(&offRampStaticChainConfig{}, nil) result4 := &types.BatchReadResult{ReadName: consts.MethodNameOffRampGetDynamicConfig} result4.SetResult(&offRampDynamicChainConfig{}, nil) - result5 := &types.BatchReadResult{ReadName: consts.MethodNameOffRampGetAllSourceChainConfigs} - result5.SetResult(&selectorsAndConfigs{}, nil) responses := types.BatchGetLatestValuesResult{ types.BoundContract{Name: consts.ContractNameOffRamp}: { - *result1, *result2, *result3, *result4, *result5, + *result1, *result2, *result3, *result4, }, } skippedContracts := []string{consts.ContractNameRMNProxy} @@ -438,12 +426,9 @@ func TestConfigCache_InvalidResults(t *testing.T) { result4 := &types.BatchReadResult{ReadName: consts.MethodNameOffRampGetDynamicConfig} result4.SetResult(&offRampDynamicChainConfig{}, nil) - result5 := &types.BatchReadResult{ReadName: consts.MethodNameOffRampGetAllSourceChainConfigs} - result5.SetResult(&selectorsAndConfigs{}, nil) - return types.BatchGetLatestValuesResult{ types.BoundContract{Name: consts.ContractNameOffRamp}: { - *result1, *result2, *result3, *result4, *result5, + *result1, *result2, *result3, *result4, }, } }, @@ -492,12 +477,10 @@ func TestConfigCache_MultipleChains(t *testing.T) { result3.SetResult(&offRampStaticChainConfig{}, nil) result4 := &types.BatchReadResult{ReadName: consts.MethodNameOffRampGetDynamicConfig} result4.SetResult(&offRampDynamicChainConfig{}, nil) - result5 := &types.BatchReadResult{ReadName: consts.MethodNameOffRampGetAllSourceChainConfigs} - result5.SetResult(&selectorsAndConfigs{}, nil) return types.BatchGetLatestValuesResult{ types.BoundContract{Name: consts.ContractNameOffRamp}: { - *result1, *result2, *result3, *result4, *result5, + *result1, *result2, *result3, *result4, }, } } @@ -573,12 +556,10 @@ func TestConfigCache_RefreshPeriod(t *testing.T) { result3.SetResult(&offRampStaticChainConfig{}, nil) result4 := &types.BatchReadResult{ReadName: consts.MethodNameOffRampGetDynamicConfig} result4.SetResult(&offRampDynamicChainConfig{}, nil) - result5 := &types.BatchReadResult{ReadName: consts.MethodNameOffRampGetAllSourceChainConfigs} - result5.SetResult(&selectorsAndConfigs{}, nil) responses := types.BatchGetLatestValuesResult{ types.BoundContract{Name: consts.ContractNameOffRamp}: { - *result1, *result2, *result3, *result4, *result5, + *result1, *result2, *result3, *result4, }, } From 84f824f139bebc1ee983978a50e1a6966812aac7 Mon Sep 17 00:00:00 2001 From: nogo <0xnogo@gmail.com> Date: Fri, 7 Feb 2025 12:28:03 +0400 Subject: [PATCH 17/18] move request logic in reader --- pkg/reader/ccip.go | 228 ++++++++++++++++++++++++++- pkg/reader/config_poller.go | 260 ++----------------------------- pkg/reader/config_poller_test.go | 128 +++++++++++---- 3 files changed, 334 insertions(+), 282 deletions(-) diff --git a/pkg/reader/ccip.go b/pkg/reader/ccip.go index 40ba79678..7eb1ec5aa 100644 --- a/pkg/reader/ccip.go +++ b/pkg/reader/ccip.go @@ -72,7 +72,7 @@ func newCCIPChainReaderInternal( } // Initialize cache with readers - reader.configPoller = newConfigPoller(lggr, crs, defaultRefreshPeriod) + reader.configPoller = newConfigPoller(lggr, reader, defaultRefreshPeriod) contracts := ContractAddresses{ consts.ContractNameOffRamp: { @@ -1514,6 +1514,232 @@ func (r *ccipChainReader) GetOffRampConfigDigest(ctx context.Context, pluginType return resp.OCRConfig.ConfigInfo.ConfigDigest, nil } +func (r *ccipChainReader) prepareBatchConfigRequests() contractreader.ExtendedBatchGetLatestValuesRequest { + var ( + commitLatestOCRConfig OCRConfigResponse + execLatestOCRConfig OCRConfigResponse + staticConfig offRampStaticChainConfig + dynamicConfig offRampDynamicChainConfig + rmnRemoteAddress []byte + rmnDigestHeader rmnDigestHeader + rmnVersionConfig versionedConfig + feeQuoterConfig feeQuoterStaticConfig + ) + + return contractreader.ExtendedBatchGetLatestValuesRequest{ + consts.ContractNameOffRamp: { + { + ReadName: consts.MethodNameOffRampLatestConfigDetails, + Params: map[string]any{ + "ocrPluginType": consts.PluginTypeCommit, + }, + ReturnVal: &commitLatestOCRConfig, + }, + { + ReadName: consts.MethodNameOffRampLatestConfigDetails, + Params: map[string]any{ + "ocrPluginType": consts.PluginTypeExecute, + }, + ReturnVal: &execLatestOCRConfig, + }, + { + ReadName: consts.MethodNameOffRampGetStaticConfig, + Params: map[string]any{}, + ReturnVal: &staticConfig, + }, + { + ReadName: consts.MethodNameOffRampGetDynamicConfig, + Params: map[string]any{}, + ReturnVal: &dynamicConfig, + }, + }, + consts.ContractNameRMNProxy: {{ + ReadName: consts.MethodNameGetARM, + Params: map[string]any{}, + ReturnVal: &rmnRemoteAddress, + }}, + consts.ContractNameRMNRemote: { + { + ReadName: consts.MethodNameGetReportDigestHeader, + Params: map[string]any{}, + ReturnVal: &rmnDigestHeader, + }, + { + ReadName: consts.MethodNameGetVersionedConfig, + Params: map[string]any{}, + ReturnVal: &rmnVersionConfig, + }, + }, + consts.ContractNameFeeQuoter: {{ + ReadName: consts.MethodNameFeeQuoterGetStaticConfig, + Params: map[string]any{}, + ReturnVal: &feeQuoterConfig, + }}, + } +} + +func (r *ccipChainReader) processConfigResults( + batchResult types.BatchGetLatestValuesResult) (ChainConfigSnapshot, error) { + config := ChainConfigSnapshot{} + + for contract, results := range batchResult { + var err error + switch contract.Name { + case consts.ContractNameOffRamp: + config.Offramp, err = r.processOfframpResults(results) + case consts.ContractNameRMNProxy: + config.RMNProxy, err = r.processRMNProxyResults(results) + case consts.ContractNameRMNRemote: + config.RMNRemote, err = r.processRMNRemoteResults(results) + case consts.ContractNameFeeQuoter: + config.FeeQuoter, err = r.processFeeQuoterResults(results) + default: + r.lggr.Warnw("Unhandled contract in batch results", "contract", contract.Name) + } + if err != nil { + return ChainConfigSnapshot{}, fmt.Errorf("process %s results: %w", contract.Name, err) + } + } + + return config, nil +} + +func (r *ccipChainReader) processOfframpResults( + results []types.BatchReadResult) (OfframpConfig, error) { + + if len(results) != 4 { + return OfframpConfig{}, fmt.Errorf("expected 4 offramp results, got %d", len(results)) + } + + config := OfframpConfig{} + + // Define processors for each expected result + processors := []resultProcessor{ + // CommitLatestOCRConfig + func(val interface{}) error { + typed, ok := val.(*OCRConfigResponse) + if !ok { + return fmt.Errorf("invalid type for CommitLatestOCRConfig: %T", val) + } + config.CommitLatestOCRConfig = *typed + return nil + }, + // ExecLatestOCRConfig + func(val interface{}) error { + typed, ok := val.(*OCRConfigResponse) + if !ok { + return fmt.Errorf("invalid type for ExecLatestOCRConfig: %T", val) + } + config.ExecLatestOCRConfig = *typed + return nil + }, + // StaticConfig + func(val interface{}) error { + typed, ok := val.(*offRampStaticChainConfig) + if !ok { + return fmt.Errorf("invalid type for StaticConfig: %T", val) + } + config.StaticConfig = *typed + return nil + }, + // DynamicConfig + func(val interface{}) error { + typed, ok := val.(*offRampDynamicChainConfig) + if !ok { + return fmt.Errorf("invalid type for DynamicConfig: %T", val) + } + config.DynamicConfig = *typed + return nil + }, + } + + // Process each result with its corresponding processor + for i, result := range results { + val, err := result.GetResult() + if err != nil { + return OfframpConfig{}, fmt.Errorf("get offramp result %d: %w", i, err) + } + + if err := processors[i](val); err != nil { + return OfframpConfig{}, fmt.Errorf("process result %d: %w", i, err) + } + } + + return config, nil +} + +func (r *ccipChainReader) processRMNProxyResults(results []types.BatchReadResult) (RMNProxyConfig, error) { + if len(results) != 1 { + return RMNProxyConfig{}, fmt.Errorf("expected 1 RMN proxy result, got %d", len(results)) + } + + val, err := results[0].GetResult() + if err != nil { + return RMNProxyConfig{}, fmt.Errorf("get RMN proxy result: %w", err) + } + + if bytes, ok := val.(*[]byte); ok { + return RMNProxyConfig{ + RemoteAddress: *bytes, + }, nil + } + + return RMNProxyConfig{}, fmt.Errorf("invalid type for RMN proxy remote address: %T", val) +} + +func (r *ccipChainReader) processRMNRemoteResults(results []types.BatchReadResult) (RMNRemoteConfig, error) { + config := RMNRemoteConfig{} + + if len(results) != 2 { + return RMNRemoteConfig{}, fmt.Errorf("expected 2 RMN remote results, got %d", len(results)) + } + + // Process DigestHeader + val, err := results[0].GetResult() + if err != nil { + return RMNRemoteConfig{}, fmt.Errorf("get RMN remote digest header result: %w", err) + } + + typed, ok := val.(*rmnDigestHeader) + if !ok { + return RMNRemoteConfig{}, fmt.Errorf("invalid type for RMN remote digest header: %T", val) + } + config.DigestHeader = *typed + + // Process VersionedConfig + val, err = results[1].GetResult() + if err != nil { + return RMNRemoteConfig{}, fmt.Errorf("get RMN remote versioned config result: %w", err) + } + + vconf, ok := val.(*versionedConfig) + if !ok { + return RMNRemoteConfig{}, fmt.Errorf("invalid type for RMN remote versioned config: %T", val) + } + config.VersionedConfig = *vconf + + return config, nil +} + +func (r *ccipChainReader) processFeeQuoterResults(results []types.BatchReadResult) (FeeQuoterConfig, error) { + if len(results) != 1 { + return FeeQuoterConfig{}, fmt.Errorf("expected 1 fee quoter result, got %d", len(results)) + } + + val, err := results[0].GetResult() + if err != nil { + return FeeQuoterConfig{}, fmt.Errorf("get fee quoter result: %w", err) + } + + if typed, ok := val.(*feeQuoterStaticConfig); ok { + return FeeQuoterConfig{ + StaticConfig: *typed, + }, nil + } + + return FeeQuoterConfig{}, fmt.Errorf("invalid type for fee quoter static config: %T", val) +} + func validateCommitReportAcceptedEvent(seq types.Sequence, gteTimestamp time.Time) (*CommitReportAcceptedEvent, error) { ev, is := (seq.Data).(*CommitReportAcceptedEvent) if !is { diff --git a/pkg/reader/config_poller.go b/pkg/reader/config_poller.go index ba2f052e8..a3952e2f8 100644 --- a/pkg/reader/config_poller.go +++ b/pkg/reader/config_poller.go @@ -6,12 +6,9 @@ import ( "sync" "time" - "github.com/smartcontractkit/chainlink-ccip/pkg/consts" - "github.com/smartcontractkit/chainlink-ccip/pkg/contractreader" cciptypes "github.com/smartcontractkit/chainlink-ccip/pkg/types/ccipocr3" "github.com/smartcontractkit/chainlink-common/pkg/logger" - "github.com/smartcontractkit/chainlink-common/pkg/types" ) // ConfigPoller defines the interface for caching chain configuration data @@ -30,7 +27,7 @@ type configPoller struct { sync.RWMutex chainCaches map[cciptypes.ChainSelector]*chainCache refreshPeriod time.Duration - readers map[cciptypes.ChainSelector]contractreader.Extended + reader *ccipChainReader // Reference to the reader for fetching configs lggr logger.Logger } @@ -46,13 +43,13 @@ type chainCache struct { // newConfigPoller creates a new config cache instance func newConfigPoller( lggr logger.Logger, - readers map[cciptypes.ChainSelector]contractreader.Extended, + reader *ccipChainReader, refreshPeriod time.Duration, ) *configPoller { return &configPoller{ chainCaches: make(map[cciptypes.ChainSelector]*chainCache), refreshPeriod: refreshPeriod, - readers: readers, + reader: reader, lggr: lggr, } } @@ -67,7 +64,7 @@ func (c *configPoller) getOrCreateChainCache(chainSel cciptypes.ChainSelector) * } // verify we have the reader for this chain - if _, exists := c.readers[chainSel]; !exists { + if _, exists := c.reader.contractReaders[chainSel]; !exists { c.lggr.Errorw("No contract reader for chain", "chain", chainSel) return nil } @@ -83,7 +80,7 @@ func (c *configPoller) GetChainConfig( chainSel cciptypes.ChainSelector, ) (ChainConfigSnapshot, error) { // Check if we have a reader for this chain - reader, exists := c.readers[chainSel] + reader, exists := c.reader.contractReaders[chainSel] if !exists || reader == nil { c.lggr.Errorw("No contract reader for chain", "chain", chainSel) return ChainConfigSnapshot{}, fmt.Errorf("no contract reader for chain %d", chainSel) @@ -155,81 +152,16 @@ func (c *configPoller) RefreshChainConfig( return newData, nil } -// prepareBatchRequests creates the batch request for all configurations -func (c *configPoller) prepareBatchRequests() contractreader.ExtendedBatchGetLatestValuesRequest { - var ( - commitLatestOCRConfig OCRConfigResponse - execLatestOCRConfig OCRConfigResponse - staticConfig offRampStaticChainConfig - dynamicConfig offRampDynamicChainConfig - rmnRemoteAddress []byte - rmnDigestHeader rmnDigestHeader - rmnVersionConfig versionedConfig - feeQuoterConfig feeQuoterStaticConfig - ) - - return contractreader.ExtendedBatchGetLatestValuesRequest{ - consts.ContractNameOffRamp: { - { - ReadName: consts.MethodNameOffRampLatestConfigDetails, - Params: map[string]any{ - "ocrPluginType": consts.PluginTypeCommit, - }, - ReturnVal: &commitLatestOCRConfig, - }, - { - ReadName: consts.MethodNameOffRampLatestConfigDetails, - Params: map[string]any{ - "ocrPluginType": consts.PluginTypeExecute, - }, - ReturnVal: &execLatestOCRConfig, - }, - { - ReadName: consts.MethodNameOffRampGetStaticConfig, - Params: map[string]any{}, - ReturnVal: &staticConfig, - }, - { - ReadName: consts.MethodNameOffRampGetDynamicConfig, - Params: map[string]any{}, - ReturnVal: &dynamicConfig, - }, - }, - consts.ContractNameRMNProxy: {{ - ReadName: consts.MethodNameGetARM, - Params: map[string]any{}, - ReturnVal: &rmnRemoteAddress, - }}, - consts.ContractNameRMNRemote: { - { - ReadName: consts.MethodNameGetReportDigestHeader, - Params: map[string]any{}, - ReturnVal: &rmnDigestHeader, - }, - { - ReadName: consts.MethodNameGetVersionedConfig, - Params: map[string]any{}, - ReturnVal: &rmnVersionConfig, - }, - }, - consts.ContractNameFeeQuoter: {{ - ReadName: consts.MethodNameFeeQuoterGetStaticConfig, - Params: map[string]any{}, - ReturnVal: &feeQuoterConfig, - }}, - } -} - -// fetchChainConfig fetches the latest configuration for a specific chain func (c *configPoller) fetchChainConfig( ctx context.Context, chainSel cciptypes.ChainSelector) (ChainConfigSnapshot, error) { - reader, exists := c.readers[chainSel] + + reader, exists := c.reader.contractReaders[chainSel] if !exists { return ChainConfigSnapshot{}, fmt.Errorf("no contract reader for chain %d", chainSel) } - requests := c.prepareBatchRequests() + requests := c.reader.prepareBatchConfigRequests() batchResult, skipped, err := reader.ExtendedBatchGetLatestValues(ctx, requests, true) if err != nil { return ChainConfigSnapshot{}, fmt.Errorf("batch get latest values for chain %d: %w", chainSel, err) @@ -241,185 +173,11 @@ func (c *configPoller) fetchChainConfig( "contracts", skipped) } - return c.updateFromResults(batchResult) -} - -func (c *configPoller) updateFromResults(batchResult types.BatchGetLatestValuesResult) (ChainConfigSnapshot, error) { - config := ChainConfigSnapshot{} - - for contract, results := range batchResult { - var err error - switch contract.Name { - case consts.ContractNameOffRamp: - config.Offramp, err = c.processOfframpResults(results) - if err != nil { - return ChainConfigSnapshot{}, fmt.Errorf("process offramp results: %w", err) - } - - case consts.ContractNameRMNProxy: - config.RMNProxy, err = c.processRMNProxyResults(results) - if err != nil { - return ChainConfigSnapshot{}, fmt.Errorf("process RMN proxy results: %w", err) - } - - case consts.ContractNameRMNRemote: - config.RMNRemote, err = c.processRMNRemoteResults(results) - if err != nil { - return ChainConfigSnapshot{}, fmt.Errorf("process RMN remote results: %w", err) - } - - case consts.ContractNameFeeQuoter: - config.FeeQuoter, err = c.processFeeQuoterResults(results) - if err != nil { - return ChainConfigSnapshot{}, fmt.Errorf("process fee quoter results: %w", err) - } - - default: - c.lggr.Warnw("Unhandled contract in batch results", "contract", contract.Name) - } - } - - return config, nil + return c.reader.processConfigResults(batchResult) } // resultProcessor defines a function type for processing individual results type resultProcessor func(interface{}) error -func (c *configPoller) processOfframpResults( - results []types.BatchReadResult) (OfframpConfig, error) { - - if len(results) != 5 { - return OfframpConfig{}, fmt.Errorf("expected 5 offramp results, got %d", len(results)) - } - - config := OfframpConfig{} - - // Define processors for each expected result - processors := []resultProcessor{ - // CommitLatestOCRConfig - func(val interface{}) error { - typed, ok := val.(*OCRConfigResponse) - if !ok { - return fmt.Errorf("invalid type for CommitLatestOCRConfig: %T", val) - } - config.CommitLatestOCRConfig = *typed - return nil - }, - // ExecLatestOCRConfig - func(val interface{}) error { - typed, ok := val.(*OCRConfigResponse) - if !ok { - return fmt.Errorf("invalid type for ExecLatestOCRConfig: %T", val) - } - config.ExecLatestOCRConfig = *typed - return nil - }, - // StaticConfig - func(val interface{}) error { - typed, ok := val.(*offRampStaticChainConfig) - if !ok { - return fmt.Errorf("invalid type for StaticConfig: %T", val) - } - config.StaticConfig = *typed - return nil - }, - // DynamicConfig - func(val interface{}) error { - typed, ok := val.(*offRampDynamicChainConfig) - if !ok { - return fmt.Errorf("invalid type for DynamicConfig: %T", val) - } - config.DynamicConfig = *typed - return nil - }, - } - - // Process each result with its corresponding processor - for i, result := range results { - val, err := result.GetResult() - if err != nil { - return OfframpConfig{}, fmt.Errorf("get offramp result %d: %w", i, err) - } - - if err := processors[i](val); err != nil { - return OfframpConfig{}, fmt.Errorf("process result %d: %w", i, err) - } - } - - return config, nil -} - -func (c *configPoller) processRMNProxyResults(results []types.BatchReadResult) (RMNProxyConfig, error) { - if len(results) != 1 { - return RMNProxyConfig{}, fmt.Errorf("expected 1 RMN proxy result, got %d", len(results)) - } - - val, err := results[0].GetResult() - if err != nil { - return RMNProxyConfig{}, fmt.Errorf("get RMN proxy result: %w", err) - } - - if bytes, ok := val.(*[]byte); ok { - return RMNProxyConfig{ - RemoteAddress: *bytes, - }, nil - } - - return RMNProxyConfig{}, fmt.Errorf("invalid type for RMN proxy remote address: %T", val) -} - -func (c *configPoller) processRMNRemoteResults(results []types.BatchReadResult) (RMNRemoteConfig, error) { - config := RMNRemoteConfig{} - - if len(results) != 2 { - return RMNRemoteConfig{}, fmt.Errorf("expected 2 RMN remote results, got %d", len(results)) - } - - // Process DigestHeader - val, err := results[0].GetResult() - if err != nil { - return RMNRemoteConfig{}, fmt.Errorf("get RMN remote digest header result: %w", err) - } - - typed, ok := val.(*rmnDigestHeader) - if !ok { - return RMNRemoteConfig{}, fmt.Errorf("invalid type for RMN remote digest header: %T", val) - } - config.DigestHeader = *typed - - // Process VersionedConfig - val, err = results[1].GetResult() - if err != nil { - return RMNRemoteConfig{}, fmt.Errorf("get RMN remote versioned config result: %w", err) - } - - vconf, ok := val.(*versionedConfig) - if !ok { - return RMNRemoteConfig{}, fmt.Errorf("invalid type for RMN remote versioned config: %T", val) - } - config.VersionedConfig = *vconf - - return config, nil -} - -func (c *configPoller) processFeeQuoterResults(results []types.BatchReadResult) (FeeQuoterConfig, error) { - if len(results) != 1 { - return FeeQuoterConfig{}, fmt.Errorf("expected 1 fee quoter result, got %d", len(results)) - } - - val, err := results[0].GetResult() - if err != nil { - return FeeQuoterConfig{}, fmt.Errorf("get fee quoter result: %w", err) - } - - if typed, ok := val.(*feeQuoterStaticConfig); ok { - return FeeQuoterConfig{ - StaticConfig: *typed, - }, nil - } - - return FeeQuoterConfig{}, fmt.Errorf("invalid type for fee quoter static config: %T", val) -} - // Ensure configCache implements ConfigCache var _ ConfigPoller = (*configPoller)(nil) diff --git a/pkg/reader/config_poller_test.go b/pkg/reader/config_poller_test.go index e7e838183..b80ccdfea 100644 --- a/pkg/reader/config_poller_test.go +++ b/pkg/reader/config_poller_test.go @@ -20,12 +20,18 @@ import ( ) func setupBasicCache(t *testing.T) (*configPoller, *reader_mocks.MockExtended) { - reader := reader_mocks.NewMockExtended(t) - readers := map[cciptypes.ChainSelector]contractreader.Extended{ - chainA: reader, + mockReader := reader_mocks.NewMockExtended(t) + + reader := &ccipChainReader{ + lggr: logger.Test(t), + contractReaders: map[cciptypes.ChainSelector]contractreader.Extended{ + chainA: mockReader, + }, + destChain: chainA, } - cache := newConfigPoller(logger.Test(t), readers, 1*time.Second) - return cache, reader + + cache := newConfigPoller(logger.Test(t), reader, 1*time.Second) + return cache, mockReader } func TestConfigCache_GetChainConfig_CacheHit(t *testing.T) { @@ -302,29 +308,47 @@ func TestConfigCache_ConcurrentAccess(t *testing.T) { func TestConfigCache_Initialization(t *testing.T) { testCases := []struct { name string - readers map[cciptypes.ChainSelector]contractreader.Extended + setupReader func() *ccipChainReader refreshPeriod time.Duration chainToTest cciptypes.ChainSelector expectedErr string }{ { - name: "nil readers map", - readers: nil, + name: "nil readers map", + setupReader: func() *ccipChainReader { + return &ccipChainReader{ + lggr: logger.Test(t), + contractReaders: nil, + destChain: chainA, + } + }, refreshPeriod: time.Second, chainToTest: chainA, expectedErr: "no contract reader for chain", }, { - name: "empty readers map", - readers: make(map[cciptypes.ChainSelector]contractreader.Extended), + name: "empty readers map", + setupReader: func() *ccipChainReader { + return &ccipChainReader{ + lggr: logger.Test(t), + contractReaders: make(map[cciptypes.ChainSelector]contractreader.Extended), + destChain: chainA, + } + }, refreshPeriod: time.Second, chainToTest: chainA, expectedErr: "no contract reader for chain", }, { name: "missing specific chain", - readers: map[cciptypes.ChainSelector]contractreader.Extended{ - chainB: nil, // Different chain than we'll test + setupReader: func() *ccipChainReader { + return &ccipChainReader{ + lggr: logger.Test(t), + contractReaders: map[cciptypes.ChainSelector]contractreader.Extended{ + chainB: nil, // Different chain than we'll test + }, + destChain: chainA, + } }, refreshPeriod: time.Second, chainToTest: chainA, @@ -337,12 +361,14 @@ func TestConfigCache_Initialization(t *testing.T) { lggr := logger.Test(t) ctx := tests.Context(t) - cache := newConfigPoller(lggr, tc.readers, tc.refreshPeriod) + reader := tc.setupReader() + cache := newConfigPoller(lggr, reader, tc.refreshPeriod) require.NotNil(t, cache, "cache should never be nil after initialization") // Verify the cache's internal state require.NotNil(t, cache.chainCaches, "chainCaches map should never be nil") assert.Equal(t, tc.refreshPeriod, cache.refreshPeriod) + assert.Equal(t, reader, cache.reader) // Test getting config for a chain _, err := cache.GetChainConfig(ctx, tc.chainToTest) @@ -395,7 +421,6 @@ func TestConfigCache_InvalidResults(t *testing.T) { cache, reader := setupBasicCache(t) ctx := tests.Context(t) - // Test cases for different invalid results testCases := []struct { name string setupMock func() types.BatchGetLatestValuesResult @@ -404,32 +429,59 @@ func TestConfigCache_InvalidResults(t *testing.T) { { name: "missing offramp results", setupMock: func() types.BatchGetLatestValuesResult { + // Setup minimum valid responses for other contracts + rmnProxyResult := &types.BatchReadResult{ReadName: consts.MethodNameGetARM} + rmnProxyResult.SetResult(&[]byte{1, 2, 3}, nil) + + rmnDigestResult := &types.BatchReadResult{ReadName: consts.MethodNameGetReportDigestHeader} + rmnDigestResult.SetResult(&rmnDigestHeader{}, nil) + rmnConfigResult := &types.BatchReadResult{ReadName: consts.MethodNameGetVersionedConfig} + rmnConfigResult.SetResult(&versionedConfig{}, nil) + + feeQuoterResult := &types.BatchReadResult{ReadName: consts.MethodNameFeeQuoterGetStaticConfig} + feeQuoterResult.SetResult(&feeQuoterStaticConfig{}, nil) + return types.BatchGetLatestValuesResult{ - types.BoundContract{Name: consts.ContractNameOffRamp}: {}, + types.BoundContract{Name: consts.ContractNameOffRamp}: {}, + types.BoundContract{Name: consts.ContractNameRMNProxy}: {*rmnProxyResult}, + types.BoundContract{Name: consts.ContractNameRMNRemote}: {*rmnDigestResult, *rmnConfigResult}, + types.BoundContract{Name: consts.ContractNameFeeQuoter}: {*feeQuoterResult}, } }, - expectedErr: "expected 5 offramp results", + expectedErr: "expected 4 offramp results", }, { name: "invalid commit config type", setupMock: func() types.BatchGetLatestValuesResult { - // Setup all 5 required results, but make the first one invalid + // Setup all 4 required offramp results, but make the first one invalid result1 := &types.BatchReadResult{ReadName: consts.MethodNameOffRampLatestConfigDetails} result1.SetResult("invalid type", nil) - result2 := &types.BatchReadResult{ReadName: consts.MethodNameOffRampLatestConfigDetails} result2.SetResult(&OCRConfigResponse{}, nil) - result3 := &types.BatchReadResult{ReadName: consts.MethodNameOffRampGetStaticConfig} result3.SetResult(&offRampStaticChainConfig{}, nil) - result4 := &types.BatchReadResult{ReadName: consts.MethodNameOffRampGetDynamicConfig} result4.SetResult(&offRampDynamicChainConfig{}, nil) + // Setup valid responses for other contracts + rmnProxyResult := &types.BatchReadResult{ReadName: consts.MethodNameGetARM} + rmnProxyResult.SetResult(&[]byte{1, 2, 3}, nil) + + rmnDigestResult := &types.BatchReadResult{ReadName: consts.MethodNameGetReportDigestHeader} + rmnDigestResult.SetResult(&rmnDigestHeader{}, nil) + rmnConfigResult := &types.BatchReadResult{ReadName: consts.MethodNameGetVersionedConfig} + rmnConfigResult.SetResult(&versionedConfig{}, nil) + + feeQuoterResult := &types.BatchReadResult{ReadName: consts.MethodNameFeeQuoterGetStaticConfig} + feeQuoterResult.SetResult(&feeQuoterStaticConfig{}, nil) + return types.BatchGetLatestValuesResult{ types.BoundContract{Name: consts.ContractNameOffRamp}: { *result1, *result2, *result3, *result4, }, + types.BoundContract{Name: consts.ContractNameRMNProxy}: {*rmnProxyResult}, + types.BoundContract{Name: consts.ContractNameRMNRemote}: {*rmnDigestResult, *rmnConfigResult}, + types.BoundContract{Name: consts.ContractNameFeeQuoter}: {*feeQuoterResult}, } }, expectedErr: "invalid type for CommitLatestOCRConfig", @@ -447,6 +499,8 @@ func TestConfigCache_InvalidResults(t *testing.T) { _, err := cache.GetChainConfig(ctx, chainA) require.Error(t, err) assert.Contains(t, err.Error(), tc.expectedErr) + + reader.AssertExpectations(t) }) } } @@ -454,11 +508,18 @@ func TestConfigCache_InvalidResults(t *testing.T) { func TestConfigCache_MultipleChains(t *testing.T) { readerA := reader_mocks.NewMockExtended(t) readerB := reader_mocks.NewMockExtended(t) - readers := map[cciptypes.ChainSelector]contractreader.Extended{ - chainA: readerA, - chainB: readerB, + + // Create ccipChainReader with multiple chain readers + reader := &ccipChainReader{ + lggr: logger.Test(t), + contractReaders: map[cciptypes.ChainSelector]contractreader.Extended{ + chainA: readerA, + chainB: readerB, + }, + destChain: chainA, } - cache := newConfigPoller(logger.Test(t), readers, 1*time.Second) + + cache := newConfigPoller(logger.Test(t), reader, 1*time.Second) ctx := tests.Context(t) // Setup mock response for both chains @@ -535,11 +596,18 @@ func TestConfigCache_RefreshPeriod(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - reader := reader_mocks.NewMockExtended(t) - readers := map[cciptypes.ChainSelector]contractreader.Extended{ - chainA: reader, + mockReader := reader_mocks.NewMockExtended(t) + + // Create ccipChainReader with the mock reader + reader := &ccipChainReader{ + lggr: logger.Test(t), + contractReaders: map[cciptypes.ChainSelector]contractreader.Extended{ + chainA: mockReader, + }, + destChain: chainA, } - cache := newConfigPoller(logger.Test(t), readers, tc.refreshPeriod) + + cache := newConfigPoller(logger.Test(t), reader, tc.refreshPeriod) ctx := tests.Context(t) mockConfig := OCRConfigResponse{ @@ -569,7 +637,7 @@ func TestConfigCache_RefreshPeriod(t *testing.T) { expectedCalls = 2 } - reader.On("ExtendedBatchGetLatestValues", + mockReader.On("ExtendedBatchGetLatestValues", mock.Anything, mock.Anything, true, @@ -587,7 +655,7 @@ func TestConfigCache_RefreshPeriod(t *testing.T) { require.NoError(t, err) // Verify number of calls - reader.AssertNumberOfCalls(t, "ExtendedBatchGetLatestValues", expectedCalls) + mockReader.AssertNumberOfCalls(t, "ExtendedBatchGetLatestValues", expectedCalls) }) } } From 52bf3514a9ba3cf0379bea79c124d26e85b82110 Mon Sep 17 00:00:00 2001 From: nogo <0xnogo@gmail.com> Date: Mon, 10 Feb 2025 15:06:10 +0400 Subject: [PATCH 18/18] address comments --- pkg/reader/config_poller.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/pkg/reader/config_poller.go b/pkg/reader/config_poller.go index a3952e2f8..b7579c465 100644 --- a/pkg/reader/config_poller.go +++ b/pkg/reader/config_poller.go @@ -92,7 +92,7 @@ func (c *configPoller) GetChainConfig( timeSinceLastRefresh := time.Since(chainCache.lastRefresh) if timeSinceLastRefresh < c.refreshPeriod { defer chainCache.RUnlock() - c.lggr.Infow("Cache hit", + c.lggr.Debugw("Cache hit", "chain", chainSel, "timeSinceLastRefresh", timeSinceLastRefresh, "refreshPeriod", c.refreshPeriod) @@ -116,7 +116,7 @@ func (c *configPoller) RefreshChainConfig( // Double check if another goroutine has already refreshed timeSinceLastRefresh := time.Since(chainCache.lastRefresh) if timeSinceLastRefresh < c.refreshPeriod { - c.lggr.Infow("Cache was refreshed by another goroutine", + c.lggr.Debugw("Cache was refreshed by another goroutine", "chain", chainSel, "timeSinceLastRefresh", timeSinceLastRefresh) return chainCache.data, nil @@ -124,7 +124,7 @@ func (c *configPoller) RefreshChainConfig( startTime := time.Now() newData, err := c.fetchChainConfig(ctx, chainSel) - refreshDuration := time.Since(startTime) + fetchConfigLatency := time.Since(startTime) if err != nil { if !chainCache.lastRefresh.IsZero() { @@ -132,22 +132,22 @@ func (c *configPoller) RefreshChainConfig( "chain", chainSel, "error", err, "lastRefresh", chainCache.lastRefresh, - "refreshDuration", refreshDuration) + "fetchConfigLatency", fetchConfigLatency) return chainCache.data, nil } c.lggr.Errorw("Failed to refresh cache, no old data available", "chain", chainSel, "error", err, - "refreshDuration", refreshDuration) + "fetchConfigLatency", fetchConfigLatency) return ChainConfigSnapshot{}, fmt.Errorf("failed to refresh cache for chain %d: %w", chainSel, err) } chainCache.data = newData chainCache.lastRefresh = time.Now() - c.lggr.Infow("Successfully refreshed cache", + c.lggr.Debugw("Successfully refreshed cache", "chain", chainSel, - "refreshDuration", refreshDuration) + "fetchConfigLatency", fetchConfigLatency) return newData, nil }