diff --git a/commit/factory.go b/commit/factory.go index f5c8138b7..f2ae8793c 100644 --- a/commit/factory.go +++ b/commit/factory.go @@ -49,15 +49,15 @@ const ( // maxObservationLength is set to the maximum size of an observation // check factory_test for the calculation - maxObservationLength = 1_047_202 + maxObservationLength = 1_047_206 // maxOutcomeLength is set to the maximum size of an outcome // check factory_test for the calculation - maxOutcomeLength = 1_167_836 + maxOutcomeLength = 1_167_845 // maxReportLength is set to an estimate of a maximum report size // check factory_test for the calculation - maxReportLength = 128_2900 + maxReportLength = 128_2933 // maxReportCount is set to 1 because the commit plugin only generates one report per round. maxReportCount = 1 diff --git a/commit/factory_test.go b/commit/factory_test.go index 77e2bfc49..f5743ec76 100644 --- a/commit/factory_test.go +++ b/commit/factory_test.go @@ -251,7 +251,7 @@ func Test_maxOutcomeLength(t *testing.T) { func Test_maxReportLength(t *testing.T) { rep := ccipocr3.CommitPluginReport{ - MerkleRoots: make([]ccipocr3.MerkleRootChain, estimatedMaxNumberOfSourceChains), + BlessedMerkleRoots: make([]ccipocr3.MerkleRootChain, estimatedMaxNumberOfSourceChains), PriceUpdates: ccipocr3.PriceUpdates{ TokenPriceUpdates: make([]ccipocr3.TokenPrice, estimatedMaxNumberOfPricedTokens), GasPriceUpdates: make([]ccipocr3.GasPriceChain, estimatedMaxNumberOfSourceChains), @@ -259,8 +259,8 @@ func Test_maxReportLength(t *testing.T) { RMNSignatures: make([]ccipocr3.RMNECDSASignature, estimatedMaxRmnNodesCount), } - for i := range rep.MerkleRoots { - rep.MerkleRoots[i] = ccipocr3.MerkleRootChain{ + for i := range rep.BlessedMerkleRoots { + rep.BlessedMerkleRoots[i] = ccipocr3.MerkleRootChain{ ChainSel: math.MaxUint64, OnRampAddress: make([]byte, 40), SeqNumsRange: ccipocr3.NewSeqNumRange(math.MaxUint64, math.MaxUint64), diff --git a/commit/merkleroot/observation.go b/commit/merkleroot/observation.go index c72916cb7..edf454df4 100644 --- a/commit/merkleroot/observation.go +++ b/commit/merkleroot/observation.go @@ -74,7 +74,7 @@ func (p *Processor) Observation( } tStart := time.Now() - observation, nextState, err := p.getObservation(ctx, q, prevOutcome) + observation, nextState, err := p.getObservation(ctx, lggr, q, prevOutcome) if err != nil { return Observation{}, fmt.Errorf("get observation: %w", err) } @@ -231,7 +231,7 @@ func shouldSkipRMNVerification(nextState processorState, q Query, prevOutcome Ou } func (p *Processor) getObservation( - ctx context.Context, q Query, previousOutcome Outcome) (Observation, processorState, error) { + ctx context.Context, lggr logger.Logger, q Query, previousOutcome Outcome) (Observation, processorState, error) { nextState := previousOutcome.nextState() switch nextState { case selectingRangesForReport: @@ -279,9 +279,23 @@ func (p *Processor) getObservation( FChain: p.observer.ObserveFChain(ctx), }, nextState, nil } + + rmnEnabledChains := make(map[cciptypes.ChainSelector]bool) + + if p.offchainCfg.RMNEnabled { + var err error + rmnEnabledChains, err = p.rmnHomeReader.GetRMNEnabledSourceChains(previousOutcome.RMNRemoteCfg.ConfigDigest) + if err != nil { + return Observation{}, nextState, fmt.Errorf("failed to get RMN enabled source chains for %s: %w", + previousOutcome.RMNRemoteCfg.ConfigDigest.String(), err) + } + lggr.Debugw("fetched RMN-enabled chains from rmnHome", "rmnEnabledChains", rmnEnabledChains) + } + return Observation{ - MerkleRoots: p.observer.ObserveMerkleRoots(ctx, previousOutcome.RangesSelectedForReport), - FChain: p.observer.ObserveFChain(ctx), + MerkleRoots: p.observer.ObserveMerkleRoots(ctx, previousOutcome.RangesSelectedForReport), + FChain: p.observer.ObserveFChain(ctx), + RMNEnabledChains: rmnEnabledChains, }, nextState, nil case waitingForReportTransmission: return Observation{ diff --git a/commit/merkleroot/observation_test.go b/commit/merkleroot/observation_test.go index c434f346c..cb5856e71 100644 --- a/commit/merkleroot/observation_test.go +++ b/commit/merkleroot/observation_test.go @@ -119,7 +119,8 @@ func TestObservation(t *testing.T) { SeqNumsRange: [2]cciptypes.SeqNum{5, 10}, MerkleRoot: [32]byte{1}}, }, - FChain: map[cciptypes.ChainSelector]int{1: 3}, + RMNEnabledChains: map[cciptypes.ChainSelector]bool{1: true}, + FChain: map[cciptypes.ChainSelector]int{1: 3}, }, }, { @@ -160,6 +161,11 @@ func TestObservation(t *testing.T) { t.Run(tc.name, func(t *testing.T) { tc.setupMocks() + rmnHomeReader := readerpkg_mock.NewMockRMNHome(t) + rmnHomeReader.EXPECT().GetRMNEnabledSourceChains(tc.prevOutcome.RMNRemoteCfg.ConfigDigest). + Return(map[cciptypes.ChainSelector]bool{1: true}, nil).Maybe() + + p.rmnHomeReader = rmnHomeReader p.rmnControllerCfgDigest = tc.prevOutcome.RMNRemoteCfg.ConfigDigest // skip rmn controller setup obs, err := p.Observation(ctx, tc.prevOutcome, tc.query) diff --git a/commit/merkleroot/outcome.go b/commit/merkleroot/outcome.go index ee104ee9a..d0efba84e 100644 --- a/commit/merkleroot/outcome.go +++ b/commit/merkleroot/outcome.go @@ -12,9 +12,10 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-ccip/commit/merkleroot/rmn" + "github.com/smartcontractkit/chainlink-ccip/commit/merkleroot/rmn/rmnpb" typconv "github.com/smartcontractkit/chainlink-ccip/internal/libs/typeconv" - "github.com/smartcontractkit/chainlink-ccip/commit/merkleroot/rmn" rmntypes "github.com/smartcontractkit/chainlink-ccip/commit/merkleroot/rmn/types" "github.com/smartcontractkit/chainlink-ccip/internal/plugincommon" "github.com/smartcontractkit/chainlink-ccip/internal/plugincommon/consensus" @@ -177,69 +178,35 @@ func buildMerkleRootsOutcome( outcomeType = ReportEmpty } - if len(roots) > 0 && rmnEnabled && q.RMNSignatures == nil { - return Outcome{}, fmt.Errorf("RMN signatures are nil while RMN is enabled") - } + lggr.Debugw("building merkle roots outcome", + "rmnEnabled", rmnEnabled, + "rmnEnabledChains", consensusObservation.RMNEnabledChains, + "roots", roots, + "rmnSignatures", q.RMNSignatures) sort.Slice(roots, func(i, j int) bool { return roots[i].ChainSel < roots[j].ChainSel }) sigs := make([]cciptypes.RMNECDSASignature, 0) - if rmnEnabled && q.RMNSignatures != nil { - parsedSigs, err := rmn.NewECDSASigsFromPB(q.RMNSignatures.Signatures) - if err != nil { - return Outcome{}, fmt.Errorf("failed to parse RMN signatures: %w", err) - } - sigs = parsedSigs + var err error - type rootKey struct { - ChainSel cciptypes.ChainSelector - SeqNumsRange cciptypes.SeqNumRange - MerkleRoot cciptypes.Bytes32 - OnRampAddress string + if len(roots) > 0 && rmnEnabled { + if q.RMNSignatures == nil { + return Outcome{}, fmt.Errorf("RMN signatures are nil while RMN is enabled") } - signedRoots := mapset.NewSet[rootKey]() - for _, laneUpdate := range q.RMNSignatures.LaneUpdates { - rk := rootKey{ - ChainSel: cciptypes.ChainSelector(laneUpdate.LaneSource.SourceChainSelector), - SeqNumsRange: cciptypes.NewSeqNumRange( - cciptypes.SeqNum(laneUpdate.ClosedInterval.MinMsgNr), - cciptypes.SeqNum(laneUpdate.ClosedInterval.MaxMsgNr), - ), - MerkleRoot: cciptypes.Bytes32(laneUpdate.Root), - // NOTE: convert address into a comparable value for mapset. - OnRampAddress: typconv.AddressBytesToString( - laneUpdate.LaneSource.OnrampAddress, - laneUpdate.LaneSource.SourceChainSelector), - } - - lggr.Infow("Found signed root", "root", rk) - signedRoots.Add(rk) + sigs, err = rmn.NewECDSASigsFromPB(q.RMNSignatures.Signatures) + if err != nil { + return Outcome{}, fmt.Errorf("failed to parse RMN signatures: %w", err) } - // Only report roots that are present in RMN signatures. - rootsToReport := make([]cciptypes.MerkleRootChain, 0) - for _, root := range roots { - rk := rootKey{ - ChainSel: root.ChainSel, - SeqNumsRange: root.SeqNumsRange, - MerkleRoot: root.MerkleRoot, - OnRampAddress: typconv.AddressBytesToString(root.OnRampAddress, uint64(root.ChainSel)), - } - - if signedRoots.Contains(rk) { - lggr.Infow("Root is signed, appending to the report", "root", rk) - rootsToReport = append(rootsToReport, root) - } else { - lggr.Infow("Root not signed by RMN, skipping from the report", "root", rk) - } - } - roots = rootsToReport + roots = filterRootsBasedOnRmnSigs( + lggr, q.RMNSignatures.LaneUpdates, roots, consensusObservation.RMNEnabledChains) } outcome := Outcome{ OutcomeType: outcomeType, RootsToReport: roots, + RMNEnabledChains: consensusObservation.RMNEnabledChains, OffRampNextSeqNums: prevOutcome.OffRampNextSeqNums, RMNReportSignatures: sigs, RMNRemoteCfg: prevOutcome.RMNRemoteCfg, @@ -248,6 +215,74 @@ func buildMerkleRootsOutcome( return outcome, nil } +// filterRootsBasedOnRmnSigs filters the roots to only include the ones that are either: +// 1) RMN-enabled and have RMN signatures +// 2) RMN-disabled and do not have RMN signatures +func filterRootsBasedOnRmnSigs( + lggr logger.Logger, + signedLaneUpdates []*rmnpb.FixedDestLaneUpdate, + roots []cciptypes.MerkleRootChain, + rmnEnabledChains map[cciptypes.ChainSelector]bool, +) []cciptypes.MerkleRootChain { + // Create a set of signed roots for quick lookup. + signedRoots := mapset.NewSet[rootKey]() + for _, laneUpdate := range signedLaneUpdates { + rk := rootKey{ + ChainSel: cciptypes.ChainSelector(laneUpdate.LaneSource.SourceChainSelector), + SeqNumsRange: cciptypes.NewSeqNumRange( + cciptypes.SeqNum(laneUpdate.ClosedInterval.MinMsgNr), + cciptypes.SeqNum(laneUpdate.ClosedInterval.MaxMsgNr), + ), + MerkleRoot: cciptypes.Bytes32(laneUpdate.Root), + // NOTE: convert address into a comparable value for mapset. + OnRampAddress: typconv.AddressBytesToString( + laneUpdate.LaneSource.OnrampAddress, + laneUpdate.LaneSource.SourceChainSelector), + } + lggr.Infow("Found signed root", "root", rk) + signedRoots.Add(rk) + } + + validRoots := make([]cciptypes.MerkleRootChain, 0) + for _, root := range roots { + rk := rootKey{ + ChainSel: root.ChainSel, + SeqNumsRange: root.SeqNumsRange, + MerkleRoot: root.MerkleRoot, + OnRampAddress: typconv.AddressBytesToString(root.OnRampAddress, uint64(root.ChainSel)), + } + + rootIsSignedAndRmnEnabled := signedRoots.Contains(rk) && + rmnEnabledChains[root.ChainSel] + + rootNotSignedButRmnDisabled := !signedRoots.Contains(rk) && + !rmnEnabledChains[root.ChainSel] + + if rootIsSignedAndRmnEnabled || rootNotSignedButRmnDisabled { + lggr.Infow("Adding root to the report", + "root", rk, + "rootIsSignedAndRmnEnabled", rootIsSignedAndRmnEnabled, + "rootNotSignedButRmnDisabled", rootNotSignedButRmnDisabled) + validRoots = append(validRoots, root) + } else { + lggr.Infow("Root invalid, skipping from the report", + "root", rk, + "rootIsSignedAndRmnEnabled", rootIsSignedAndRmnEnabled, + "rootNotSignedButRmnDisabled", rootNotSignedButRmnDisabled, + ) + } + } + + return validRoots +} + +type rootKey struct { + ChainSel cciptypes.ChainSelector + SeqNumsRange cciptypes.SeqNumRange + MerkleRoot cciptypes.Bytes32 + OnRampAddress string +} + // checkForReportTransmission checks if the OffRamp has an updated set of max seq nums compared to the seq nums that // were observed when the most recent report was generated. If an update to these max seq sums is detected, it means // that the previous report has been transmitted, and we output ReportTransmitted to dictate that a new report @@ -329,6 +364,7 @@ func getConsensusObservation( twoFChainPlus1 := consensus.MakeMultiThreshold(fChains, consensus.TwoFPlus1) consensusObs := consensusObservation{ MerkleRoots: consensus.GetConsensusMap(lggr, "Merkle Root", aggObs.MerkleRoots, twoFChainPlus1), + RMNEnabledChains: consensus.GetConsensusMap(lggr, "RMNEnabledChains", aggObs.RMNEnabledChains, twoFChainPlus1), OnRampMaxSeqNums: consensus.GetConsensusMap(lggr, "OnRamp Max Seq Nums", aggObs.OnRampMaxSeqNums, twoFChainPlus1), OffRampNextSeqNums: consensus.GetConsensusMap( lggr, diff --git a/commit/merkleroot/outcome_test.go b/commit/merkleroot/outcome_test.go index 430244479..0834cfa95 100644 --- a/commit/merkleroot/outcome_test.go +++ b/commit/merkleroot/outcome_test.go @@ -377,6 +377,14 @@ func Test_Processor_Outcome(t *testing.T) { MerkleRoot: bytes32a, }, }, + RMNEnabledChains: map[cciptypes.ChainSelector]bool{ + chainA: true, + chainB: true, + chainC: true, + chainD: true, + chainE: true, + chainF: true, + }, FChain: map[cciptypes.ChainSelector]int{ chainA: 1, chainB: 1, @@ -416,6 +424,14 @@ func Test_Processor_Outcome(t *testing.T) { MerkleRoot: cciptypes.Bytes32{0xa}, }, }, + RMNEnabledChains: map[cciptypes.ChainSelector]bool{ + chainA: true, + chainB: true, + chainC: true, + chainD: true, + chainE: true, + chainF: true, + }, RMNReportSignatures: []cciptypes.RMNECDSASignature{ {R: bytes32a, S: bytes32b}, {R: bytes32a, S: bytes32b}, diff --git a/commit/merkleroot/query.go b/commit/merkleroot/query.go index a9bebe99d..136b6985a 100644 --- a/commit/merkleroot/query.go +++ b/commit/merkleroot/query.go @@ -12,6 +12,7 @@ import ( "github.com/smartcontractkit/chainlink-ccip/pkg/logutil" ) +//nolint:gocyclo //todo func (p *Processor) Query(ctx context.Context, prevOutcome Outcome) (Query, error) { lggr := logutil.WithContextValues(ctx, p.lggr) @@ -41,8 +42,21 @@ func (p *Processor) Query(ctx context.Context, prevOutcome Outcome) (Query, erro OfframpAddress: offRampAddress, } + rmnEnabledChains, err := p.rmnHomeReader.GetRMNEnabledSourceChains(prevOutcome.RMNRemoteCfg.ConfigDigest) + if err != nil { + return Query{}, fmt.Errorf("get RMN enabled chains %s: %w", + prevOutcome.RMNRemoteCfg.ConfigDigest.String(), err) + } + lggr.Debugw("fetched RMN-enabled chains from rmnHome", "rmnEnabledChains", rmnEnabledChains) + reqUpdates := make([]*rmnpb.FixedDestLaneUpdateRequest, 0, len(prevOutcome.RangesSelectedForReport)) for _, sourceChainRange := range prevOutcome.RangesSelectedForReport { + if !rmnEnabledChains[sourceChainRange.ChainSel] { + lggr.Debugw("chain not RMN-enabled, signatures not requested", + "chain", sourceChainRange.ChainSel, "rmnEnabledChains", rmnEnabledChains) + continue + } + onRampAddress, err := p.ccipReader.GetContractAddress(consts.ContractNameOnRamp, sourceChainRange.ChainSel) if err != nil { lggr.Warnw("failed to get onRamp address", "chain", sourceChainRange.ChainSel, "err", err) @@ -62,8 +76,14 @@ func (p *Processor) Query(ctx context.Context, prevOutcome Outcome) (Query, erro } if len(reqUpdates) == 0 { - lggr.Debugw("no RMN-enabled chains to request signatures, empty query returned") - return Query{}, nil + lggr.Debugw("no RMN-enabled chains to request signatures, empty query returned", + "rmnEnabledChains", rmnEnabledChains) + return Query{ + RMNSignatures: &rmn.ReportSignatures{ + Signatures: []*rmnpb.EcdsaSignature{}, + LaneUpdates: []*rmnpb.FixedDestLaneUpdate{}, + }, + }, nil } ctxQuery, cancel := context.WithTimeout(ctx, p.offchainCfg.RMNSignaturesTimeout) diff --git a/commit/merkleroot/query_test.go b/commit/merkleroot/query_test.go index a0e519732..132ee462f 100644 --- a/commit/merkleroot/query_test.go +++ b/commit/merkleroot/query_test.go @@ -297,10 +297,18 @@ func TestProcessor_Query(t *testing.T) { } } + rmnHomeReader := reader.NewMockRMNHome(t) + rmnHomeReader.EXPECT().GetRMNEnabledSourceChains( + tc.prevOutcome.RMNRemoteCfg.ConfigDigest).Return(map[ccipocr3.ChainSelector]bool{ + srcChain1: true, + srcChain2: true, + }, nil).Maybe() + w := Processor{ offchainCfg: tc.cfg, destChain: tc.destChain, ccipReader: ccipReader, + rmnHomeReader: rmnHomeReader, rmnController: tc.rmnClient(t), lggr: logger.Test(t), metricsReporter: NoopMetrics{}, diff --git a/commit/merkleroot/rmn/types/config.go b/commit/merkleroot/rmn/types/config.go index f5b4ce8bb..914ba4e71 100644 --- a/commit/merkleroot/rmn/types/config.go +++ b/commit/merkleroot/rmn/types/config.go @@ -14,7 +14,9 @@ type NodeID uint32 // HomeConfig contains the configuration fetched from the RMNHome contract. type HomeConfig struct { - Nodes []HomeNodeInfo + Nodes []HomeNodeInfo + // SourceChainF contains the "fObserve" for RMN interactions for each source chain. + // If a chain does not appear in this map, it is assumed that it is not RMN-enabled and signatures are not required. SourceChainF map[cciptypes.ChainSelector]int ConfigDigest cciptypes.Bytes32 OffchainConfig cciptypes.Bytes // The raw offchain config diff --git a/commit/merkleroot/transmission_checks.go b/commit/merkleroot/transmission_checks.go index faadeab90..ac5230d5e 100644 --- a/commit/merkleroot/transmission_checks.go +++ b/commit/merkleroot/transmission_checks.go @@ -7,6 +7,7 @@ import ( mapset "github.com/deckarep/golang-set/v2" + "github.com/smartcontractkit/chainlink-ccip/internal/libs/slicelib" "github.com/smartcontractkit/chainlink-ccip/pkg/reader" cciptypes "github.com/smartcontractkit/chainlink-ccip/pkg/types/ccipocr3" ) @@ -15,13 +16,16 @@ import ( // This function is not-pure as it reads from the chain by making one network/reader call. func ValidateMerkleRootsState( ctx context.Context, - proposedMerkleRoots []cciptypes.MerkleRootChain, + proposedBlessedMerkleRoots []cciptypes.MerkleRootChain, + proposedUnblessedMerkleRoots []cciptypes.MerkleRootChain, reader reader.CCIPReader, ) error { - if len(proposedMerkleRoots) == 0 { + if len(proposedBlessedMerkleRoots) == 0 && len(proposedUnblessedMerkleRoots) == 0 { return nil } + proposedMerkleRoots := append(proposedBlessedMerkleRoots, proposedUnblessedMerkleRoots...) + chainSet := mapset.NewSet[cciptypes.ChainSelector]() newNextOnRampSeqNums := make(map[cciptypes.ChainSelector]cciptypes.SeqNum) @@ -55,5 +59,49 @@ func ValidateMerkleRootsState( } } + return validateRootBlessings(ctx, reader, proposedBlessedMerkleRoots, proposedUnblessedMerkleRoots) +} + +func validateRootBlessings( + ctx context.Context, ccipReader reader.CCIPReader, blessedRoots, unblessedRoots []cciptypes.MerkleRootChain) error { + allSourceChains := make([]cciptypes.ChainSelector, 0, len(blessedRoots)+len(unblessedRoots)) + for _, r := range append(blessedRoots, unblessedRoots...) { + allSourceChains = append(allSourceChains, r.ChainSel) + } + if slicelib.CountUnique(allSourceChains) != len(allSourceChains) { + return fmt.Errorf("duplicate chain in blessed and unblessed roots") + } + + sourceChainsConfig, err := ccipReader.GetOffRampSourceChainsConfig(ctx, allSourceChains) + if err != nil { + return fmt.Errorf("get offRamp source chains config: %w", err) + } + + for _, r := range blessedRoots { + sourceChainCfg, ok := sourceChainsConfig[r.ChainSel] + if !ok { + return fmt.Errorf("chain %d is not in the offRampSourceChainsConfig", r.ChainSel) + } + if sourceChainCfg.IsRMNVerificationDisabled { + return fmt.Errorf("chain %d is RMN-disabled but root is blessed", r.ChainSel) + } + if !sourceChainCfg.IsEnabled { + return fmt.Errorf("chain %d is disabled but root is blessed", r.ChainSel) + } + } + + for _, r := range unblessedRoots { + sourceChainCfg, ok := sourceChainsConfig[r.ChainSel] + if !ok { + return fmt.Errorf("chain %d is not in the offRampSourceChainsConfig", r.ChainSel) + } + if !sourceChainCfg.IsRMNVerificationDisabled { + return fmt.Errorf("chain %d is RMN-enabled but root is unblessed", r.ChainSel) + } + if !sourceChainCfg.IsEnabled { + return fmt.Errorf("chain %d is disabled but root is reported", r.ChainSel) + } + } + return nil } diff --git a/commit/merkleroot/transmission_checks_test.go b/commit/merkleroot/transmission_checks_test.go index 6f7a2b1ed..0e415e8f3 100644 --- a/commit/merkleroot/transmission_checks_test.go +++ b/commit/merkleroot/transmission_checks_test.go @@ -9,10 +9,17 @@ import ( "github.com/smartcontractkit/chainlink-ccip/internal/plugintypes" readermock "github.com/smartcontractkit/chainlink-ccip/mocks/pkg/reader" + reader2 "github.com/smartcontractkit/chainlink-ccip/pkg/reader" cciptypes "github.com/smartcontractkit/chainlink-ccip/pkg/types/ccipocr3" ) func Test_validateMerkleRootsState(t *testing.T) { + sourceChainConfig := map[cciptypes.ChainSelector]reader2.SourceChainConfig{ + 10: {IsRMNVerificationDisabled: false, IsEnabled: true}, + 20: {IsRMNVerificationDisabled: false, IsEnabled: true}, + 30: {IsRMNVerificationDisabled: true, IsEnabled: true}, + } + testCases := []struct { name string onRampNextSeqNum []plugintypes.SeqNumChain @@ -67,6 +74,16 @@ func Test_validateMerkleRootsState(t *testing.T) { readerErr: fmt.Errorf("reader error"), expErr: true, }, + { + name: "happy path one root unblessed", + onRampNextSeqNum: []plugintypes.SeqNumChain{ + plugintypes.NewSeqNumChain(10, 100), + plugintypes.NewSeqNumChain(20, 200), + plugintypes.NewSeqNumChain(30, 300), + }, + offRampExpNextSeqNum: map[cciptypes.ChainSelector]cciptypes.SeqNum{10: 100, 20: 200, 30: 300}, + expErr: false, + }, } ctx := tests.Context(t) @@ -76,15 +93,24 @@ func Test_validateMerkleRootsState(t *testing.T) { rep := cciptypes.CommitPluginReport{} chains := make([]cciptypes.ChainSelector, 0, len(tc.onRampNextSeqNum)) for _, snc := range tc.onRampNextSeqNum { - rep.MerkleRoots = append(rep.MerkleRoots, cciptypes.MerkleRootChain{ - ChainSel: snc.ChainSel, - SeqNumsRange: cciptypes.NewSeqNumRange(snc.SeqNum, snc.SeqNum+10), - }) + if sourceChainConfig[snc.ChainSel].IsRMNVerificationDisabled { + rep.UnblessedMerkleRoots = append(rep.UnblessedMerkleRoots, cciptypes.MerkleRootChain{ + ChainSel: snc.ChainSel, + SeqNumsRange: cciptypes.NewSeqNumRange(snc.SeqNum, snc.SeqNum+10), + }) + } else { + rep.BlessedMerkleRoots = append(rep.BlessedMerkleRoots, cciptypes.MerkleRootChain{ + ChainSel: snc.ChainSel, + SeqNumsRange: cciptypes.NewSeqNumRange(snc.SeqNum, snc.SeqNum+10), + }) + } chains = append(chains, snc.ChainSel) } reader.EXPECT().NextSeqNum(ctx, chains).Return(tc.offRampExpNextSeqNum, tc.readerErr) - err := ValidateMerkleRootsState(ctx, rep.MerkleRoots, reader) + reader.EXPECT().GetOffRampSourceChainsConfig(ctx, chains).Return(sourceChainConfig, nil).Maybe() + + err := ValidateMerkleRootsState(ctx, rep.BlessedMerkleRoots, rep.UnblessedMerkleRoots, reader) if tc.expErr { assert.Error(t, err) return diff --git a/commit/merkleroot/types.go b/commit/merkleroot/types.go index af9305284..f607e14b2 100644 --- a/commit/merkleroot/types.go +++ b/commit/merkleroot/types.go @@ -16,11 +16,12 @@ type Query struct { } type Observation struct { - MerkleRoots []cciptypes.MerkleRootChain `json:"merkleRoots"` - OnRampMaxSeqNums []plugintypes.SeqNumChain `json:"onRampMaxSeqNums"` - OffRampNextSeqNums []plugintypes.SeqNumChain `json:"offRampNextSeqNums"` - RMNRemoteConfig rmntypes.RemoteConfig `json:"rmnRemoteConfig"` - FChain map[cciptypes.ChainSelector]int `json:"fChain"` + MerkleRoots []cciptypes.MerkleRootChain `json:"merkleRoots"` + RMNEnabledChains map[cciptypes.ChainSelector]bool `json:"rmnEnabledChains"` + OnRampMaxSeqNums []plugintypes.SeqNumChain `json:"onRampMaxSeqNums"` + OffRampNextSeqNums []plugintypes.SeqNumChain `json:"offRampNextSeqNums"` + RMNRemoteConfig rmntypes.RemoteConfig `json:"rmnRemoteConfig"` + FChain map[cciptypes.ChainSelector]int `json:"fChain"` } func (o Observation) IsEmpty() bool { @@ -36,6 +37,9 @@ type aggregatedObservation struct { // A map from chain selectors to the list of merkle roots observed for each chain MerkleRoots map[cciptypes.ChainSelector][]cciptypes.MerkleRootChain + // RMNEnabledChains is a map of the RMN-enabled source chains. + RMNEnabledChains map[cciptypes.ChainSelector][]bool + // A map from chain selectors to the list of OnRamp max sequence numbers observed for each chain OnRampMaxSeqNums map[cciptypes.ChainSelector][]cciptypes.SeqNum @@ -53,6 +57,7 @@ type aggregatedObservation struct { func aggregateObservations(aos []plugincommon.AttributedObservation[Observation]) aggregatedObservation { aggObs := aggregatedObservation{ MerkleRoots: make(map[cciptypes.ChainSelector][]cciptypes.MerkleRootChain), + RMNEnabledChains: make(map[cciptypes.ChainSelector][]bool), OnRampMaxSeqNums: make(map[cciptypes.ChainSelector][]cciptypes.SeqNum), OffRampNextSeqNums: make(map[cciptypes.ChainSelector][]cciptypes.SeqNum), RMNRemoteConfigs: make([]rmntypes.RemoteConfig, 0), @@ -67,6 +72,11 @@ func aggregateObservations(aos []plugincommon.AttributedObservation[Observation] append(aggObs.MerkleRoots[merkleRoot.ChainSel], merkleRoot) } + // RMNEnabledChains + for chainSel, enabled := range obs.RMNEnabledChains { + aggObs.RMNEnabledChains[chainSel] = append(aggObs.RMNEnabledChains[chainSel], enabled) + } + // OnRampMaxSeqNums for _, seqNumChain := range obs.OnRampMaxSeqNums { aggObs.OnRampMaxSeqNums[seqNumChain.ChainSel] = @@ -99,6 +109,9 @@ type consensusObservation struct { // A map from chain selectors to each chain's consensus merkle root MerkleRoots map[cciptypes.ChainSelector]cciptypes.MerkleRootChain + // RMNEnabledChains holds the consensus of RMNEnabledChains + RMNEnabledChains map[cciptypes.ChainSelector]bool + // A map from chain selectors to each chain's consensus OnRamp max sequence number OnRampMaxSeqNums map[cciptypes.ChainSelector]cciptypes.SeqNum @@ -124,13 +137,14 @@ const ( ) type Outcome struct { - OutcomeType OutcomeType `json:"outcomeType"` - RangesSelectedForReport []plugintypes.ChainRange `json:"rangesSelectedForReport"` - RootsToReport []cciptypes.MerkleRootChain `json:"rootsToReport"` - OffRampNextSeqNums []plugintypes.SeqNumChain `json:"offRampNextSeqNums"` - ReportTransmissionCheckAttempts uint `json:"reportTransmissionCheckAttempts"` - RMNReportSignatures []cciptypes.RMNECDSASignature `json:"rmnReportSignatures"` - RMNRemoteCfg rmntypes.RemoteConfig `json:"rmnRemoteCfg"` + OutcomeType OutcomeType `json:"outcomeType"` + RangesSelectedForReport []plugintypes.ChainRange `json:"rangesSelectedForReport"` + RootsToReport []cciptypes.MerkleRootChain `json:"rootsToReport"` + RMNEnabledChains map[cciptypes.ChainSelector]bool `json:"rmnEnabledChains"` + OffRampNextSeqNums []plugintypes.SeqNumChain `json:"offRampNextSeqNums"` + ReportTransmissionCheckAttempts uint `json:"reportTransmissionCheckAttempts"` + RMNReportSignatures []cciptypes.RMNECDSASignature `json:"rmnReportSignatures"` + RMNRemoteCfg rmntypes.RemoteConfig `json:"rmnRemoteCfg"` } // Sort all fields of the given Outcome diff --git a/commit/merkleroot/types_test.go b/commit/merkleroot/types_test.go index a0979442c..ac5b4ff2b 100644 --- a/commit/merkleroot/types_test.go +++ b/commit/merkleroot/types_test.go @@ -158,6 +158,7 @@ func TestAggregateObservations(t *testing.T) { OffRampNextSeqNums: make(map[cciptypes.ChainSelector][]cciptypes.SeqNum), RMNRemoteConfigs: make([]rmntypes.RemoteConfig, 0), FChain: make(map[cciptypes.ChainSelector][]int), + RMNEnabledChains: map[cciptypes.ChainSelector][]bool{}, }, }, { @@ -179,6 +180,7 @@ func TestAggregateObservations(t *testing.T) { OffRampNextSeqNums: map[cciptypes.ChainSelector][]cciptypes.SeqNum{1: {1}}, RMNRemoteConfigs: []rmntypes.RemoteConfig{{ContractAddress: cciptypes.UnknownAddress("address")}}, FChain: map[cciptypes.ChainSelector][]int{1: {1}}, + RMNEnabledChains: map[cciptypes.ChainSelector][]bool{}, }, }, { @@ -211,7 +213,8 @@ func TestAggregateObservations(t *testing.T) { {ContractAddress: cciptypes.UnknownAddress("address1")}, {ContractAddress: cciptypes.UnknownAddress("address2")}, }, - FChain: map[cciptypes.ChainSelector][]int{1: {1}, 2: {2}}, + RMNEnabledChains: map[cciptypes.ChainSelector][]bool{}, + FChain: map[cciptypes.ChainSelector][]int{1: {1}, 2: {2}}, }, }, } diff --git a/commit/plugin_e2e_test.go b/commit/plugin_e2e_test.go index 7bc20e70b..a0f52f2c2 100644 --- a/commit/plugin_e2e_test.go +++ b/commit/plugin_e2e_test.go @@ -85,6 +85,11 @@ var ( DeviationPPB: ccipocr3.NewBigInt(big.NewInt(1e5)), Decimals: decimals18, } + + sourceChainConfigs = map[ccipocr3.ChainSelector]reader2.SourceChainConfig{ + sourceChain1: {IsEnabled: true, IsRMNVerificationDisabled: true}, + sourceChain2: {IsEnabled: true, IsRMNVerificationDisabled: true}, + } ) func TestPlugin_E2E_AllNodesAgree_MerkleRoots(t *testing.T) { @@ -121,6 +126,7 @@ func TestPlugin_E2E_AllNodesAgree_MerkleRoots(t *testing.T) { {ChainSel: sourceChain1, SeqNum: 10}, {ChainSel: sourceChain2, SeqNum: 20}, }, + RMNEnabledChains: map[ccipocr3.ChainSelector]bool{}, RMNReportSignatures: []ccipocr3.RMNECDSASignature{}, RMNRemoteCfg: params.rmnReportCfg, }, @@ -157,7 +163,7 @@ func TestPlugin_E2E_AllNodesAgree_MerkleRoots(t *testing.T) { expOutcome: outcomeReportGenerated, expTransmittedReports: []ccipocr3.CommitPluginReport{ { - MerkleRoots: []ccipocr3.MerkleRootChain{ + UnblessedMerkleRoots: []ccipocr3.MerkleRootChain{ { ChainSel: sourceChain1, SeqNumsRange: ccipocr3.NewSeqNumRange(0xa, 0xa), @@ -165,8 +171,9 @@ func TestPlugin_E2E_AllNodesAgree_MerkleRoots(t *testing.T) { MerkleRoot: merkleRoot1, }, }, - PriceUpdates: ccipocr3.PriceUpdates{}, - RMNSignatures: []ccipocr3.RMNECDSASignature{}, + BlessedMerkleRoots: make([]ccipocr3.MerkleRootChain, 0), + PriceUpdates: ccipocr3.PriceUpdates{}, + RMNSignatures: []ccipocr3.RMNECDSASignature{}, }, }, }, @@ -313,6 +320,8 @@ func TestPlugin_E2E_AllNodesAgree_TokenPrices(t *testing.T) { }, }, }, + BlessedMerkleRoots: make([]ccipocr3.MerkleRootChain, 0), + UnblessedMerkleRoots: make([]ccipocr3.MerkleRootChain, 0), }, }, }, @@ -412,6 +421,8 @@ func TestPlugin_E2E_AllNodesAgree_TokenPrices(t *testing.T) { }, }, }, + BlessedMerkleRoots: make([]ccipocr3.MerkleRootChain, 0), + UnblessedMerkleRoots: make([]ccipocr3.MerkleRootChain, 0), }, }, }, @@ -727,6 +738,8 @@ func prepareCcipReaderMock( Return(ccipocr3.Bytes{1}, nil).Maybe() ccipReader.EXPECT().GetRmnCurseInfo(mock.Anything, mock.Anything). Return(&reader2.CurseInfo{}, nil).Maybe() + ccipReader.EXPECT().GetOffRampSourceChainsConfig(mock.Anything, mock.Anything). + Return(sourceChainConfigs, nil).Maybe() if mockEmptySeqNrs { ccipReader.EXPECT().NextSeqNum(mock.Anything, mock.Anything).Unset() @@ -787,6 +800,11 @@ func setupNode(params SetupNodeParams) nodeSetup { homeChainReader := reader_mock.NewMockHomeChain(params.t) rmnHomeReader := readerpkg_mock.NewMockRMNHome(params.t) + rmnHomeReader.EXPECT().GetRMNEnabledSourceChains(mock.Anything).Return(map[ccipocr3.ChainSelector]bool{ + sourceChain1: false, + sourceChain2: false, + }, nil).Maybe() + fChain := map[ccipocr3.ChainSelector]int{} supportedChainsForPeer := make(map[libocrtypes.PeerID]mapset.Set[ccipocr3.ChainSelector]) for chainSel, cfg := range params.chainCfg { @@ -1003,6 +1021,7 @@ func noReportMerkleOutcome(r rmntypes.RemoteConfig) merkleroot.Outcome { OffRampNextSeqNums: []plugintypes.SeqNumChain{}, RMNReportSignatures: []ccipocr3.RMNECDSASignature{}, RMNRemoteCfg: r, + RMNEnabledChains: map[ccipocr3.ChainSelector]bool{}, } } diff --git a/commit/report.go b/commit/report.go index a16632f44..b2beca41f 100644 --- a/commit/report.go +++ b/commit/report.go @@ -5,6 +5,7 @@ import ( "context" "encoding/hex" "fmt" + "sort" "time" "golang.org/x/exp/maps" @@ -44,9 +45,21 @@ func (p *Plugin) Reports( repInfo cciptypes.CommitReportInfo ) + blessedMerkleRoots := make([]cciptypes.MerkleRootChain, 0) + unblessedMerkleRoots := make([]cciptypes.MerkleRootChain, 0) + + for _, r := range outcome.MerkleRootOutcome.RootsToReport { + if outcome.MerkleRootOutcome.RMNEnabledChains[r.ChainSel] { + blessedMerkleRoots = append(blessedMerkleRoots, r) + } else { + unblessedMerkleRoots = append(unblessedMerkleRoots, r) + } + } + // MerkleRoots and RMNSignatures will be empty arrays if there is nothing to report rep = cciptypes.CommitPluginReport{ - MerkleRoots: outcome.MerkleRootOutcome.RootsToReport, + BlessedMerkleRoots: blessedMerkleRoots, + UnblessedMerkleRoots: unblessedMerkleRoots, PriceUpdates: cciptypes.PriceUpdates{ TokenPriceUpdates: outcome.TokenPriceOutcome.TokenPrices.ToSortedSlice(), GasPriceUpdates: outcome.ChainFeeOutcome.GasPrices, @@ -55,14 +68,17 @@ func (p *Plugin) Reports( } if outcome.MerkleRootOutcome.OutcomeType == merkleroot.ReportEmpty { - rep.MerkleRoots = []cciptypes.MerkleRootChain{} + rep.BlessedMerkleRoots = []cciptypes.MerkleRootChain{} + rep.UnblessedMerkleRoots = []cciptypes.MerkleRootChain{} rep.RMNSignatures = []cciptypes.RMNECDSASignature{} } if outcome.MerkleRootOutcome.OutcomeType == merkleroot.ReportGenerated { + allRoots := append(blessedMerkleRoots, unblessedMerkleRoots...) + sort.Slice(allRoots, func(i, j int) bool { return allRoots[i].ChainSel < allRoots[j].ChainSel }) repInfo = cciptypes.CommitReportInfo{ RemoteF: outcome.MerkleRootOutcome.RMNRemoteCfg.FSign, - MerkleRoots: rep.MerkleRoots, + MerkleRoots: allRoots, TokenPrices: rep.PriceUpdates.TokenPriceUpdates, } } @@ -139,7 +155,7 @@ func (p *Plugin) validateReport( } if p.offchainCfg.RMNEnabled && - len(decodedReport.MerkleRoots) > 0 && + len(decodedReport.BlessedMerkleRoots) > 0 && consensus.LtFPlusOne(int(reportInfo.RemoteF), len(decodedReport.RMNSignatures)) { lggr.Infof("report with insufficient RMN signatures %d < %d+1", len(decodedReport.RMNSignatures), reportInfo.RemoteF) @@ -179,10 +195,17 @@ func (p *Plugin) validateReport( return false, cciptypes.CommitPluginReport{}, nil } - err = merkleroot.ValidateMerkleRootsState(ctx, decodedReport.MerkleRoots, p.ccipReader) + err = merkleroot.ValidateMerkleRootsState( + ctx, + decodedReport.BlessedMerkleRoots, + decodedReport.UnblessedMerkleRoots, + p.ccipReader, + ) if err != nil { lggr.Infow("report reached transmission protocol but not transmitted, invalid merkle roots state", - "err", err, "merkleRoots", decodedReport.MerkleRoots) + "err", err, + "blessedMerkleRoots", decodedReport.BlessedMerkleRoots, + "unblessedMerkleRoots", decodedReport.UnblessedMerkleRoots) return false, cciptypes.CommitPluginReport{}, nil } @@ -212,7 +235,8 @@ func (p *Plugin) ShouldAcceptAttestedReport( lggr.Infow("ShouldAcceptedAttestedReport passed checks", "timestamp", time.Now().UTC(), - "rootsLen", len(decodedReport.MerkleRoots), + "blessedRootsLen", len(decodedReport.BlessedMerkleRoots), + "unblessedRootsLen", len(decodedReport.UnblessedMerkleRoots), "tokenPriceUpdatesLen", len(decodedReport.PriceUpdates.TokenPriceUpdates), "gasPriceUpdatesLen", len(decodedReport.PriceUpdates.GasPriceUpdates), ) @@ -241,7 +265,9 @@ func (p *Plugin) isStaleReport( latestPriceSeqNr uint64, decodedReport cciptypes.CommitPluginReport, ) bool { - if seqNr <= latestPriceSeqNr && len(decodedReport.MerkleRoots) == 0 { + if seqNr <= latestPriceSeqNr && + len(decodedReport.BlessedMerkleRoots) == 0 && + len(decodedReport.UnblessedMerkleRoots) == 0 { lggr.Infow( "skipping stale report due to stale price seq nr and no merkle roots", "latestPriceSeqNr", latestPriceSeqNr) @@ -255,10 +281,11 @@ func (p *Plugin) checkReportCursed( lggr logger.Logger, decodedReport cciptypes.CommitPluginReport, ) (bool, error) { - sourceChains := slicelib.Map(decodedReport.MerkleRoots, - func(r cciptypes.MerkleRootChain) cciptypes.ChainSelector { - return r.ChainSel - }) + allRoots := append(decodedReport.BlessedMerkleRoots, decodedReport.UnblessedMerkleRoots...) + + sourceChains := slicelib.Map(allRoots, + func(r cciptypes.MerkleRootChain) cciptypes.ChainSelector { return r.ChainSel }) + isCursed, err := plugincommon.IsReportCursed(ctx, lggr, p.ccipReader, p.chainSupport.DestChain(), sourceChains) if err != nil { lggr.Errorw("report not accepted due to curse checking error", "err", err) @@ -285,7 +312,8 @@ func (p *Plugin) ShouldTransmitAcceptedReport( lggr.Infow("ShouldTransmitAcceptedReport passed checks", "seqNr", seqNr, "timestamp", time.Now().UTC(), - "rootsLen", len(decodedReport.MerkleRoots), + "blessedRootsLen", len(decodedReport.BlessedMerkleRoots), + "unblessedRootsLen", len(decodedReport.UnblessedMerkleRoots), "tokenPriceUpdatesLen", len(decodedReport.PriceUpdates.TokenPriceUpdates), "gasPriceUpdatesLen", len(decodedReport.PriceUpdates.GasPriceUpdates), ) diff --git a/commit/report_test.go b/commit/report_test.go index 38ac855d2..887e95aeb 100644 --- a/commit/report_test.go +++ b/commit/report_test.go @@ -77,7 +77,9 @@ func TestPluginReports(t *testing.T) { {GasPrice: ccipocr3.NewBigIntFromInt64(3), ChainSel: 123}, }, }, - RMNSignatures: nil, + RMNSignatures: nil, + UnblessedMerkleRoots: make([]ccipocr3.MerkleRootChain, 0), + BlessedMerkleRoots: make([]ccipocr3.MerkleRootChain, 0), }, }, expReportInfo: ccipocr3.CommitReportInfo{}, @@ -99,7 +101,9 @@ func TestPluginReports(t *testing.T) { {GasPrice: ccipocr3.NewBigIntFromInt64(3), ChainSel: 123}, }, }, - RMNSignatures: nil, + UnblessedMerkleRoots: make([]ccipocr3.MerkleRootChain, 0), + BlessedMerkleRoots: make([]ccipocr3.MerkleRootChain, 0), + RMNSignatures: nil, }, }, expReportInfo: ccipocr3.CommitReportInfo{}, @@ -117,8 +121,15 @@ func TestPluginReports(t *testing.T) { SeqNumsRange: ccipocr3.NewSeqNumRange(10, 20), MerkleRoot: ccipocr3.Bytes32{1, 2, 3, 4, 5, 6}, }, + { + ChainSel: 2, + OnRampAddress: []byte{1, 2, 3}, + SeqNumsRange: ccipocr3.NewSeqNumRange(110, 210), + MerkleRoot: ccipocr3.Bytes32{1, 2, 3, 4, 5, 6, 7}, + }, }, - RMNRemoteCfg: rmntypes.RemoteConfig{FSign: 123}, + RMNRemoteCfg: rmntypes.RemoteConfig{FSign: 123}, + RMNEnabledChains: map[ccipocr3.ChainSelector]bool{3: true, 2: false}, }, TokenPriceOutcome: tokenprice.Outcome{ TokenPrices: ccipocr3.TokenPriceMap{ @@ -133,7 +144,7 @@ func TestPluginReports(t *testing.T) { }, expReports: []ccipocr3.CommitPluginReport{ { - MerkleRoots: []ccipocr3.MerkleRootChain{ + BlessedMerkleRoots: []ccipocr3.MerkleRootChain{ { ChainSel: 3, OnRampAddress: []byte{1, 2, 3}, @@ -141,6 +152,14 @@ func TestPluginReports(t *testing.T) { MerkleRoot: ccipocr3.Bytes32{1, 2, 3, 4, 5, 6}, }, }, + UnblessedMerkleRoots: []ccipocr3.MerkleRootChain{ + { + ChainSel: 2, + OnRampAddress: []byte{1, 2, 3}, + SeqNumsRange: ccipocr3.NewSeqNumRange(110, 210), + MerkleRoot: ccipocr3.Bytes32{1, 2, 3, 4, 5, 6, 7}, + }, + }, PriceUpdates: ccipocr3.PriceUpdates{ TokenPriceUpdates: []ccipocr3.TokenPrice{ {TokenID: "a", Price: ccipocr3.NewBigIntFromInt64(123)}, @@ -155,6 +174,12 @@ func TestPluginReports(t *testing.T) { expReportInfo: ccipocr3.CommitReportInfo{ RemoteF: 123, MerkleRoots: []ccipocr3.MerkleRootChain{ + { + ChainSel: 2, + OnRampAddress: []byte{1, 2, 3}, + SeqNumsRange: ccipocr3.NewSeqNumRange(110, 210), + MerkleRoot: ccipocr3.Bytes32{1, 2, 3, 4, 5, 6, 7}, + }, { ChainSel: 3, OnRampAddress: []byte{1, 2, 3}, @@ -265,7 +290,7 @@ func Test_Plugin_isStaleReport(t *testing.T) { lggr: logger.Test(t), } report := ccipocr3.CommitPluginReport{ - MerkleRoots: make([]ccipocr3.MerkleRootChain, tc.lenMerkleRoots), + BlessedMerkleRoots: make([]ccipocr3.MerkleRootChain, tc.lenMerkleRoots), } stale := p.isStaleReport(p.lggr, tc.reportSeqNum, tc.onChainSeqNum, report) require.Equal(t, tc.shouldBeStale, stale) diff --git a/execute/plugin_functions.go b/execute/plugin_functions.go index cfecf48b0..645802149 100644 --- a/execute/plugin_functions.go +++ b/execute/plugin_functions.go @@ -267,7 +267,8 @@ func groupByChainSelector( reports []plugintypes2.CommitPluginReportWithMeta) exectypes.CommitObservations { commitReportCache := make(map[cciptypes.ChainSelector][]exectypes.CommitData) for _, report := range reports { - for _, singleReport := range report.Report.MerkleRoots { + merkleRoots := append(report.Report.BlessedMerkleRoots, report.Report.UnblessedMerkleRoots...) + for _, singleReport := range merkleRoots { commitReportCache[singleReport.ChainSel] = append(commitReportCache[singleReport.ChainSel], exectypes.CommitData{ SourceChain: singleReport.ChainSel, diff --git a/execute/plugin_functions_test.go b/execute/plugin_functions_test.go index 0b1d92562..dae51a417 100644 --- a/execute/plugin_functions_test.go +++ b/execute/plugin_functions_test.go @@ -424,7 +424,7 @@ func Test_groupByChainSelector(t *testing.T) { name: "reports", args: args{reports: []plugintypes2.CommitPluginReportWithMeta{{ Report: cciptypes.CommitPluginReport{ - MerkleRoots: []cciptypes.MerkleRootChain{ + BlessedMerkleRoots: []cciptypes.MerkleRootChain{ {ChainSel: 1, SeqNumsRange: cciptypes.NewSeqNumRange(10, 20), MerkleRoot: cciptypes.Bytes32{1}}, {ChainSel: 2, SeqNumsRange: cciptypes.NewSeqNumRange(30, 40), MerkleRoot: cciptypes.Bytes32{2}}, }}}}}, diff --git a/execute/plugin_test.go b/execute/plugin_test.go index 44c7feaac..8ef77e7be 100644 --- a/execute/plugin_test.go +++ b/execute/plugin_test.go @@ -69,7 +69,7 @@ func Test_getPendingExecutedReports(t *testing.T) { BlockNum: 999, Timestamp: time.UnixMilli(10101010101), Report: cciptypes.CommitPluginReport{ - MerkleRoots: []cciptypes.MerkleRootChain{ + BlessedMerkleRoots: []cciptypes.MerkleRootChain{ { ChainSel: 1, SeqNumsRange: cciptypes.NewSeqNumRange(1, 10), @@ -100,7 +100,7 @@ func Test_getPendingExecutedReports(t *testing.T) { BlockNum: 999, Timestamp: time.UnixMilli(10101010101), Report: cciptypes.CommitPluginReport{ - MerkleRoots: []cciptypes.MerkleRootChain{ + BlessedMerkleRoots: []cciptypes.MerkleRootChain{ { ChainSel: 1, SeqNumsRange: cciptypes.NewSeqNumRange(1, 10), @@ -150,7 +150,7 @@ func Test_getPendingExecutedReports(t *testing.T) { BlockNum: 999, Timestamp: time.UnixMilli(10101010101), Report: cciptypes.CommitPluginReport{ - MerkleRoots: []cciptypes.MerkleRootChain{ + BlessedMerkleRoots: []cciptypes.MerkleRootChain{ { ChainSel: 1, SeqNumsRange: cciptypes.NewSeqNumRange(1, 10), @@ -182,7 +182,7 @@ func Test_getPendingExecutedReports(t *testing.T) { BlockNum: 999, Timestamp: time.UnixMilli(10101010101), Report: cciptypes.CommitPluginReport{ - MerkleRoots: []cciptypes.MerkleRootChain{ + BlessedMerkleRoots: []cciptypes.MerkleRootChain{ { ChainSel: 1, SeqNumsRange: cciptypes.NewSeqNumRange(1, 10), diff --git a/execute/test_utils.go b/execute/test_utils.go index b2a1166b8..b4631b454 100644 --- a/execute/test_utils.go +++ b/execute/test_utils.go @@ -131,7 +131,7 @@ func (it *IntTest) WithMessages( it.ccipReader.Reports = append(it.ccipReader.Reports, plugintypes2.CommitPluginReportWithMeta{ Report: cciptypes.CommitPluginReport{ - MerkleRoots: []cciptypes.MerkleRootChain{ + BlessedMerkleRoots: []cciptypes.MerkleRootChain{ { ChainSel: reportData.SourceChain, SeqNumsRange: reportData.SequenceNumberRange, diff --git a/internal/mocks/inmem/ccipreader_inmem.go b/internal/mocks/inmem/ccipreader_inmem.go index 93fa42d8b..98f9903da 100644 --- a/internal/mocks/inmem/ccipreader_inmem.go +++ b/internal/mocks/inmem/ccipreader_inmem.go @@ -206,5 +206,10 @@ func (r InMemoryCCIPReader) GetOffRampConfigDigest(ctx context.Context, pluginTy return r.ConfigDigest, nil } +func (r InMemoryCCIPReader) GetOffRampSourceChainsConfig(ctx context.Context, chains []cciptypes.ChainSelector, +) (map[cciptypes.ChainSelector]reader.SourceChainConfig, error) { + return nil, nil +} + // Interface compatibility check. var _ reader.CCIPReader = InMemoryCCIPReader{} diff --git a/mocks/pkg/reader/ccip_reader.go b/mocks/pkg/reader/ccip_reader.go index ffe77a4f5..5e7020b10 100644 --- a/mocks/pkg/reader/ccip_reader.go +++ b/mocks/pkg/reader/ccip_reader.go @@ -655,6 +655,65 @@ func (_c *MockCCIPReader_GetOffRampConfigDigest_Call) RunAndReturn(run func(cont return _c } +// GetOffRampSourceChainsConfig provides a mock function with given fields: ctx, sourceChains +func (_m *MockCCIPReader) GetOffRampSourceChainsConfig(ctx context.Context, sourceChains []ccipocr3.ChainSelector) (map[ccipocr3.ChainSelector]reader.SourceChainConfig, error) { + ret := _m.Called(ctx, sourceChains) + + if len(ret) == 0 { + panic("no return value specified for GetOffRampSourceChainsConfig") + } + + var r0 map[ccipocr3.ChainSelector]reader.SourceChainConfig + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, []ccipocr3.ChainSelector) (map[ccipocr3.ChainSelector]reader.SourceChainConfig, error)); ok { + return rf(ctx, sourceChains) + } + if rf, ok := ret.Get(0).(func(context.Context, []ccipocr3.ChainSelector) map[ccipocr3.ChainSelector]reader.SourceChainConfig); ok { + r0 = rf(ctx, sourceChains) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(map[ccipocr3.ChainSelector]reader.SourceChainConfig) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, []ccipocr3.ChainSelector) error); ok { + r1 = rf(ctx, sourceChains) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockCCIPReader_GetOffRampSourceChainsConfig_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetOffRampSourceChainsConfig' +type MockCCIPReader_GetOffRampSourceChainsConfig_Call struct { + *mock.Call +} + +// GetOffRampSourceChainsConfig is a helper method to define mock.On call +// - ctx context.Context +// - sourceChains []ccipocr3.ChainSelector +func (_e *MockCCIPReader_Expecter) GetOffRampSourceChainsConfig(ctx interface{}, sourceChains interface{}) *MockCCIPReader_GetOffRampSourceChainsConfig_Call { + return &MockCCIPReader_GetOffRampSourceChainsConfig_Call{Call: _e.mock.On("GetOffRampSourceChainsConfig", ctx, sourceChains)} +} + +func (_c *MockCCIPReader_GetOffRampSourceChainsConfig_Call) Run(run func(ctx context.Context, sourceChains []ccipocr3.ChainSelector)) *MockCCIPReader_GetOffRampSourceChainsConfig_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].([]ccipocr3.ChainSelector)) + }) + return _c +} + +func (_c *MockCCIPReader_GetOffRampSourceChainsConfig_Call) Return(_a0 map[ccipocr3.ChainSelector]reader.SourceChainConfig, _a1 error) *MockCCIPReader_GetOffRampSourceChainsConfig_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockCCIPReader_GetOffRampSourceChainsConfig_Call) RunAndReturn(run func(context.Context, []ccipocr3.ChainSelector) (map[ccipocr3.ChainSelector]reader.SourceChainConfig, error)) *MockCCIPReader_GetOffRampSourceChainsConfig_Call { + _c.Call.Return(run) + return _c +} + // GetRMNRemoteConfig provides a mock function with given fields: ctx func (_m *MockCCIPReader) GetRMNRemoteConfig(ctx context.Context) (rmntypes.RemoteConfig, error) { ret := _m.Called(ctx) diff --git a/mocks/pkg/reader/rmn_home.go b/mocks/pkg/reader/rmn_home.go index 1f4d8084b..229c00d0b 100644 --- a/mocks/pkg/reader/rmn_home.go +++ b/mocks/pkg/reader/rmn_home.go @@ -245,6 +245,64 @@ func (_c *MockRMNHome_GetOffChainConfig_Call) RunAndReturn(run func(ccipocr3.Byt return _c } +// GetRMNEnabledSourceChains provides a mock function with given fields: configDigest +func (_m *MockRMNHome) GetRMNEnabledSourceChains(configDigest ccipocr3.Bytes32) (map[ccipocr3.ChainSelector]bool, error) { + ret := _m.Called(configDigest) + + if len(ret) == 0 { + panic("no return value specified for GetRMNEnabledSourceChains") + } + + var r0 map[ccipocr3.ChainSelector]bool + var r1 error + if rf, ok := ret.Get(0).(func(ccipocr3.Bytes32) (map[ccipocr3.ChainSelector]bool, error)); ok { + return rf(configDigest) + } + if rf, ok := ret.Get(0).(func(ccipocr3.Bytes32) map[ccipocr3.ChainSelector]bool); ok { + r0 = rf(configDigest) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(map[ccipocr3.ChainSelector]bool) + } + } + + if rf, ok := ret.Get(1).(func(ccipocr3.Bytes32) error); ok { + r1 = rf(configDigest) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockRMNHome_GetRMNEnabledSourceChains_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetRMNEnabledSourceChains' +type MockRMNHome_GetRMNEnabledSourceChains_Call struct { + *mock.Call +} + +// GetRMNEnabledSourceChains is a helper method to define mock.On call +// - configDigest ccipocr3.Bytes32 +func (_e *MockRMNHome_Expecter) GetRMNEnabledSourceChains(configDigest interface{}) *MockRMNHome_GetRMNEnabledSourceChains_Call { + return &MockRMNHome_GetRMNEnabledSourceChains_Call{Call: _e.mock.On("GetRMNEnabledSourceChains", configDigest)} +} + +func (_c *MockRMNHome_GetRMNEnabledSourceChains_Call) Run(run func(configDigest ccipocr3.Bytes32)) *MockRMNHome_GetRMNEnabledSourceChains_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(ccipocr3.Bytes32)) + }) + return _c +} + +func (_c *MockRMNHome_GetRMNEnabledSourceChains_Call) Return(_a0 map[ccipocr3.ChainSelector]bool, _a1 error) *MockRMNHome_GetRMNEnabledSourceChains_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockRMNHome_GetRMNEnabledSourceChains_Call) RunAndReturn(run func(ccipocr3.Bytes32) (map[ccipocr3.ChainSelector]bool, error)) *MockRMNHome_GetRMNEnabledSourceChains_Call { + _c.Call.Return(run) + return _c +} + // GetRMNNodesInfo provides a mock function with given fields: configDigest func (_m *MockRMNHome) GetRMNNodesInfo(configDigest ccipocr3.Bytes32) ([]types.HomeNodeInfo, error) { ret := _m.Called(configDigest) diff --git a/pkg/reader/ccip.go b/pkg/reader/ccip.go index 059dcf1f2..efd7daff8 100644 --- a/pkg/reader/ccip.go +++ b/pkg/reader/ccip.go @@ -113,8 +113,9 @@ type PriceUpdates struct { } type CommitReportAcceptedEvent struct { - MerkleRoots []MerkleRoot - PriceUpdates PriceUpdates + BlessedMerkleRoots []MerkleRoot + UnblessedMerkleRoots []MerkleRoot + PriceUpdates PriceUpdates } // --------------------------------------------------- @@ -166,8 +167,14 @@ func (r *ccipChainReader) CommitReportsGTETimestamp( lggr.Debugw("processing commit report", "report", ev, "item", item) - merkleRoots := make([]cciptypes.MerkleRootChain, 0, len(ev.MerkleRoots)) - for _, mr := range ev.MerkleRoots { + isBlessed := make(map[cciptypes.Bytes32]bool, len(ev.BlessedMerkleRoots)) + for _, mr := range ev.BlessedMerkleRoots { + isBlessed[mr.MerkleRoot] = true + } + allMerkleRoots := append(ev.BlessedMerkleRoots, ev.UnblessedMerkleRoots...) + blessedMerkleRoots := make([]cciptypes.MerkleRootChain, 0, len(ev.BlessedMerkleRoots)) + unblessedMerkleRoots := make([]cciptypes.MerkleRootChain, 0, len(ev.UnblessedMerkleRoots)) + for _, mr := range allMerkleRoots { onRampAddress, err := r.GetContractAddress( consts.ContractNameOnRamp, cciptypes.ChainSelector(mr.SourceChainSelector), @@ -177,7 +184,7 @@ func (r *ccipChainReader) CommitReportsGTETimestamp( continue } - merkleRoots = append(merkleRoots, cciptypes.MerkleRootChain{ + mrc := cciptypes.MerkleRootChain{ ChainSel: cciptypes.ChainSelector(mr.SourceChainSelector), OnRampAddress: onRampAddress, SeqNumsRange: cciptypes.NewSeqNumRange( @@ -185,7 +192,12 @@ func (r *ccipChainReader) CommitReportsGTETimestamp( cciptypes.SeqNum(mr.MaxSeqNr), ), MerkleRoot: mr.MerkleRoot, - }) + } + if isBlessed[mr.MerkleRoot] { + blessedMerkleRoots = append(blessedMerkleRoots, mrc) + } else { + unblessedMerkleRoots = append(unblessedMerkleRoots, mrc) + } } priceUpdates := cciptypes.PriceUpdates{ @@ -215,8 +227,9 @@ func (r *ccipChainReader) CommitReportsGTETimestamp( reports = append(reports, plugintypes2.CommitPluginReportWithMeta{ Report: cciptypes.CommitPluginReport{ - MerkleRoots: merkleRoots, - PriceUpdates: priceUpdates, + BlessedMerkleRoots: blessedMerkleRoots, + UnblessedMerkleRoots: unblessedMerkleRoots, + PriceUpdates: priceUpdates, }, Timestamp: time.Unix(int64(item.Timestamp), 0), BlockNum: blockNum, @@ -443,7 +456,7 @@ func (r *ccipChainReader) NextSeqNum( ctx context.Context, chains []cciptypes.ChainSelector, ) (map[cciptypes.ChainSelector]cciptypes.SeqNum, error) { lggr := logutil.WithContextValues(ctx, r.lggr) - cfgs, err := r.getOffRampSourceChainsConfig(ctx, lggr, chains) + cfgs, err := r.getOffRampSourceChainsConfig(ctx, lggr, chains, false) if err != nil { return nil, fmt.Errorf("get source chains config: %w", err) } @@ -865,7 +878,7 @@ func (r *ccipChainReader) discoverOffRampContracts( // OnRamps are in the offRamp SourceChainConfig. { - sourceConfigs, err := r.getOffRampSourceChainsConfig(ctx, lggr, chains) + sourceConfigs, err := r.getOffRampSourceChainsConfig(ctx, lggr, chains, false) if err != nil { return nil, fmt.Errorf("unable to get SourceChainsConfig: %w", err) @@ -1120,14 +1133,15 @@ func (r *ccipChainReader) getFeeQuoterTokenPriceUSD(ctx context.Context, tokenAd // See: https://github.com/smartcontractkit/ccip/blob/a3f61f7458e4499c2c62eb38581c60b4942b1160/contracts/src/v0.8/ccip/offRamp/OffRamp.sol#L94 // //nolint:lll // It's a URL. -type sourceChainConfig struct { - Router []byte // local router - IsEnabled bool - MinSeqNr uint64 - OnRamp cciptypes.UnknownAddress +type SourceChainConfig struct { + Router []byte // local router + IsEnabled bool + IsRMNVerificationDisabled bool + MinSeqNr uint64 + OnRamp cciptypes.UnknownAddress } -func (scc sourceChainConfig) check() (bool /* enabled */, error) { +func (scc SourceChainConfig) check() (bool /* enabled */, error) { // The chain may be set in CCIPHome's ChainConfig map but not hooked up yet in the offramp. if !scc.IsEnabled { return false, nil @@ -1148,17 +1162,26 @@ func (scc sourceChainConfig) check() (bool /* enabled */, error) { return scc.IsEnabled, nil } +// GetOffRampSourceChainsConfig returns all the source chains configs including disabled chains. +func (r *ccipChainReader) GetOffRampSourceChainsConfig(ctx context.Context, chains []cciptypes.ChainSelector, +) (map[cciptypes.ChainSelector]SourceChainConfig, error) { + return r.getOffRampSourceChainsConfig(ctx, r.lggr, chains, true) +} + // getOffRampSourceChainsConfig get all enabled source chain configs from the offRamp for dest chain +// +//nolint:revive func (r *ccipChainReader) getOffRampSourceChainsConfig( ctx context.Context, lggr logger.Logger, chains []cciptypes.ChainSelector, -) (map[cciptypes.ChainSelector]sourceChainConfig, error) { + includeDisabled bool, +) (map[cciptypes.ChainSelector]SourceChainConfig, error) { if err := validateExtendedReaderExistence(r.contractReaders, r.destChain); err != nil { return nil, fmt.Errorf("validate extended reader existence: %w", err) } - configs := make(map[cciptypes.ChainSelector]sourceChainConfig) + configs := make(map[cciptypes.ChainSelector]SourceChainConfig) contractBatch := make(types.ContractBatch, 0, len(chains)) sourceChains := make([]any, 0, len(chains)) @@ -1173,7 +1196,7 @@ func (r *ccipChainReader) getOffRampSourceChainsConfig( Params: map[string]any{ "sourceChainSelector": chain, }, - ReturnVal: new(sourceChainConfig), + ReturnVal: new(SourceChainConfig), }) } @@ -1201,7 +1224,7 @@ func (r *ccipChainReader) getOffRampSourceChainsConfig( return nil, fmt.Errorf("GetSourceChainConfig for chainSelector=%d failed: %w", chainSel, err) } - cfg, ok := v.(*sourceChainConfig) + cfg, ok := v.(*SourceChainConfig) if !ok { return nil, fmt.Errorf("invalid result type from GetSourceChainConfig for chainSelector=%d: %w", chainSel, err) } @@ -1210,7 +1233,7 @@ func (r *ccipChainReader) getOffRampSourceChainsConfig( if err != nil { return nil, fmt.Errorf("source chain config check for chain %d failed: %w", chainSel, err) } - if !enabled { + if !enabled && !includeDisabled { // We don't want to process disabled chains prematurely. lggr.Debugw("source chain is disabled", "chain", chainSel) continue @@ -1604,7 +1627,7 @@ func validateCommitReportAcceptedEvent(seq types.Sequence, gteTimestamp time.Tim seq.Timestamp, gteTimestamp.Unix()) } - if err := validateMerkleRoots(ev.MerkleRoots); err != nil { + if err := validateMerkleRoots(append(ev.BlessedMerkleRoots, ev.UnblessedMerkleRoots...)); err != nil { return nil, fmt.Errorf("merkle roots: %w", err) } @@ -1630,7 +1653,14 @@ func validateCommitReportAcceptedEvent(seq types.Sequence, gteTimestamp time.Tim } func validateMerkleRoots(merkleRoots []MerkleRoot) error { + seenRoots := mapset.NewSet[cciptypes.Bytes32]() + for _, mr := range merkleRoots { + if seenRoots.Contains(mr.MerkleRoot) { + return fmt.Errorf("duplicate merkle root: %s", mr.MerkleRoot.String()) + } + seenRoots.Add(mr.MerkleRoot) + if mr.SourceChainSelector == 0 { return fmt.Errorf("source chain is zero") } diff --git a/pkg/reader/ccip_interface.go b/pkg/reader/ccip_interface.go index 9d2ac262a..78ffb1217 100644 --- a/pkg/reader/ccip_interface.go +++ b/pkg/reader/ccip_interface.go @@ -175,4 +175,9 @@ type CCIPReader interface { // GetOffRampConfigDigest returns the offramp config digest for the provided plugin type. GetOffRampConfigDigest(ctx context.Context, pluginType uint8) ([32]byte, error) + + // GetOffRampSourceChainsConfig returns the sourceChains config for all the provided source chains. + // If a config was not found it will be missing from the returned map. + GetOffRampSourceChainsConfig(ctx context.Context, sourceChains []cciptypes.ChainSelector, + ) (map[cciptypes.ChainSelector]SourceChainConfig, error) } diff --git a/pkg/reader/ccip_test.go b/pkg/reader/ccip_test.go index e786bc751..b2d2a31f6 100644 --- a/pkg/reader/ccip_test.go +++ b/pkg/reader/ccip_test.go @@ -63,7 +63,7 @@ func TestCCIPChainReader_getSourceChainsConfig(t *testing.T) { } params := readReq.Params.(map[string]any) sourceChain := params["sourceChainSelector"].(cciptypes.ChainSelector) - v := readReq.ReturnVal.(*sourceChainConfig) + v := readReq.ReturnVal.(*SourceChainConfig) fromString, err := cciptypes.NewBytesFromString(fmt.Sprintf( "0x%d000000000000000000000000000000000000000", sourceChain), @@ -100,7 +100,8 @@ func TestCCIPChainReader_getSourceChainsConfig(t *testing.T) { Address: typeconv.AddressBytesToString(offrampAddress, 111_111)}})) ctx := context.Background() - cfgs, err := ccipReader.getOffRampSourceChainsConfig(ctx, logger.Test(t), []cciptypes.ChainSelector{chainA, chainB}) + cfgs, err := ccipReader.getOffRampSourceChainsConfig( + ctx, logger.Test(t), []cciptypes.ChainSelector{chainA, chainB}, false) assert.NoError(t, err) assert.Len(t, cfgs, 2) assert.Equal(t, "0x1000000000000000000000000000000000000000", cfgs[chainA].OnRamp.String()) @@ -442,9 +443,9 @@ func TestCCIPChainReader_DiscoverContracts_HappyPath_Round1(t *testing.T) { srcRouters := [][]byte{{0x7}, {0x8}} //srcFeeQuoters := [2][]byte{{0x7}, {0x8}} - sourceChainConfigs := make(map[cciptypes.ChainSelector]sourceChainConfig, len(sourceChain)) + sourceChainConfigs := make(map[cciptypes.ChainSelector]SourceChainConfig, len(sourceChain)) for i, chain := range sourceChain { - sourceChainConfigs[chain] = sourceChainConfig{ + sourceChainConfigs[chain] = SourceChainConfig{ Router: srcRouters[i], IsEnabled: true, MinSeqNr: 0, @@ -475,11 +476,11 @@ func TestCCIPChainReader_DiscoverContracts_HappyPath_Round1(t *testing.T) { mock.Anything, ).RunAndReturn(withBatchGetLatestValuesRetValues(t, "0x1234567890123456789012345678901234567890", - []any{&sourceChainConfig{ + []any{&SourceChainConfig{ OnRamp: onramps[0], Router: destRouter, IsEnabled: true, - }, &sourceChainConfig{ + }, &SourceChainConfig{ OnRamp: onramps[1], Router: destRouter, IsEnabled: true, @@ -611,12 +612,12 @@ func TestCCIPChainReader_DiscoverContracts_HappyPath_Round2(t *testing.T) { mock.Anything, ).RunAndReturn(withBatchGetLatestValuesRetValues(t, "0x1234567890123456789012345678901234567890", - []any{&sourceChainConfig{ + []any{&SourceChainConfig{ OnRamp: onramps[0], Router: destRouter[0], IsEnabled: true, }, - &sourceChainConfig{ + &SourceChainConfig{ OnRamp: onramps[1], Router: destRouter[1], IsEnabled: true, @@ -722,7 +723,7 @@ func TestCCIPChainReader_DiscoverContracts_GetOfframpStaticConfig_Errors(t *test mock.Anything, ).RunAndReturn(withBatchGetLatestValuesRetValues(t, "0x1234567890123456789012345678901234567890", - []any{&sourceChainConfig{}, &sourceChainConfig{}})) + []any{&SourceChainConfig{}, &SourceChainConfig{}})) // mock the call to get the static config - failure getLatestValueErr := errors.New("some error") @@ -786,7 +787,7 @@ func withBatchGetLatestValuesRetValues( ctx context.Context, req contractreader.ExtendedBatchGetLatestValuesRequest, graceful bool, ) (types.BatchGetLatestValuesResult, []string, error) { require.GreaterOrEqual(t, len(retVals), 1) - _, ok := retVals[0].(*sourceChainConfig) + _, ok := retVals[0].(*SourceChainConfig) require.True(t, ok) require.Len(t, req, 1) contract := maps.Keys(req)[0] diff --git a/pkg/reader/rmn_home.go b/pkg/reader/rmn_home.go index 142fe5c05..29431499e 100644 --- a/pkg/reader/rmn_home.go +++ b/pkg/reader/rmn_home.go @@ -29,6 +29,9 @@ type RMNHome interface { GetRMNNodesInfo(configDigest cciptypes.Bytes32) ([]rmntypes.HomeNodeInfo, error) // IsRMNHomeConfigDigestSet checks if the configDigest is set in the RMNHome contract IsRMNHomeConfigDigestSet(configDigest cciptypes.Bytes32) bool + // GetRMNEnabledSourceChains gets the RMN-enabled source chains for the given configDigest. + // If a chain is not RMN-enabled it means that we don't need to do RMN signature related operations for that chain. + GetRMNEnabledSourceChains(configDigest cciptypes.Bytes32) (map[cciptypes.ChainSelector]bool, error) // GetFObserve gets the F value for each source chain in the given configDigest. // Maximum number of faulty observers; F+1 observers required to agree on an observation for a source chain. GetFObserve(configDigest cciptypes.Bytes32) (map[cciptypes.ChainSelector]int, error) @@ -117,6 +120,24 @@ func (r *rmnHome) GetFObserve(configDigest cciptypes.Bytes32) (map[cciptypes.Cha return state.rmnHomeConfig[configDigest].SourceChainF, nil } +// GetRMNEnabledSourceChains returns the source chains that are RMN-enabled. A chain is considered RMN-enabled if +// F is present in RMNHome config. +func (r *rmnHome) GetRMNEnabledSourceChains(configDigest cciptypes.Bytes32) (map[cciptypes.ChainSelector]bool, error) { + state := r.bgPoller.getRMNHomeState() + homeCfg, ok := state.rmnHomeConfig[configDigest] + if !ok { + return map[cciptypes.ChainSelector]bool{}, + fmt.Errorf("configDigest %s not found in RMNHomeConfig", configDigest) + } + + enabledChains := make(map[cciptypes.ChainSelector]bool, len(homeCfg.SourceChainF)) + for chain := range homeCfg.SourceChainF { + enabledChains[chain] = true + } + + return enabledChains, nil +} + func (r *rmnHome) GetOffChainConfig(configDigest cciptypes.Bytes32) (cciptypes.Bytes, error) { state := r.bgPoller.getRMNHomeState() cfg, ok := state.rmnHomeConfig[configDigest] diff --git a/pkg/reader/rmn_home_poller.go b/pkg/reader/rmn_home_poller.go index c79bf5ff5..ad02e6d90 100644 --- a/pkg/reader/rmn_home_poller.go +++ b/pkg/reader/rmn_home_poller.go @@ -317,15 +317,15 @@ func convertOnChainConfigToRMNHomeChainConfig( } rmnHomeConfigs := make(map[cciptypes.Bytes32]rmntypes.HomeConfig) - for _, versionedConfig := range versionedConfigWithDigests { - err := validate(versionedConfig) + for _, cfg := range versionedConfigWithDigests { + err := validate(cfg) if err != nil { lggr.Warnw("invalid on chain RMNHomeConfig", "err", err) continue } - nodes := make([]rmntypes.HomeNodeInfo, len(versionedConfig.StaticConfig.Nodes)) - for i, node := range versionedConfig.StaticConfig.Nodes { + nodes := make([]rmntypes.HomeNodeInfo, len(cfg.StaticConfig.Nodes)) + for i, node := range cfg.StaticConfig.Nodes { pubKey := ed25519.PublicKey(node.OffchainPublicKey[:]) nodes[i] = rmntypes.HomeNodeInfo{ @@ -337,9 +337,9 @@ func convertOnChainConfigToRMNHomeChainConfig( } } - homeFMap := make(map[cciptypes.ChainSelector]int) + homeFMap := make(map[cciptypes.ChainSelector]int, len(cfg.DynamicConfig.SourceChains)) - for _, chain := range versionedConfig.DynamicConfig.SourceChains { + for _, chain := range cfg.DynamicConfig.SourceChains { homeFMap[chain.ChainSelector] = int(chain.FObserve) for j := 0; j < len(nodes); j++ { isObserver, err := isNodeObserver(chain, j, len(nodes)) @@ -353,11 +353,11 @@ func convertOnChainConfigToRMNHomeChainConfig( } } - rmnHomeConfigs[versionedConfig.ConfigDigest] = rmntypes.HomeConfig{ + rmnHomeConfigs[cfg.ConfigDigest] = rmntypes.HomeConfig{ Nodes: nodes, SourceChainF: homeFMap, - ConfigDigest: versionedConfig.ConfigDigest, - OffchainConfig: versionedConfig.DynamicConfig.OffchainConfig, + ConfigDigest: cfg.ConfigDigest, + OffchainConfig: cfg.DynamicConfig.OffchainConfig, } } return rmnHomeConfigs diff --git a/pkg/types/ccipocr3/plugin_commit_types.go b/pkg/types/ccipocr3/plugin_commit_types.go index b2c8da1af..956d5f22a 100644 --- a/pkg/types/ccipocr3/plugin_commit_types.go +++ b/pkg/types/ccipocr3/plugin_commit_types.go @@ -21,8 +21,9 @@ import ( // RMNSignatures, if RMN is configured for some lanes involved in the commitment. // A report with RMN signatures but without merkle roots is invalid. type CommitPluginReport struct { - PriceUpdates PriceUpdates `json:"priceUpdates"` - MerkleRoots []MerkleRootChain `json:"merkleRoots"` + PriceUpdates PriceUpdates `json:"priceUpdates"` + BlessedMerkleRoots []MerkleRootChain `json:"blessedMerkleRoots"` + UnblessedMerkleRoots []MerkleRootChain `json:"unblessedMerkleRoots"` // RMNSignatures are the ECDSA signatures from the RMN signing nodes on the RMNReport structure. // For more details see the contract here: https://github.com/smartcontractkit/chainlink/blob/7ba0f37134a618375542079ff1805fe2224d7916/contracts/src/v0.8/ccip/interfaces/IRMNV2.sol#L8-L12 @@ -33,7 +34,8 @@ type CommitPluginReport struct { // IsEmpty returns true if the CommitPluginReport is empty. // NOTE: A report is considered empty when core fields are missing (MerkleRoots, TokenPrices, GasPriceUpdates). func (r CommitPluginReport) IsEmpty() bool { - return len(r.MerkleRoots) == 0 && + return len(r.BlessedMerkleRoots) == 0 && + len(r.UnblessedMerkleRoots) == 0 && len(r.PriceUpdates.TokenPriceUpdates) == 0 && len(r.PriceUpdates.GasPriceUpdates) == 0 } @@ -66,7 +68,7 @@ type PriceUpdates struct { GasPriceUpdates []GasPriceChain `json:"gasPriceUpdates"` } -// ReportInfo is the info data that will be sent with the along with the report +// CommitReportInfo is the info data that will be sent with the along with the report // It will be used to determine if the report should be accepted or not type CommitReportInfo struct { // RemoteF Max number of faulty RMN nodes; f+1 signers are required to verify a report. diff --git a/pkg/types/ccipocr3/plugin_commit_types_test.go b/pkg/types/ccipocr3/plugin_commit_types_test.go index 91c7e6be9..9f9a9cd21 100644 --- a/pkg/types/ccipocr3/plugin_commit_types_test.go +++ b/pkg/types/ccipocr3/plugin_commit_types_test.go @@ -23,7 +23,7 @@ func TestCommitPluginReport(t *testing.T) { t.Run("is not empty", func(t *testing.T) { r := CommitPluginReport{ - MerkleRoots: make([]MerkleRootChain, 1), + BlessedMerkleRoots: make([]MerkleRootChain, 1), } assert.False(t, r.IsEmpty()) @@ -36,7 +36,7 @@ func TestCommitPluginReport(t *testing.T) { assert.False(t, r.IsEmpty()) r = CommitPluginReport{ - MerkleRoots: make([]MerkleRootChain, 1), + BlessedMerkleRoots: make([]MerkleRootChain, 1), PriceUpdates: PriceUpdates{ TokenPriceUpdates: make([]TokenPrice, 1), GasPriceUpdates: make([]GasPriceChain, 1),