Skip to content

Commit

Permalink
Source chain specific RMN blessings enable/disable (#550)
Browse files Browse the repository at this point in the history
  • Loading branch information
dimkouv authored Feb 7, 2025
1 parent 540f826 commit 27a940e
Show file tree
Hide file tree
Showing 30 changed files with 606 additions and 159 deletions.
6 changes: 3 additions & 3 deletions commit/factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions commit/factory_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -251,16 +251,16 @@ 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),
},
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),
Expand Down
22 changes: 18 additions & 4 deletions commit/merkleroot/observation.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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{
Expand Down
8 changes: 7 additions & 1 deletion commit/merkleroot/observation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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},
},
},
{
Expand Down Expand Up @@ -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)

Expand Down
136 changes: 86 additions & 50 deletions commit/merkleroot/outcome.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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,
Expand Down
16 changes: 16 additions & 0 deletions commit/merkleroot/outcome_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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},
Expand Down
24 changes: 22 additions & 2 deletions commit/merkleroot/query.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down
8 changes: 8 additions & 0 deletions commit/merkleroot/query_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{},
Expand Down
4 changes: 3 additions & 1 deletion commit/merkleroot/rmn/types/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading

0 comments on commit 27a940e

Please sign in to comment.