diff --git a/commit_rmn_ocb/outcome.go b/commit_rmn_ocb/outcome.go index 53104d12..38a41553 100644 --- a/commit_rmn_ocb/outcome.go +++ b/commit_rmn_ocb/outcome.go @@ -73,10 +73,9 @@ func (p *Plugin) ReportRangesOutcome( "offRampMaxSeqNum", offRampMaxSeqNum, "onRampMaxSeqNum", onRampMaxSeqNum, "chainSelector", chainSel) - continue - } else if offRampMaxSeqNum == onRampMaxSeqNum { - continue - } else { + } + + if offRampMaxSeqNum < onRampMaxSeqNum { chainRange := ChainRange{ ChainSel: chainSel, SeqNumRange: [2]cciptypes.SeqNum{offRampMaxSeqNum, onRampMaxSeqNum}, @@ -85,6 +84,7 @@ func (p *Plugin) ReportRangesOutcome( } } + // TODO: explain this sort.Slice(rangesToReport, func(i, j int) bool { return rangesToReport[i].ChainSel < rangesToReport[j].ChainSel }) outcome := CommitPluginOutcome{ @@ -95,20 +95,55 @@ func (p *Plugin) ReportRangesOutcome( return outcome.Encode() } -// TODO: doc +// TODO: doc, break apart func (p *Plugin) buildReport( query CommitQuery, consensusObservation ConsensusObservation, ) (ocr3types.Outcome, error) { - if query.SignedMerkleRoots == nil || len(query.SignedMerkleRoots) == 0 { - // TODO: metrics - return ocr3types.Outcome{}, fmt.Errorf("buildReport: query.SignedMerkleRoots is empty") + verifiedSignedRoots := p.verifySignedRoots(query.SignedMerkleRoots, consensusObservation.MerkleRoots) + + chainsToExclude := make([]cciptypes.ChainSelector, 0) + for chain := range verifiedSignedRoots { + if _, exists := consensusObservation.GasPrices[chain]; !exists { + // TODO: metrics + p.log.Warnw( + "did not find a consensus gas price for chain %d, excluding it from the report", chain) + chainsToExclude = append(chainsToExclude, chain) + } } - observedMerkleRoots := consensusObservation.MerkleRoots + for _, chainSelector := range chainsToExclude { + delete(verifiedSignedRoots, chainSelector) + } + // TODO: token prices validation + // exclude merkle roots if expected token prices don't exist + + verifiedSignedRootsArray := make([]SignedMerkleRoot, 0, len(verifiedSignedRoots)) + for _, signedRoot := range verifiedSignedRoots { + verifiedSignedRootsArray = append(verifiedSignedRootsArray, signedRoot) + } + + // TODO: explain this + sort.Slice(verifiedSignedRootsArray, func(i, j int) bool { + return verifiedSignedRootsArray[i].chain() < verifiedSignedRootsArray[j].chain() + }) + + return CommitPluginOutcome{ + OutcomeType: ReportGenerated, + SignedRootsToReport: verifiedSignedRootsArray, + GasPrices: consensusObservation.GasPricesSortedArray(), + TokenPrices: consensusObservation.TokenPricesSortedArray(), + }.Encode() +} + +// TODO: doc, break apart +func (p *Plugin) verifySignedRoots( + rmnSignedRoots []SignedMerkleRoot, + observedRoots map[cciptypes.ChainSelector]MerkleRoot, +) map[cciptypes.ChainSelector]SignedMerkleRoot { verifiedSignedRoots := make(map[cciptypes.ChainSelector]SignedMerkleRoot) - for _, signedRoot := range query.SignedMerkleRoots { + for _, signedRoot := range rmnSignedRoots { if err := p.rmn.VerifySignedMerkleRoot(signedRoot); err != nil { // TODO: metrics p.log.Warnw("failed to verify signed merkle root", @@ -117,16 +152,14 @@ func (p *Plugin) buildReport( continue } - if observedMerkleRoot, exists := observedMerkleRoots[signedRoot.chain()]; exists { - // check merkle root equality - if observedMerkleRoot != signedRoot.MerkleRoot { + if observedMerkleRoot, exists := observedRoots[signedRoot.chain()]; exists { + if observedMerkleRoot == signedRoot.MerkleRoot { + verifiedSignedRoots[signedRoot.chain()] = signedRoot + } else { // TODO: metrics p.log.Warnw("observed merkle root does not match merkle root received from RMN", "rmnSignedRoot", signedRoot, "observedMerkleRoot", observedMerkleRoot) - continue - } else { - } } else { // TODO: metrics @@ -134,13 +167,11 @@ func (p *Plugin) buildReport( "received a signed merkle root from RMN for a chain, but did not observe a merkle root for "+ "this chain", "rmnSignedRoot", signedRoot) - continue } - - verifiedSignedRoots[signedRoot.chain()] = signedRoot } - for chain, observedMerkleRoot := range observedMerkleRoots { + // TODO: explain this + for chain, observedMerkleRoot := range observedRoots { if _, exists := verifiedSignedRoots[chain]; !exists { if p.rmn.ChainThreshold(chain) == 0 { verifiedSignedRoots[chain] = SignedMerkleRoot{ @@ -156,23 +187,7 @@ func (p *Plugin) buildReport( } } - chainsToExclude := make([]cciptypes.ChainSelector, 0) - for chain, _ := range verifiedSignedRoots { - if _, exists := consensusObservation.GasPrices[chain]; !exists { - // TODO: metrics - p.log.Warnw( - "did not find a consensus gas price for chain %d, excluding it from the report", chain) - chainsToExclude = append(chainsToExclude, chain) - } - } - - for _, chainSelector := range chainsToExclude { - delete(verifiedSignedRoots, chainSelector) - } - - // TODO: token prices validation - - return nil, nil + return verifiedSignedRoots } // TODO: doc @@ -193,21 +208,21 @@ func (p *Plugin) checkForReportTransmission( if offRampUpdated { return CommitPluginOutcome{ - OutcomeType: CommitPluginOutcomeType(ReportGenerated), + OutcomeType: ReportGenerated, }.Encode() - } else { - if previousOutcome.ReportTransmissionCheckAttempts+1 >= p.cfg.MaxReportTransmissionCheckAttempts { - return CommitPluginOutcome{ - OutcomeType: CommitPluginOutcomeType(ReportNotTransmitted), - }.Encode() - } else { - return CommitPluginOutcome{ - OutcomeType: CommitPluginOutcomeType(ReportNotYetTransmitted), - OffRampMaxSeqNums: previousOutcome.OffRampMaxSeqNums, - ReportTransmissionCheckAttempts: previousOutcome.ReportTransmissionCheckAttempts + 1, - }.Encode() - } } + + if previousOutcome.ReportTransmissionCheckAttempts+1 >= p.cfg.MaxReportTransmissionCheckAttempts { + return CommitPluginOutcome{ + OutcomeType: ReportNotTransmitted, + }.Encode() + } + + return CommitPluginOutcome{ + OutcomeType: ReportNotYetTransmitted, + OffRampMaxSeqNums: previousOutcome.OffRampMaxSeqNums, + ReportTransmissionCheckAttempts: previousOutcome.ReportTransmissionCheckAttempts + 1, + }.Encode() } // getConsensusObservation TODO: doc @@ -401,11 +416,7 @@ func mostFrequentElem[T comparable](elems []T) (T, int) { func counts[T comparable](elems []T) map[T]int { m := make(map[T]int) for _, elem := range elems { - if _, exists := m[elem]; exists { - m[elem]++ - } else { - m[elem] = 1 - } + m[elem]++ } return m diff --git a/commit_rmn_ocb/plugin.go b/commit_rmn_ocb/plugin.go index f8c94d09..ed2f67c9 100644 --- a/commit_rmn_ocb/plugin.go +++ b/commit_rmn_ocb/plugin.go @@ -57,7 +57,7 @@ func NewPlugin( // SelectingRangesForReport doesn't depend on the previous outcome, explain how this is resilient (to being unable // to parse previous outcome) func (p *Plugin) decodeOutcome(outcome ocr3types.Outcome) (CommitPluginOutcome, CommitPluginState) { - if outcome == nil || len(outcome) == 0 { + if len(outcome) == 0 { return CommitPluginOutcome{}, SelectingRangesForReport } diff --git a/commit_rmn_ocb/report.go b/commit_rmn_ocb/report.go new file mode 100644 index 00000000..9fd8c255 --- /dev/null +++ b/commit_rmn_ocb/report.go @@ -0,0 +1,82 @@ +package commitrmnocb + +import ( + "context" + "fmt" + + "github.com/smartcontractkit/libocr/offchainreporting2plus/ocr3types" +) + +// Reports TODO: doc, metrics +func (p *Plugin) Reports(seqNr uint64, outcomeBytes ocr3types.Outcome) ([]ocr3types.ReportWithInfo[[]byte], error) { + outcome, err := DecodeCommitPluginOutcome(outcomeBytes) + if err != nil { + // TODO: metrics + p.log.Errorw("failed to decode CommitPluginOutcome", "outcomeBytes", outcomeBytes, "err", err) + return nil, fmt.Errorf("failed to decode CommitPluginOutcome: %w", err) + } + + report := CommitPluginReport{ + SignedRoots: outcome.SignedRootsToReport, + GasPrices: outcome.GasPrices, + TokenPrices: outcome.TokenPrices, + } + + // TODO: log, metrics + + encodedReport, err := report.Encode() + if err != nil { + return nil, fmt.Errorf("encode commit plugin report: %w", err) + } + + return []ocr3types.ReportWithInfo[[]byte]{{Report: encodedReport, Info: nil}}, nil +} + +func (p *Plugin) ShouldAcceptAttestedReport( + _ context.Context, _ uint64, r ocr3types.ReportWithInfo[[]byte], +) (bool, error) { + decodedReport, err := DecodeCommitPluginReport(r.Report) + if err != nil { + // TODO: metrics + p.log.Errorw("failed to decode CommitPluginOutcome", "outcomeBytes", r.Report, "err", err) + return false, err + } + + if decodedReport.IsEmpty() { + // TODO: metrics + p.log.Warnf("found an empty report") + return false, nil + } + + return true, nil +} + +func (p *Plugin) ShouldTransmitAcceptedReport( + _ context.Context, _ uint64, r ocr3types.ReportWithInfo[[]byte], +) (bool, error) { + destChainSupported, err := p.supportsDestChain(p.nodeID) + if err != nil { + return false, fmt.Errorf("call to supportsDestChain failed: %w", err) + } + if !destChainSupported { + p.log.Debugw("oracle does not support dest chain, skipping report transmission") + return false, nil + } + + decodedReport, err := DecodeCommitPluginReport(r.Report) + if err != nil { + return false, fmt.Errorf("decode commit plugin report: %w", err) + } + + // TODO: metrics + p.log.Debugw("transmitting report", + "signedRoots", len(decodedReport.SignedRoots), + "tokenPrices", len(decodedReport.TokenPrices), + "gasPriceUpdates", len(decodedReport.GasPrices), + ) + return true, nil +} + +func (p *Plugin) Close() error { + return nil +} diff --git a/commit_rmn_ocb/types.go b/commit_rmn_ocb/types.go index 07386fb1..30f9e876 100644 --- a/commit_rmn_ocb/types.go +++ b/commit_rmn_ocb/types.go @@ -3,6 +3,7 @@ package commitrmnocb import ( "encoding/json" "fmt" + "sort" "github.com/smartcontractkit/libocr/offchainreporting2plus/types" @@ -138,47 +139,43 @@ func aggregateObservations(aos []types.AttributedObservation) AggregatedObservat // MerkleRoots for _, merkleRoot := range obs.MerkleRoots { - AppendToMap(aggObs.MerkleRoots, merkleRoot.ChainSel, merkleRoot) + aggObs.MerkleRoots[merkleRoot.ChainSel] = + append(aggObs.MerkleRoots[merkleRoot.ChainSel], merkleRoot) } // GasPrices for _, gasPriceChain := range obs.GasPrices { - AppendToMap(aggObs.GasPrices, gasPriceChain.ChainSel, gasPriceChain.GasPrice) + aggObs.GasPrices[gasPriceChain.ChainSel] = + append(aggObs.GasPrices[gasPriceChain.ChainSel], gasPriceChain.GasPrice) } // TokenPrices for _, tokenPrice := range obs.TokenPrices { - AppendToMap(aggObs.TokenPrices, tokenPrice.TokenID, tokenPrice.Price) + aggObs.TokenPrices[tokenPrice.TokenID] = + append(aggObs.TokenPrices[tokenPrice.TokenID], tokenPrice.Price) } // OnRampMaxSeqNums for _, seqNumChain := range obs.OnRampMaxSeqNums { - AppendToMap(aggObs.OnRampMaxSeqNums, seqNumChain.ChainSel, seqNumChain.SeqNum) + aggObs.OnRampMaxSeqNums[seqNumChain.ChainSel] = + append(aggObs.OnRampMaxSeqNums[seqNumChain.ChainSel], seqNumChain.SeqNum) } // OffRampMaxSeqNums for _, seqNumChain := range obs.OffRampMaxSeqNums { - AppendToMap(aggObs.OffRampMaxSeqNums, seqNumChain.ChainSel, seqNumChain.SeqNum) + aggObs.OffRampMaxSeqNums[seqNumChain.ChainSel] = + append(aggObs.OffRampMaxSeqNums[seqNumChain.ChainSel], seqNumChain.SeqNum) } // FChain for chainSel, f := range obs.FChain { - AppendToMap(aggObs.FChain, chainSel, f) + aggObs.FChain[chainSel] = append(aggObs.FChain[chainSel], f) } } return aggObs } -// AppendToMap TODO: doc -func AppendToMap[K comparable, V any](m map[K][]V, k K, v V) { - if _, exists := m[k]; exists { - m[k] = append(m[k], v) - } else { - m[k] = []V{v} - } -} - // ConsensusObservation TODO: doc type ConsensusObservation struct { // A map from chain selectors to each chain's consensus merkle root @@ -200,6 +197,36 @@ type ConsensusObservation struct { FChain map[cciptypes.ChainSelector]int } +// GasPricesSortedArray TODO: doc +func (co ConsensusObservation) GasPricesSortedArray() []cciptypes.GasPriceChain { + gasPrices := make([]cciptypes.GasPriceChain, 0, len(co.GasPrices)) + for chain, gasPrice := range co.GasPrices { + gasPrices = append(gasPrices, cciptypes.NewGasPriceChain(gasPrice.Int, chain)) + } + + // TODO: explain this + sort.Slice(gasPrices, func(i, j int) bool { + return gasPrices[i].ChainSel < gasPrices[j].ChainSel + }) + + return gasPrices +} + +// TokenPricesSortedArray TODO: doc +func (co ConsensusObservation) TokenPricesSortedArray() []cciptypes.TokenPrice { + tokenPrices := make([]cciptypes.TokenPrice, 0, len(co.TokenPrices)) + for tokenID, tokenPrice := range co.TokenPrices { + tokenPrices = append(tokenPrices, cciptypes.NewTokenPrice(tokenID, tokenPrice.Int)) + } + + // TODO: explain this + sort.Slice(tokenPrices, func(i, j int) bool { + return tokenPrices[i].TokenID < tokenPrices[j].TokenID + }) + + return tokenPrices +} + type CommitPluginOutcomeType int const ( @@ -284,3 +311,28 @@ type OnChain interface { GetOffRampMaxSeqNums() ([]plugintypes.SeqNumChain, error) GetMerkleRoots([]ChainRange) ([]MerkleRoot, error) } + +type CommitPluginReport struct { + SignedRoots []SignedMerkleRoot + TokenPrices []cciptypes.TokenPrice `json:"tokenPrices"` + GasPrices []cciptypes.GasPriceChain `json:"gasPrices"` +} + +func (r CommitPluginReport) IsEmpty() bool { + return len(r.SignedRoots) == 0 && len(r.TokenPrices) == 0 && len(r.GasPrices) == 0 +} + +func (r CommitPluginReport) Encode() ([]byte, error) { + encodedReport, err := json.Marshal(r) + if err != nil { + return nil, fmt.Errorf("failed to encode CommitPluginReport: %w", err) + } + + return encodedReport, nil +} + +func DecodeCommitPluginReport(b []byte) (CommitPluginReport, error) { + r := CommitPluginReport{} + err := json.Unmarshal(b, &r) + return r, err +} diff --git a/commit_rmn_ocb/validate_observation.go b/commit_rmn_ocb/validate_observation.go index 947c812b..c8ceae61 100644 --- a/commit_rmn_ocb/validate_observation.go +++ b/commit_rmn_ocb/validate_observation.go @@ -50,7 +50,7 @@ func (p *Plugin) ValidateObservation(_ ocr3types.OutcomeContext, _ types.Query, // validateMerkleRoots TODO: doc // No duplicate chains, only contains chainSelector that the owner can read func (p *Plugin) validateObservedMerkleRoots(merkleRoots []MerkleRoot, observer commontypes.OracleID) error { - if merkleRoots == nil || len(merkleRoots) == 0 { + if len(merkleRoots) == 0 { return nil } @@ -81,7 +81,7 @@ func (p *Plugin) validateObservedOnRampMaxSeqNums( onRampMaxSeqNums []plugintypes.SeqNumChain, observer commontypes.OracleID, ) error { - if onRampMaxSeqNums == nil || len(onRampMaxSeqNums) == 0 { + if len(onRampMaxSeqNums) == 0 { return nil } @@ -112,7 +112,7 @@ func (p *Plugin) validateObservedOffRampMaxSeqNums( offRampMaxSeqNums []plugintypes.SeqNumChain, observer commontypes.OracleID, ) error { - if offRampMaxSeqNums == nil || len(offRampMaxSeqNums) == 0 { + if len(offRampMaxSeqNums) == 0 { return nil } @@ -139,7 +139,7 @@ func (p *Plugin) validateObservedOffRampMaxSeqNums( // validateFChains TODO: doc // FChain must not be empty func (p *Plugin) validateFChain(fchain map[cciptypes.ChainSelector]int) error { - if fchain == nil || len(fchain) == 0 { + if len(fchain) == 0 { return fmt.Errorf("fchain map is empty") }