From 5ea19a7d34057a1d9353a38afda5256cf53bdc39 Mon Sep 17 00:00:00 2001 From: Connor Stein Date: Thu, 28 Sep 2023 08:59:58 -0400 Subject: [PATCH] Version abstraction (#151) --- .../plugins/ccip/abihelpers/abi_helpers.go | 137 +------ .../ocr2/plugins/ccip/commit_plugin.go | 179 ++++----- .../ocr2/plugins/ccip/commit_plugin_test.go | 30 +- .../plugins/ccip/commit_reporting_plugin.go | 75 ++-- .../ccip/commit_reporting_plugin_test.go | 65 ++-- .../plugins/ccip/config/type_and_version.go | 4 +- .../plugins/ccip/execution_batch_building.go | 17 +- .../ocr2/plugins/ccip/execution_plugin.go | 196 ++++------ .../plugins/ccip/execution_plugin_test.go | 25 +- .../ccip/execution_reporting_plugin.go | 29 +- .../ccip/execution_reporting_plugin_test.go | 110 +++--- .../ccip/internal/ccipdata/logpoller.go | 122 +----- .../ccip/internal/ccipdata/logpoller_test.go | 110 ------ .../ccip/internal/ccipdata/onramp_reader.go | 106 ++++++ .../internal/ccipdata/onramp_reader_mock.go | 140 +++++++ .../ccip/internal/ccipdata/onramp_v1_0_0.go | 254 +++++++++++++ .../onramp_v1_0_0_test.go} | 48 +-- .../ccip/internal/ccipdata/onramp_v1_1_0.go | 38 ++ .../ccip/internal/ccipdata/onramp_v1_2_0.go | 349 ++++++++++++++++++ .../internal/ccipdata/onramp_v1_2_0_test.go | 147 ++++++++ .../plugins/ccip/internal/ccipdata/reader.go | 19 +- .../ccipdata/{mock.go => reader_mock.go} | 80 ---- .../ccip/internal/ccipdata/usdc_reader.go | 97 +++++ .../internal/ccipdata/usdc_reader_mock.go | 71 ++++ .../internal/ccipdata/usdc_reader_test.go | 78 ++++ .../plugins/ccip/internal/hashlib/common.go | 35 -- .../ccip/internal/hashlib/leaf_hasher.go | 106 ------ .../ccip/testhelpers/ccip_contracts.go | 15 +- .../ccip/testhelpers/integration/chainlink.go | 6 +- .../ocr2/plugins/ccip/tokendata/reader.go | 5 +- .../plugins/ccip/tokendata/reader_mock.go | 14 +- .../ocr2/plugins/ccip/tokendata/usdc/usdc.go | 66 +--- .../ccip/tokendata/usdc/usdc_blackbox_test.go | 89 +---- .../plugins/ccip/tokendata/usdc/usdc_test.go | 63 ++-- 34 files changed, 1675 insertions(+), 1250 deletions(-) create mode 100644 core/services/ocr2/plugins/ccip/internal/ccipdata/onramp_reader.go create mode 100644 core/services/ocr2/plugins/ccip/internal/ccipdata/onramp_reader_mock.go create mode 100644 core/services/ocr2/plugins/ccip/internal/ccipdata/onramp_v1_0_0.go rename core/services/ocr2/plugins/ccip/internal/{hashlib/leaf_hasher_test.go => ccipdata/onramp_v1_0_0_test.go} (54%) create mode 100644 core/services/ocr2/plugins/ccip/internal/ccipdata/onramp_v1_1_0.go create mode 100644 core/services/ocr2/plugins/ccip/internal/ccipdata/onramp_v1_2_0.go create mode 100644 core/services/ocr2/plugins/ccip/internal/ccipdata/onramp_v1_2_0_test.go rename core/services/ocr2/plugins/ccip/internal/ccipdata/{mock.go => reader_mock.go} (69%) create mode 100644 core/services/ocr2/plugins/ccip/internal/ccipdata/usdc_reader.go create mode 100644 core/services/ocr2/plugins/ccip/internal/ccipdata/usdc_reader_mock.go create mode 100644 core/services/ocr2/plugins/ccip/internal/ccipdata/usdc_reader_test.go delete mode 100644 core/services/ocr2/plugins/ccip/internal/hashlib/leaf_hasher.go diff --git a/core/services/ocr2/plugins/ccip/abihelpers/abi_helpers.go b/core/services/ocr2/plugins/ccip/abihelpers/abi_helpers.go index 1ed681f271..5eedc30242 100644 --- a/core/services/ocr2/plugins/ccip/abihelpers/abi_helpers.go +++ b/core/services/ocr2/plugins/ccip/abihelpers/abi_helpers.go @@ -14,7 +14,6 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/commit_store" "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/evm_2_evm_offramp" - "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/evm_2_evm_onramp" "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/price_registry" "github.com/smartcontractkit/chainlink/v2/core/utils" ) @@ -29,9 +28,8 @@ const ( ExecutionStateFailure ) +// TODO: Deprecate in favour of version specific types var EventSignatures struct { - // OnRamp - SendRequested common.Hash // CommitStore ReportAccepted common.Hash // OffRamp @@ -45,10 +43,6 @@ var EventSignatures struct { FeeTokenAdded common.Hash FeeTokenRemoved common.Hash - USDCMessageSent common.Hash - - // offset || sourceChainID || seqNum || ... - SendRequestedSequenceNumberWord int // offset || priceUpdatesOffset || minSeqNum || maxSeqNum || merkleRoot ReportAcceptedMaxSequenceNumberWord int // sig || seqNum || messageId || ... @@ -62,7 +56,7 @@ var ( ExecutionReportArgs abi.Arguments ) -func getIDOrPanic(name string, abi2 abi.ABI) common.Hash { +func GetIDOrPanic(name string, abi2 abi.ABI) common.Hash { event, ok := abi2.Events[name] if !ok { panic(fmt.Sprintf("missing event %s", name)) @@ -70,58 +64,31 @@ func getIDOrPanic(name string, abi2 abi.ABI) common.Hash { return event.ID } -func getTupleNamedElem(name string, arg abi.Argument) *abi.Type { - if arg.Type.T != abi.TupleTy { - return nil - } - for i, elem := range arg.Type.TupleElems { - if arg.Type.TupleRawNames[i] == name { - return elem - } - } - return nil -} - func init() { - onRampABI, err := abi.JSON(strings.NewReader(evm_2_evm_onramp.EVM2EVMOnRampABI)) - if err != nil { - panic(err) - } - EventSignatures.SendRequested = getIDOrPanic("CCIPSendRequested", onRampABI) - EventSignatures.SendRequestedSequenceNumberWord = 4 - commitStoreABI, err := abi.JSON(strings.NewReader(commit_store.CommitStoreABI)) if err != nil { panic(err) } - EventSignatures.ReportAccepted = getIDOrPanic("ReportAccepted", commitStoreABI) + EventSignatures.ReportAccepted = GetIDOrPanic("ReportAccepted", commitStoreABI) EventSignatures.ReportAcceptedMaxSequenceNumberWord = 3 offRampABI, err := abi.JSON(strings.NewReader(evm_2_evm_offramp.EVM2EVMOffRampABI)) if err != nil { panic(err) } - EventSignatures.ExecutionStateChanged = getIDOrPanic("ExecutionStateChanged", offRampABI) + EventSignatures.ExecutionStateChanged = GetIDOrPanic("ExecutionStateChanged", offRampABI) EventSignatures.ExecutionStateChangedSequenceNumberIndex = 1 - EventSignatures.PoolAdded = getIDOrPanic("PoolAdded", offRampABI) - EventSignatures.PoolRemoved = getIDOrPanic("PoolRemoved", offRampABI) + EventSignatures.PoolAdded = GetIDOrPanic("PoolAdded", offRampABI) + EventSignatures.PoolRemoved = GetIDOrPanic("PoolRemoved", offRampABI) priceRegistryABI, err := abi.JSON(strings.NewReader(price_registry.PriceRegistryABI)) if err != nil { panic(err) } - EventSignatures.UsdPerUnitGasUpdated = getIDOrPanic("UsdPerUnitGasUpdated", priceRegistryABI) - EventSignatures.UsdPerTokenUpdated = getIDOrPanic("UsdPerTokenUpdated", priceRegistryABI) - EventSignatures.FeeTokenAdded = getIDOrPanic("FeeTokenAdded", priceRegistryABI) - EventSignatures.FeeTokenRemoved = getIDOrPanic("FeeTokenRemoved", priceRegistryABI) - - // arguments - MessageArgs = onRampABI.Events["CCIPSendRequested"].Inputs - tokenAmountsTy := getTupleNamedElem("tokenAmounts", MessageArgs[0]) - if tokenAmountsTy == nil { - panic(fmt.Sprintf("missing component '%s' in tuple %+v", "tokenAmounts", MessageArgs)) - } - TokenAmountsArgs = abi.Arguments{{Type: *tokenAmountsTy, Name: "tokenAmounts"}} + EventSignatures.UsdPerUnitGasUpdated = GetIDOrPanic("UsdPerUnitGasUpdated", priceRegistryABI) + EventSignatures.UsdPerTokenUpdated = GetIDOrPanic("UsdPerTokenUpdated", priceRegistryABI) + EventSignatures.FeeTokenAdded = GetIDOrPanic("FeeTokenAdded", priceRegistryABI) + EventSignatures.FeeTokenRemoved = GetIDOrPanic("FeeTokenRemoved", priceRegistryABI) CommitReportArgs = commitStoreABI.Events["ReportAccepted"].Inputs @@ -130,8 +97,6 @@ func init() { panic("missing event 'manuallyExecute'") } ExecutionReportArgs = manuallyExecuteMethod.Inputs[:1] - - EventSignatures.USDCMessageSent = utils.Keccak256Fixed([]byte("MessageSent(bytes)")) } func MessagesFromExecutionReport(report types.Report) ([]evm_2_evm_offramp.InternalEVM2EVMMessage, error) { @@ -142,88 +107,6 @@ func MessagesFromExecutionReport(report types.Report) ([]evm_2_evm_offramp.Inter return decodedExecutionReport.Messages, nil } -func DecodeOffRampMessage(b []byte) (*evm_2_evm_offramp.InternalEVM2EVMMessage, error) { - unpacked, err := MessageArgs.Unpack(b) - if err != nil { - return nil, err - } - if len(unpacked) == 0 { - return nil, fmt.Errorf("no message found when unpacking") - } - - // Note must use unnamed type here - receivedCp, ok := unpacked[0].(struct { - SourceChainSelector uint64 `json:"sourceChainSelector"` - Sender common.Address `json:"sender"` - Receiver common.Address `json:"receiver"` - SequenceNumber uint64 `json:"sequenceNumber"` - GasLimit *big.Int `json:"gasLimit"` - Strict bool `json:"strict"` - Nonce uint64 `json:"nonce"` - FeeToken common.Address `json:"feeToken"` - FeeTokenAmount *big.Int `json:"feeTokenAmount"` - Data []uint8 `json:"data"` - TokenAmounts []struct { - Token common.Address `json:"token"` - Amount *big.Int `json:"amount"` - } `json:"tokenAmounts"` - SourceTokenData [][]byte `json:"sourceTokenData"` - MessageId [32]byte `json:"messageId"` - }) - if !ok { - return nil, fmt.Errorf("invalid format have %T want %T", unpacked[0], receivedCp) - } - var tokensAndAmounts []evm_2_evm_offramp.ClientEVMTokenAmount - for _, tokenAndAmount := range receivedCp.TokenAmounts { - tokensAndAmounts = append(tokensAndAmounts, evm_2_evm_offramp.ClientEVMTokenAmount{ - Token: tokenAndAmount.Token, - Amount: tokenAndAmount.Amount, - }) - } - - return &evm_2_evm_offramp.InternalEVM2EVMMessage{ - SourceChainSelector: receivedCp.SourceChainSelector, - Sender: receivedCp.Sender, - Receiver: receivedCp.Receiver, - SequenceNumber: receivedCp.SequenceNumber, - GasLimit: receivedCp.GasLimit, - Strict: receivedCp.Strict, - Nonce: receivedCp.Nonce, - FeeToken: receivedCp.FeeToken, - FeeTokenAmount: receivedCp.FeeTokenAmount, - Data: receivedCp.Data, - TokenAmounts: tokensAndAmounts, - SourceTokenData: receivedCp.SourceTokenData, - MessageId: receivedCp.MessageId, - }, nil -} - -func OnRampMessageToOffRampMessage(msg evm_2_evm_onramp.InternalEVM2EVMMessage) evm_2_evm_offramp.InternalEVM2EVMMessage { - tokensAndAmounts := make([]evm_2_evm_offramp.ClientEVMTokenAmount, len(msg.TokenAmounts)) - for i, tokenAndAmount := range msg.TokenAmounts { - tokensAndAmounts[i] = evm_2_evm_offramp.ClientEVMTokenAmount{ - Token: tokenAndAmount.Token, - Amount: tokenAndAmount.Amount, - } - } - - return evm_2_evm_offramp.InternalEVM2EVMMessage{ - SourceChainSelector: msg.SourceChainSelector, - Sender: msg.Sender, - Receiver: msg.Receiver, - SequenceNumber: msg.SequenceNumber, - GasLimit: msg.GasLimit, - Strict: msg.Strict, - Nonce: msg.Nonce, - FeeToken: msg.FeeToken, - FeeTokenAmount: msg.FeeTokenAmount, - Data: msg.Data, - TokenAmounts: tokensAndAmounts, - SourceTokenData: msg.SourceTokenData, - MessageId: msg.MessageId, - } -} - // ProofFlagsToBits transforms a list of boolean proof flags to a *big.Int // encoded number. func ProofFlagsToBits(proofFlags []bool) *big.Int { diff --git a/core/services/ocr2/plugins/ccip/commit_plugin.go b/core/services/ocr2/plugins/ccip/commit_plugin.go index 8691c4a99b..bda5f3b0e0 100644 --- a/core/services/ocr2/plugins/ccip/commit_plugin.go +++ b/core/services/ocr2/plugins/ccip/commit_plugin.go @@ -3,7 +3,6 @@ package ccip import ( "context" "encoding/json" - "fmt" "strconv" "github.com/ethereum/go-ethereum/accounts/abi/bind" @@ -14,9 +13,9 @@ import ( libocr2 "github.com/smartcontractkit/libocr/offchainreporting2plus" relaylogger "github.com/smartcontractkit/chainlink-relay/pkg/logger" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipdata" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/contractutil" - "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/hashlib" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/logpollerutil" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/oraclelib" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/pricegetter" @@ -38,117 +37,123 @@ import ( const ( COMMIT_PRICE_UPDATES = "Commit price updates" - COMMIT_CCIP_SENDS = "Commit ccip sends" ) -func NewCommitServices(lggr logger.Logger, jb job.Job, chainSet evm.LegacyChainContainer, new bool, pr pipeline.Runner, argsNoPlugin libocr2.OCR2OracleArgs, logError func(string), qopts ...pg.QOpt) ([]job.ServiceCtx, error) { - spec := jb.OCR2OracleSpec +type BackfillArgs struct { + sourceLP, destLP logpoller.LogPoller + sourceStartBlock, destStartBlock int64 +} +func jobSpecToCommitPluginConfig(lggr logger.Logger, jb job.Job, pr pipeline.Runner, chainSet evm.LegacyChainContainer) (*CommitPluginConfig, *BackfillArgs, error) { + if jb.OCR2OracleSpec == nil { + return nil, nil, errors.New("spec is nil") + } + spec := jb.OCR2OracleSpec var pluginConfig ccipconfig.CommitPluginJobSpecConfig err := json.Unmarshal(spec.PluginConfig.Bytes(), &pluginConfig) if err != nil { - return nil, err + return nil, nil, err } chainIDInterface, ok := spec.RelayConfig["chainID"] if !ok { - return nil, errors.New("chainID must be provided in relay config") + return nil, nil, errors.New("chainID must be provided in relay config") } destChainID := int64(chainIDInterface.(float64)) destChain, err := chainSet.Get(strconv.FormatInt(destChainID, 10)) if err != nil { - return nil, errors.Wrap(err, "get chainset") + return nil, nil, errors.Wrap(err, "get chainset") } commitStore, err := contractutil.LoadCommitStore(common.HexToAddress(spec.ContractID), CommitPluginLabel, destChain.Client()) if err != nil { - return nil, errors.Wrap(err, "failed loading commitStore") + return nil, nil, errors.Wrap(err, "failed loading commitStore") } staticConfig, err := commitStore.GetStaticConfig(&bind.CallOpts{}) if err != nil { - return nil, errors.Wrap(err, "failed getting the static config from the commitStore") + return nil, nil, errors.Wrap(err, "failed getting the static config from the commitStore") } chainId, err := chainselectors.ChainIdFromSelector(staticConfig.SourceChainSelector) if err != nil { - return nil, err + return nil, nil, err } sourceChain, err := chainSet.Get(strconv.FormatUint(chainId, 10)) if err != nil { - return nil, errors.Wrap(err, "unable to open source chain") + return nil, nil, errors.Wrap(err, "unable to open source chain") } offRamp, err := contractutil.LoadOffRamp(common.HexToAddress(pluginConfig.OffRamp), CommitPluginLabel, destChain.Client()) if err != nil { - return nil, errors.Wrap(err, "failed loading offRamp") + return nil, nil, errors.Wrap(err, "failed loading offRamp") } - onRamp, err := contractutil.LoadOnRamp(staticConfig.OnRamp, CommitPluginLabel, sourceChain.Client()) + commitLggr := lggr.Named("CCIPCommit").With( + "sourceChain", ChainName(int64(chainId)), + "destChain", ChainName(destChainID)) + onRampReader, err := ccipdata.NewOnRampReader(commitLggr, staticConfig.SourceChainSelector, staticConfig.ChainSelector, staticConfig.OnRamp, sourceChain.LogPoller(), sourceChain.Client(), sourceChain.Config().EVM().FinalityTagEnabled()) if err != nil { - return nil, errors.Wrap(err, "failed loading onRamp") + return nil, nil, err } priceGetterObject, err := pricegetter.NewPipelineGetter(pluginConfig.TokenPricesUSDPipeline, pr, jb.ID, jb.ExternalJobID, jb.Name.ValueOrZero(), lggr) if err != nil { - return nil, err - } - dynamicOnRampConfig, err := contractutil.LoadOnRampDynamicConfig(onRamp, sourceChain.Client()) - if err != nil { - return nil, err + return nil, nil, err } - sourceRouter, err := router.NewRouter(dynamicOnRampConfig.Router, sourceChain.Client()) + sourceRouter, err := router.NewRouter(onRampReader.RouterAddress(), sourceChain.Client()) if err != nil { - return nil, err + return nil, nil, err } sourceNative, err := sourceRouter.GetWrappedNative(nil) if err != nil { - return nil, err + return nil, nil, err } - - leafHasher := hashlib.NewLeafHasher(staticConfig.SourceChainSelector, staticConfig.ChainSelector, onRamp.Address(), hashlib.NewKeccakCtx()) - // Note that lggr already has the jobName and contractID (commit store) - commitLggr := lggr.Named("CCIPCommit").With( - "sourceChain", ChainName(int64(chainId)), - "destChain", ChainName(destChainID)) - wrappedPluginFactory := NewCommitReportingPluginFactory( - CommitPluginConfig{ + lggr.Infow("NewCommitServices", + "pluginConfig", pluginConfig, + "staticConfig", staticConfig, + // TODO bring back + //"dynamicOnRampConfig", dynamicOnRampConfig, + "sourceNative", sourceNative, + "sourceRouter", sourceRouter.Address()) + return &CommitPluginConfig{ lggr: commitLggr, - sourceLP: sourceChain.LogPoller(), destLP: destChain.LogPoller(), - sourceReader: ccipdata.NewLogPollerReader(sourceChain.LogPoller(), commitLggr, sourceChain.Client()), + onRampReader: onRampReader, destReader: ccipdata.NewLogPollerReader(destChain.LogPoller(), commitLggr, destChain.Client()), offRamp: offRamp, - onRampAddress: onRamp.Address(), priceGetter: priceGetterObject, sourceNative: sourceNative, sourceFeeEstimator: sourceChain.GasEstimator(), sourceChainSelector: staticConfig.SourceChainSelector, destClient: destChain.Client(), - sourceClient: sourceChain.Client(), commitStore: commitStore, - leafHasher: leafHasher, - checkFinalityTags: sourceChain.Config().EVM().FinalityTagEnabled(), - }) + }, &BackfillArgs{ + sourceLP: sourceChain.LogPoller(), + destLP: destChain.LogPoller(), + sourceStartBlock: pluginConfig.SourceStartBlock, + destStartBlock: pluginConfig.DestStartBlock, + }, nil +} +func NewCommitServices(lggr logger.Logger, jb job.Job, chainSet evm.LegacyChainContainer, new bool, pr pipeline.Runner, argsNoPlugin libocr2.OCR2OracleArgs, logError func(string), qopts ...pg.QOpt) ([]job.ServiceCtx, error) { + pluginConfig, backfillArgs, err := jobSpecToCommitPluginConfig(lggr, jb, pr, chainSet) + if err != nil { + return nil, err + } + wrappedPluginFactory := NewCommitReportingPluginFactory(*pluginConfig) err = wrappedPluginFactory.UpdateLogPollerFilters(utils.ZeroAddress, qopts...) if err != nil { return nil, err } - argsNoPlugin.ReportingPluginFactory = promwrapper.NewPromFactory(wrappedPluginFactory, "CCIPCommit", string(spec.Relay), destChain.ID()) - argsNoPlugin.Logger = relaylogger.NewOCRWrapper(commitLggr, true, logError) + argsNoPlugin.ReportingPluginFactory = promwrapper.NewPromFactory(wrappedPluginFactory, "CCIPCommit", jb.OCR2OracleSpec.Relay, pluginConfig.destChainEVMID) + argsNoPlugin.Logger = relaylogger.NewOCRWrapper(pluginConfig.lggr, true, logError) oracle, err := libocr2.NewOracle(argsNoPlugin) if err != nil { return nil, err } - commitLggr.Infow("NewCommitServices", - "pluginConfig", pluginConfig, - "staticConfig", staticConfig, - "dynamicOnRampConfig", dynamicOnRampConfig, - "sourceNative", sourceNative, - "sourceRouter", sourceRouter.Address()) // If this is a brand-new job, then we make use of the start blocks. If not then we're rebooting and log poller will pick up where we left off. if new { return []job.ServiceCtx{oraclelib.NewBackfilledOracle( - commitLggr, - sourceChain.LogPoller(), - destChain.LogPoller(), - pluginConfig.SourceStartBlock, - pluginConfig.DestStartBlock, + pluginConfig.lggr, + backfillArgs.sourceLP, + backfillArgs.destLP, + backfillArgs.sourceStartBlock, + backfillArgs.destStartBlock, job.NewServiceAdapter(oracle)), }, nil } @@ -172,16 +177,6 @@ func CommitReportToEthTxMeta(report []byte) (*txmgr.TxMeta, error) { }, nil } -func getCommitPluginSourceLpFilters(onRamp common.Address) []logpoller.Filter { - return []logpoller.Filter{ - { - Name: logpoller.FilterName(COMMIT_CCIP_SENDS, onRamp.String()), - EventSigs: []common.Hash{abihelpers.EventSignatures.SendRequested}, - Addresses: []common.Address{onRamp}, - }, - } -} - func getCommitPluginDestLpFilters(priceRegistry common.Address, offRamp common.Address) []logpoller.Filter { return []logpoller.Filter{ { @@ -213,71 +208,25 @@ func getCommitPluginDestLpFilters(priceRegistry common.Address, offRamp common.A } // UnregisterCommitPluginLpFilters unregisters all the registered filters for both source and dest chains. -func UnregisterCommitPluginLpFilters(ctx context.Context, spec *job.OCR2OracleSpec, chainSet evm.LegacyChainContainer, qopts ...pg.QOpt) error { - if spec == nil { +func UnregisterCommitPluginLpFilters(ctx context.Context, lggr logger.Logger, jb job.Job, pr pipeline.Runner, chainSet evm.LegacyChainContainer, qopts ...pg.QOpt) error { + commitPluginConfig, _, err := jobSpecToCommitPluginConfig(lggr, jb, pr, chainSet) + if err != nil { return errors.New("spec is nil") } - if !common.IsHexAddress(spec.ContractID) { - return fmt.Errorf("invalid contract id address: %s", spec.ContractID) - } - - var pluginConfig ccipconfig.CommitPluginJobSpecConfig - err := json.Unmarshal(spec.PluginConfig.Bytes(), &pluginConfig) - if err != nil { + if err := commitPluginConfig.onRampReader.Close(); err != nil { return err } - destChainIDInterface, ok := spec.RelayConfig["chainID"] - if !ok { - return errors.New("chainID must be provided in relay config") - } - destChainIDf64, is := destChainIDInterface.(float64) - if !is { - return fmt.Errorf("chain id '%v' is not float64", destChainIDInterface) - } - destChainID := int64(destChainIDf64) - destChain, err := chainSet.Get(strconv.FormatInt(destChainID, 10)) - if err != nil { - return err - } - commitStore, err := contractutil.LoadCommitStore(common.HexToAddress(spec.ContractID), CommitPluginLabel, destChain.Client()) - if err != nil { - return err - } - staticConfig, err := commitStore.GetStaticConfig(&bind.CallOpts{}) - if err != nil { - return err - } - chainId, err := chainselectors.ChainIdFromSelector(staticConfig.SourceChainSelector) - if err != nil { - return err - } - sourceChain, err := chainSet.Get(strconv.FormatUint(chainId, 10)) - if err != nil { - return err - } - return unregisterCommitPluginFilters(ctx, sourceChain.LogPoller(), destChain.LogPoller(), commitStore, common.HexToAddress(pluginConfig.OffRamp), qopts...) + // TODO: once offramp/commit are abstracted, we can call Close on the offramp/commit readers to unregister filters. + return unregisterCommitPluginFilters(ctx, commitPluginConfig.destLP, commitPluginConfig.commitStore, + commitPluginConfig.offRamp.Address(), qopts...) } -func unregisterCommitPluginFilters(ctx context.Context, sourceLP, destLP logpoller.LogPoller, destCommitStore commit_store.CommitStoreInterface, offRamp common.Address, qopts ...pg.QOpt) error { - staticCfg, err := destCommitStore.GetStaticConfig(&bind.CallOpts{Context: ctx}) - if err != nil { - return err - } - +func unregisterCommitPluginFilters(ctx context.Context, destLP logpoller.LogPoller, destCommitStore commit_store.CommitStoreInterface, offRamp common.Address, qopts ...pg.QOpt) error { dynamicCfg, err := destCommitStore.GetDynamicConfig(&bind.CallOpts{Context: ctx}) if err != nil { return err } - - if err := logpollerutil.UnregisterLpFilters( - sourceLP, - getCommitPluginSourceLpFilters(staticCfg.OnRamp), - qopts..., - ); err != nil { - return err - } - return logpollerutil.UnregisterLpFilters( destLP, getCommitPluginDestLpFilters(dynamicCfg.PriceRegistry, offRamp), diff --git a/core/services/ocr2/plugins/ccip/commit_plugin_test.go b/core/services/ocr2/plugins/ccip/commit_plugin_test.go index a7a53dd32b..720aadf0d6 100644 --- a/core/services/ocr2/plugins/ccip/commit_plugin_test.go +++ b/core/services/ocr2/plugins/ccip/commit_plugin_test.go @@ -16,12 +16,15 @@ import ( evmmocks "github.com/smartcontractkit/chainlink/v2/core/chains/evm/mocks" "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/commit_store" mock_contracts "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/mocks" + "github.com/smartcontractkit/chainlink/v2/core/logger" "github.com/smartcontractkit/chainlink/v2/core/services/job" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/testhelpers" + pipelinemocks "github.com/smartcontractkit/chainlink/v2/core/services/pipeline/mocks" "github.com/smartcontractkit/chainlink/v2/core/utils" ) func TestGetCommitPluginFilterNamesFromSpec(t *testing.T) { + lggr := logger.TestLogger(t) testCases := []struct { description string spec *job.OCR2OracleSpec @@ -63,6 +66,7 @@ func TestGetCommitPluginFilterNamesFromSpec(t *testing.T) { for _, tc := range testCases { t.Run(tc.description, func(t *testing.T) { chainSet := &evmmocks.LegacyChainContainer{} + prMock := &pipelinemocks.Runner{} if tc.spec != nil { if chainID, ok := tc.spec.RelayConfig["chainID"]; ok { @@ -72,7 +76,7 @@ func TestGetCommitPluginFilterNamesFromSpec(t *testing.T) { } } - err := UnregisterCommitPluginLpFilters(context.Background(), tc.spec, chainSet) + err := UnregisterCommitPluginLpFilters(context.Background(), lggr, job.Job{OCR2OracleSpec: tc.spec}, prMock, chainSet) if tc.expectingErr { assert.Error(t, err) } else { @@ -97,14 +101,13 @@ func TestGetCommitPluginFilterNames(t *testing.T) { srcLP := mocklp.NewLogPoller(t) dstLP := mocklp.NewLogPoller(t) - srcLP.On("UnregisterFilter", "Commit ccip sends - 0xdafea492D9c6733aE3d56B7ED1aDb60692C98bc2", mock.Anything).Return(nil) dstLP.On("UnregisterFilter", "Commit price updates - 0xdafEa492d9C6733aE3D56b7eD1aDb60692c98bc3", mock.Anything).Return(nil) dstLP.On("UnregisterFilter", "Fee token added - 0xdafEa492d9C6733aE3D56b7eD1aDb60692c98bc3", mock.Anything).Return(nil) dstLP.On("UnregisterFilter", "Fee token removed - 0xdafEa492d9C6733aE3D56b7eD1aDb60692c98bc3", mock.Anything).Return(nil) dstLP.On("UnregisterFilter", "Token pool added - 0xDAFeA492D9c6733Ae3D56b7eD1AdB60692C98BC4", mock.Anything).Return(nil) dstLP.On("UnregisterFilter", "Token pool removed - 0xDAFeA492D9c6733Ae3D56b7eD1AdB60692C98BC4", mock.Anything).Return(nil) - err := unregisterCommitPluginFilters(context.Background(), srcLP, dstLP, mockCommitStore, offRampAddr) + err := unregisterCommitPluginFilters(context.Background(), dstLP, mockCommitStore, offRampAddr) assert.NoError(t, err) srcLP.AssertExpectations(t) @@ -115,31 +118,22 @@ func Test_updateCommitPluginLogPollerFilters(t *testing.T) { srcLP := &mocklp.LogPoller{} dstLP := &mocklp.LogPoller{} - onRampAddr := common.HexToAddress("0xdafea492d9c6733ae3d56b7ed1adb60692c98bc2") priceRegAddr := common.HexToAddress("0xdafea492d9c6733ae3d56b7ed1adb60692c98bc3") offRampAddr := common.HexToAddress("0xDAFeA492D9c6733Ae3D56b7eD1AdB60692C98BC4") offRamp := &mock_contracts.EVM2EVMOffRampInterface{} offRamp.On("Address").Return(offRampAddr) newDestFilters := getCommitPluginDestLpFilters(priceRegAddr, offRampAddr) - newSrcFilters := getCommitPluginSourceLpFilters(onRampAddr) rf := &CommitReportingPluginFactory{ config: CommitPluginConfig{ - sourceLP: srcLP, - destLP: dstLP, - onRampAddress: onRampAddr, - offRamp: offRamp, + destLP: dstLP, + offRamp: offRamp, }, destChainFilters: []logpoller.Filter{ {Name: "a"}, {Name: "b"}, }, - sourceChainFilters: []logpoller.Filter{ - {Name: newSrcFilters[0].Name}, // should not be touched, since it's already registered - {Name: "c"}, - {Name: "d"}, - }, filtersMu: &sync.Mutex{}, } @@ -147,18 +141,10 @@ func Test_updateCommitPluginLogPollerFilters(t *testing.T) { for _, f := range rf.destChainFilters { dstLP.On("UnregisterFilter", f.Name, mock.Anything).Return(nil) } - for _, f := range rf.sourceChainFilters[1:] { // skip the first one, which should not be unregistered - srcLP.On("UnregisterFilter", f.Name, mock.Anything).Return(nil) - } - // make sure new filters are registered for _, f := range newDestFilters { dstLP.On("RegisterFilter", f).Return(nil) } - for _, f := range newSrcFilters[1:] { // skip the first one, which should not be registered - srcLP.On("RegisterFilter", f).Return(nil) - } - err := rf.UpdateLogPollerFilters(priceRegAddr) assert.NoError(t, err) diff --git a/core/services/ocr2/plugins/ccip/commit_reporting_plugin.go b/core/services/ocr2/plugins/ccip/commit_reporting_plugin.go index fd68641bb5..745ac421e1 100644 --- a/core/services/ocr2/plugins/ccip/commit_reporting_plugin.go +++ b/core/services/ocr2/plugins/ccip/commit_reporting_plugin.go @@ -59,20 +59,21 @@ type update struct { } type CommitPluginConfig struct { - lggr logger.Logger - sourceLP, destLP logpoller.LogPoller - sourceReader ccipdata.Reader - destReader ccipdata.Reader - offRamp evm_2_evm_offramp.EVM2EVMOffRampInterface - onRampAddress common.Address - commitStore commit_store.CommitStoreInterface - priceGetter pricegetter.PriceGetter - sourceChainSelector uint64 - sourceNative common.Address - sourceFeeEstimator gas.EvmFeeEstimator - sourceClient, destClient evmclient.Client - leafHasher hashlib.LeafHasherInterface[[32]byte] - checkFinalityTags bool + lggr logger.Logger + // Source + onRampReader ccipdata.OnRampReader + sourceChainSelector uint64 + sourceNative common.Address + sourceFeeEstimator gas.EvmFeeEstimator + // Dest + destLP logpoller.LogPoller + destReader ccipdata.Reader + offRamp evm_2_evm_offramp.EVM2EVMOffRampInterface + commitStore commit_store.CommitStoreInterface + destClient evmclient.Client + destChainEVMID *big.Int + // Offchain + priceGetter pricegetter.PriceGetter } type CommitReportingPlugin struct { @@ -90,9 +91,9 @@ type CommitReportingPluginFactory struct { config CommitPluginConfig // We keep track of the registered filters - sourceChainFilters []logpoller.Filter - destChainFilters []logpoller.Filter - filtersMu *sync.Mutex + // TODO: Can push this down into the readers + destChainFilters []logpoller.Filter + filtersMu *sync.Mutex } // NewCommitReportingPluginFactory return a new CommitReportingPluginFactory. @@ -208,24 +209,14 @@ func (r *CommitReportingPlugin) Observation(ctx context.Context, epochAndRound t // UpdateLogPollerFilters updates the log poller filters for the source and destination chains. // pass zeroAddress if destPriceRegistry is unknown, filters with zero address are omitted. +// TODO: Should be able to Close and re-create readers to abstract filters. func (rf *CommitReportingPluginFactory) UpdateLogPollerFilters(destPriceRegistry common.Address, qopts ...pg.QOpt) error { rf.filtersMu.Lock() defer rf.filtersMu.Unlock() - // source chain filters - sourceFiltersBefore, sourceFiltersNow := rf.sourceChainFilters, getCommitPluginSourceLpFilters(rf.config.onRampAddress) - created, deleted := logpollerutil.FiltersDiff(sourceFiltersBefore, sourceFiltersNow) - if err := logpollerutil.UnregisterLpFilters(rf.config.sourceLP, deleted, qopts...); err != nil { - return err - } - if err := logpollerutil.RegisterLpFilters(rf.config.sourceLP, created, qopts...); err != nil { - return err - } - rf.sourceChainFilters = sourceFiltersNow - // destination chain filters destFiltersBefore, destFiltersNow := rf.destChainFilters, getCommitPluginDestLpFilters(destPriceRegistry, rf.config.offRamp.Address()) - created, deleted = logpollerutil.FiltersDiff(destFiltersBefore, destFiltersNow) + created, deleted := logpollerutil.FiltersDiff(destFiltersBefore, destFiltersNow) if err := logpollerutil.UnregisterLpFilters(rf.config.destLP, deleted, qopts...); err != nil { return err } @@ -243,7 +234,7 @@ func (r *CommitReportingPlugin) calculateMinMaxSequenceNumbers(ctx context.Conte return 0, 0, err } - msgRequests, err := r.config.sourceReader.GetSendRequestsGteSeqNum(ctx, r.config.onRampAddress, nextInflightMin, r.config.checkFinalityTags, int(r.offchainConfig.SourceFinalityDepth)) + msgRequests, err := r.config.onRampReader.GetSendRequestsGteSeqNum(ctx, nextInflightMin, int(r.offchainConfig.SourceFinalityDepth)) if err != nil { return 0, 0, err } @@ -253,7 +244,7 @@ func (r *CommitReportingPlugin) calculateMinMaxSequenceNumbers(ctx context.Conte } seqNrs := make([]uint64, 0, len(msgRequests)) for _, msgReq := range msgRequests { - seqNrs = append(seqNrs, msgReq.Data.Message.SequenceNumber) + seqNrs = append(seqNrs, msgReq.Data.SequenceNumber) } minSeqNr := seqNrs[0] @@ -662,9 +653,8 @@ func (r *CommitReportingPlugin) buildReport(ctx context.Context, lggr logger.Log // Logs are guaranteed to be in order of seq num, since these are finalized logs only // and the contract's seq num is auto-incrementing. - sendRequests, err := r.config.sourceReader.GetSendRequestsBetweenSeqNums( + sendRequests, err := r.config.onRampReader.GetSendRequestsBetweenSeqNums( ctx, - r.config.onRampAddress, interval.Min, interval.Max, int(r.offchainConfig.SourceFinalityDepth), @@ -672,19 +662,22 @@ func (r *CommitReportingPlugin) buildReport(ctx context.Context, lggr logger.Log if err != nil { return commit_store.CommitStoreCommitReport{}, err } - - leaves, err := hashlib.LeavesFromIntervals(lggr, interval, r.config.leafHasher, sendRequests) - if err != nil { - return commit_store.CommitStoreCommitReport{}, err - } - - if len(leaves) == 0 { - lggr.Warn("No leaves found in interval", + if len(sendRequests) == 0 { + lggr.Warn("No messages found in interval", "minSeqNr", interval.Min, "maxSeqNr", interval.Max) return commit_store.CommitStoreCommitReport{}, fmt.Errorf("tried building a tree without leaves") } + leaves := make([][32]byte, 0, len(sendRequests)) + var seqNrs []uint64 + for _, req := range sendRequests { + leaves = append(leaves, req.Data.Hash) + seqNrs = append(seqNrs, req.Data.SequenceNumber) + } + if !ccipcalc.ContiguousReqs(lggr, interval.Min, interval.Max, seqNrs) { + return commit_store.CommitStoreCommitReport{}, errors.Errorf("do not have full range [%v, %v] have %v", interval.Min, interval.Max, seqNrs) + } tree, err := merklemulti.NewTree(hashlib.NewKeccakCtx(), leaves) if err != nil { return commit_store.CommitStoreCommitReport{}, err diff --git a/core/services/ocr2/plugins/ccip/commit_reporting_plugin_test.go b/core/services/ocr2/plugins/ccip/commit_reporting_plugin_test.go index 8d7e0ab51b..fe384f93e3 100644 --- a/core/services/ocr2/plugins/ccip/commit_reporting_plugin_test.go +++ b/core/services/ocr2/plugins/ccip/commit_reporting_plugin_test.go @@ -10,8 +10,6 @@ import ( "testing" "time" - gethtypes "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/leanovate/gopter" @@ -27,7 +25,6 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/chains/evm/gas" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/gas/mocks" "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/commit_store" - "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/evm_2_evm_onramp" "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/price_registry" "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" "github.com/smartcontractkit/chainlink/v2/core/logger" @@ -54,7 +51,7 @@ func TestCommitReportingPlugin_Observation(t *testing.T) { commitStoreIsPaused bool commitStoreSeqNum uint64 tokenPrices map[common.Address]*big.Int - sendReqs []ccipdata.Event[evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested] + sendReqs []ccipdata.Event[ccipdata.EVM2EVMMessage] tokenDecimals map[common.Address]uint8 fee *big.Int @@ -68,9 +65,9 @@ func TestCommitReportingPlugin_Observation(t *testing.T) { someTokenAddr: big.NewInt(2), sourceNativeTokenAddr: big.NewInt(2), }, - sendReqs: []ccipdata.Event[evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested]{ - {Data: evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested{Message: evm_2_evm_onramp.InternalEVM2EVMMessage{SequenceNumber: 54}}}, - {Data: evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested{Message: evm_2_evm_onramp.InternalEVM2EVMMessage{SequenceNumber: 55}}}, + sendReqs: []ccipdata.Event[ccipdata.EVM2EVMMessage]{ + {Data: ccipdata.EVM2EVMMessage{SequenceNumber: 54}}, + {Data: ccipdata.EVM2EVMMessage{SequenceNumber: 55}}, }, fee: big.NewInt(100), tokenDecimals: map[common.Address]uint8{ @@ -97,15 +94,14 @@ func TestCommitReportingPlugin_Observation(t *testing.T) { ctx := testutils.Context(t) for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - onRampAddress := utils.RandomAddress() sourceFinalityDepth := 10 commitStore, _ := testhelpers.NewFakeCommitStore(t, tc.commitStoreSeqNum) commitStore.SetPaused(tc.commitStoreIsPaused) - sourceReader := ccipdata.NewMockReader(t) + onRampReader := ccipdata.NewMockOnRampReader(t) if len(tc.sendReqs) > 0 { - sourceReader.On("GetSendRequestsGteSeqNum", ctx, onRampAddress, tc.commitStoreSeqNum, false, sourceFinalityDepth). + onRampReader.On("GetSendRequestsGteSeqNum", ctx, tc.commitStoreSeqNum, sourceFinalityDepth). Return(tc.sendReqs, nil) } @@ -133,9 +129,8 @@ func TestCommitReportingPlugin_Observation(t *testing.T) { p.lggr = logger.TestLogger(t) p.inflightReports = newInflightCommitReportsContainer(time.Hour) p.config.commitStore = commitStore - p.config.onRampAddress = onRampAddress p.offchainConfig.SourceFinalityDepth = uint32(sourceFinalityDepth) - p.config.sourceReader = sourceReader + p.config.onRampReader = onRampReader p.tokenDecimalsCache = tokenDecimalsCache p.config.priceGetter = priceGet p.config.sourceFeeEstimator = sourceFeeEst @@ -164,7 +159,7 @@ func TestCommitReportingPlugin_Report(t *testing.T) { f int gasPriceUpdates []ccipdata.Event[price_registry.PriceRegistryUsdPerUnitGasUpdated] tokenPriceUpdates []ccipdata.Event[price_registry.PriceRegistryUsdPerTokenUpdated] - sendRequests []ccipdata.Event[evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested] + sendRequests []ccipdata.Event[ccipdata.EVM2EVMMessage] expCommitReport *commit_store.CommitStoreCommitReport expSeqNumRange commit_store.CommitStoreInterval @@ -177,18 +172,16 @@ func TestCommitReportingPlugin_Report(t *testing.T) { {Interval: commit_store.CommitStoreInterval{Min: 1, Max: 1}}, }, f: 1, - sendRequests: []ccipdata.Event[evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested]{ + sendRequests: []ccipdata.Event[ccipdata.EVM2EVMMessage]{ { - Data: evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested{ - Message: evm_2_evm_onramp.InternalEVM2EVMMessage{ - SequenceNumber: 1, - }, + Data: ccipdata.EVM2EVMMessage{ + SequenceNumber: 1, }, }, }, expSeqNumRange: commit_store.CommitStoreInterval{Min: 1, Max: 1}, expCommitReport: &commit_store.CommitStoreCommitReport{ - MerkleRoot: [32]byte{123}, + MerkleRoot: [32]byte{}, Interval: commit_store.CommitStoreInterval{Min: 1, Max: 1}, PriceUpdates: commit_store.InternalPriceUpdates{ TokenPriceUpdates: nil, @@ -204,7 +197,7 @@ func TestCommitReportingPlugin_Report(t *testing.T) { {Interval: commit_store.CommitStoreInterval{Min: 1, Max: 1}}, }, f: 1, - sendRequests: []ccipdata.Event[evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested]{{}}, + sendRequests: []ccipdata.Event[ccipdata.EVM2EVMMessage]{{}}, expSeqNumRange: commit_store.CommitStoreInterval{Min: 1, Max: 1}, expErr: true, }, @@ -224,14 +217,13 @@ func TestCommitReportingPlugin_Report(t *testing.T) { {Interval: commit_store.CommitStoreInterval{Min: 2, Max: 2}}, }, f: 1, - sendRequests: []ccipdata.Event[evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested]{{}}, + sendRequests: []ccipdata.Event[ccipdata.EVM2EVMMessage]{{}}, expSeqNumRange: commit_store.CommitStoreInterval{Min: 2, Max: 2}, expErr: true, }, } ctx := testutils.Context(t) - onRampAddress := utils.RandomAddress() sourceChainSelector := rand.Int() for _, tc := range testCases { @@ -242,9 +234,9 @@ func TestCommitReportingPlugin_Report(t *testing.T) { destReader.On("GetGasPriceUpdatesCreatedAfter", ctx, destPriceRegistryAddress, uint64(sourceChainSelector), mock.Anything, 0).Return(tc.gasPriceUpdates, nil) destReader.On("GetTokenPriceUpdatesCreatedAfter", ctx, destPriceRegistryAddress, mock.Anything, 0).Return(tc.tokenPriceUpdates, nil) - sourceReader := ccipdata.NewMockReader(t) + onRampReader := ccipdata.NewMockOnRampReader(t) if len(tc.sendRequests) > 0 { - sourceReader.On("GetSendRequestsBetweenSeqNums", ctx, onRampAddress, tc.expSeqNumRange.Min, tc.expSeqNumRange.Max, 0).Return(tc.sendRequests, nil) + onRampReader.On("GetSendRequestsBetweenSeqNums", ctx, tc.expSeqNumRange.Min, tc.expSeqNumRange.Max, 0).Return(tc.sendRequests, nil) } p := &CommitReportingPlugin{} @@ -252,10 +244,8 @@ func TestCommitReportingPlugin_Report(t *testing.T) { p.inflightReports = newInflightCommitReportsContainer(time.Minute) p.destPriceRegistry = destPriceRegistry p.config.destReader = destReader - p.config.sourceReader = sourceReader - p.config.onRampAddress = onRampAddress + p.config.onRampReader = onRampReader p.config.sourceChainSelector = uint64(sourceChainSelector) - p.config.leafHasher = &leafHasher123{} aos := make([]types.AttributedObservation, 0, len(tc.observations)) for _, o := range tc.observations { @@ -1034,17 +1024,17 @@ func TestCommitReportingPlugin_calculateMinMaxSequenceNumbers(t *testing.T) { } } - sourceReader := ccipdata.NewMockReader(t) - var sendReqs []ccipdata.Event[evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested] + onRampReader := ccipdata.NewMockOnRampReader(t) + var sendReqs []ccipdata.Event[ccipdata.EVM2EVMMessage] for _, seqNum := range tc.msgSeqNums { - sendReqs = append(sendReqs, ccipdata.Event[evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested]{ - Data: evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested{ - Message: evm_2_evm_onramp.InternalEVM2EVMMessage{SequenceNumber: seqNum}, + sendReqs = append(sendReqs, ccipdata.Event[ccipdata.EVM2EVMMessage]{ + Data: ccipdata.EVM2EVMMessage{ + SequenceNumber: seqNum, }, }) } - sourceReader.On("GetSendRequestsGteSeqNum", ctx, mock.Anything, tc.expQueryMin, false, 0).Return(sendReqs, nil) - p.config.sourceReader = sourceReader + onRampReader.On("GetSendRequestsGteSeqNum", ctx, tc.expQueryMin, 0).Return(sendReqs, nil) + p.config.onRampReader = onRampReader minSeqNum, maxSeqNum, err := p.calculateMinMaxSequenceNumbers(ctx, lggr) if tc.expErr { @@ -1412,10 +1402,3 @@ func TestCommitReportToEthTxMeta(t *testing.T) { }) } } - -// leafHasher123 always returns '123' followed by zeroes in HashLeaf method. -type leafHasher123 struct{} - -func (h leafHasher123) HashLeaf(_ gethtypes.Log) ([32]byte, error) { - return [32]byte{123}, nil -} diff --git a/core/services/ocr2/plugins/ccip/config/type_and_version.go b/core/services/ocr2/plugins/ccip/config/type_and_version.go index 085b22fb23..a06c00dd8d 100644 --- a/core/services/ocr2/plugins/ccip/config/type_and_version.go +++ b/core/services/ocr2/plugins/ccip/config/type_and_version.go @@ -25,7 +25,7 @@ var ( ) func VerifyTypeAndVersion(addr common.Address, client bind.ContractBackend, expectedType ContractType) error { - contractType, _, err := typeAndVersion(addr, client) + contractType, _, err := TypeAndVersion(addr, client) if err != nil { return errors.Errorf("failed getting type and version %v", err) } @@ -35,7 +35,7 @@ func VerifyTypeAndVersion(addr common.Address, client bind.ContractBackend, expe return nil } -func typeAndVersion(addr common.Address, client bind.ContractBackend) (ContractType, semver.Version, error) { +func TypeAndVersion(addr common.Address, client bind.ContractBackend) (ContractType, semver.Version, error) { tv, err := type_and_version.NewTypeAndVersionInterface(addr, client) if err != nil { return "", semver.Version{}, err diff --git a/core/services/ocr2/plugins/ccip/execution_batch_building.go b/core/services/ocr2/plugins/ccip/execution_batch_building.go index b2be5a6f13..7634c5532b 100644 --- a/core/services/ocr2/plugins/ccip/execution_batch_building.go +++ b/core/services/ocr2/plugins/ccip/execution_batch_building.go @@ -4,13 +4,10 @@ import ( "context" "github.com/ethereum/go-ethereum/accounts/abi/bind" - "github.com/ethereum/go-ethereum/common" "github.com/pkg/errors" "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/commit_store" "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/evm_2_evm_offramp" - "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/evm_2_evm_onramp" - "github.com/smartcontractkit/chainlink/v2/core/logger" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/abihelpers" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipdata" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/hashlib" @@ -19,15 +16,11 @@ import ( func getProofData( ctx context.Context, - lggr logger.Logger, - hashLeaf hashlib.LeafHasherInterface[[32]byte], - onRampAddress common.Address, - sourceReader ccipdata.Reader, + sourceReader ccipdata.OnRampReader, interval commit_store.CommitStoreInterval, -) (sendReqsInRoot []ccipdata.Event[evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested], leaves [][32]byte, tree *merklemulti.Tree[[32]byte], err error) { +) (sendReqsInRoot []ccipdata.Event[ccipdata.EVM2EVMMessage], leaves [][32]byte, tree *merklemulti.Tree[[32]byte], err error) { sendReqs, err := sourceReader.GetSendRequestsBetweenSeqNums( ctx, - onRampAddress, interval.Min, interval.Max, 0, // no need for confirmations, commitReport was already confirmed and we need all msgs in it @@ -35,9 +28,9 @@ func getProofData( if err != nil { return nil, nil, nil, err } - leaves, err = hashlib.LeavesFromIntervals(lggr, interval, hashLeaf, sendReqs) - if err != nil { - return nil, nil, nil, err + leaves = make([][32]byte, 0, len(sendReqs)) + for _, req := range sendReqs { + leaves = append(leaves, req.Data.Hash) } tree, err = merklemulti.NewTree(hashlib.NewKeccakCtx(), leaves) if err != nil { diff --git a/core/services/ocr2/plugins/ccip/execution_plugin.go b/core/services/ocr2/plugins/ccip/execution_plugin.go index 95426bdd40..03f454209c 100644 --- a/core/services/ocr2/plugins/ccip/execution_plugin.go +++ b/core/services/ocr2/plugins/ccip/execution_plugin.go @@ -3,7 +3,6 @@ package ccip import ( "context" "encoding/json" - "fmt" "net/url" "strconv" @@ -19,7 +18,6 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipdata" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/contractutil" - "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/hashlib" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/logpollerutil" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/oraclelib" "github.com/smartcontractkit/chainlink/v2/core/utils" @@ -43,7 +41,6 @@ import ( ) const ( - EXEC_CCIP_SENDS = "Exec ccip sends" EXEC_REPORT_ACCEPTS = "Exec report accepts" EXEC_EXECUTION_STATE_CHANGES = "Exec execution state changes" EXEC_TOKEN_POOL_ADDED = "Token pool added" @@ -52,81 +49,90 @@ const ( FEE_TOKEN_REMOVED = "Fee token removed" ) -func NewExecutionServices(lggr logger.Logger, jb job.Job, chainSet evm.LegacyChainContainer, new bool, argsNoPlugin libocr2.OCR2OracleArgs, logError func(string), qopts ...pg.QOpt) ([]job.ServiceCtx, error) { +func jobSpecToExecPluginConfig(lggr logger.Logger, jb job.Job, chainSet evm.LegacyChainContainer) (*ExecutionPluginConfig, *BackfillArgs, error) { + if jb.OCR2OracleSpec == nil { + return nil, nil, errors.New("spec is nil") + } spec := jb.OCR2OracleSpec var pluginConfig ccipconfig.ExecutionPluginJobSpecConfig err := json.Unmarshal(spec.PluginConfig.Bytes(), &pluginConfig) if err != nil { - return nil, err + return nil, nil, err } - chainIDInterface, ok := spec.RelayConfig["chainID"] if !ok { - return nil, errors.New("chainID must be provided in relay config") + return nil, nil, errors.New("chainID must be provided in relay config") } destChainID := int64(chainIDInterface.(float64)) destChain, err := chainSet.Get(strconv.FormatInt(destChainID, 10)) if err != nil { - return nil, errors.Wrap(err, "get chainset") + return nil, nil, errors.Wrap(err, "get chainset") } offRamp, err := contractutil.LoadOffRamp(common.HexToAddress(spec.ContractID), ExecPluginLabel, destChain.Client()) if err != nil { - return nil, errors.Wrap(err, "failed loading offRamp") + return nil, nil, errors.Wrap(err, "failed loading offRamp") } offRampConfig, err := offRamp.GetStaticConfig(&bind.CallOpts{}) if err != nil { - return nil, err + return nil, nil, err } chainId, err := chainselectors.ChainIdFromSelector(offRampConfig.SourceChainSelector) if err != nil { - return nil, err + return nil, nil, err } sourceChain, err := chainSet.Get(strconv.FormatUint(chainId, 10)) if err != nil { - return nil, errors.Wrap(err, "unable to open source chain") + return nil, nil, errors.Wrap(err, "unable to open source chain") } commitStore, err := contractutil.LoadCommitStore(offRampConfig.CommitStore, ExecPluginLabel, destChain.Client()) if err != nil { - return nil, errors.Wrap(err, "failed loading commitStore") + return nil, nil, errors.Wrap(err, "failed loading commitStore") } onRamp, err := contractutil.LoadOnRamp(offRampConfig.OnRamp, ExecPluginLabel, sourceChain.Client()) if err != nil { - return nil, errors.Wrap(err, "failed loading onRamp") + return nil, nil, errors.Wrap(err, "failed loading onRamp") } dynamicOnRampConfig, err := contractutil.LoadOnRampDynamicConfig(onRamp, sourceChain.Client()) if err != nil { - return nil, errors.Wrap(err, "failed loading onRamp config") + return nil, nil, errors.Wrap(err, "failed loading onRamp config") } sourceRouter, err := router.NewRouter(dynamicOnRampConfig.Router, sourceChain.Client()) if err != nil { - return nil, errors.Wrap(err, "failed loading source router") + return nil, nil, errors.Wrap(err, "failed loading source router") } sourceWrappedNative, err := sourceRouter.GetWrappedNative(&bind.CallOpts{}) if err != nil { - return nil, errors.Wrap(err, "could not get source native token") + return nil, nil, errors.Wrap(err, "could not get source native token") } sourcePriceRegistry, err := observability.NewObservedPriceRegistry(dynamicOnRampConfig.PriceRegistry, ExecPluginLabel, sourceChain.Client()) if err != nil { - return nil, errors.Wrap(err, "could not create source price registry") + return nil, nil, errors.Wrap(err, "could not create source price registry") } execLggr := lggr.Named("CCIPExecution").With( "sourceChain", ChainName(int64(chainId)), "destChain", ChainName(destChainID)) - - sourceChainEventClient := ccipdata.NewLogPollerReader(sourceChain.LogPoller(), execLggr, sourceChain.Client()) - - tokenDataProviders, err := getTokenDataProviders(lggr, pluginConfig, offRampConfig.OnRamp, sourceChainEventClient) + onRampReader, err := ccipdata.NewOnRampReader(execLggr, offRampConfig.SourceChainSelector, + offRampConfig.ChainSelector, offRampConfig.OnRamp, sourceChain.LogPoller(), sourceChain.Client(), sourceChain.Config().EVM().FinalityTagEnabled()) if err != nil { - return nil, errors.Wrap(err, "could not get token data providers") + return nil, nil, err } - - wrappedPluginFactory := NewExecutionReportingPluginFactory( - ExecutionPluginConfig{ + tokenDataProviders, err := getTokenDataProviders(lggr, pluginConfig, sourceChain.LogPoller()) + if err != nil { + return nil, nil, errors.Wrap(err, "could not get token data providers") + } + execLggr.Infow("Initialized exec plugin", + "pluginConfig", pluginConfig, + "onRampAddress", onRamp.Address(), + "sourcePriceRegistry", sourcePriceRegistry.Address(), + "dynamicOnRampConfig", dynamicOnRampConfig, + "sourceNative", sourceWrappedNative, + "sourceRouter", sourceRouter.Address()) + return &ExecutionPluginConfig{ lggr: execLggr, sourceLP: sourceChain.LogPoller(), destLP: destChain.LogPoller(), - sourceReader: sourceChainEventClient, + onRampReader: onRampReader, destReader: ccipdata.NewLogPollerReader(destChain.LogPoller(), execLggr, destChain.Client()), onRamp: onRamp, offRamp: offRamp, @@ -136,44 +142,49 @@ func NewExecutionServices(lggr logger.Logger, jb job.Job, chainSet evm.LegacyCha destClient: destChain.Client(), sourceClient: sourceChain.Client(), destGasEstimator: destChain.GasEstimator(), - leafHasher: hashlib.NewLeafHasher(offRampConfig.SourceChainSelector, offRampConfig.ChainSelector, onRamp.Address(), hashlib.NewKeccakCtx()), + destChainEVMID: destChain.ID(), tokenDataProviders: tokenDataProviders, - }) + }, &BackfillArgs{ + sourceLP: sourceChain.LogPoller(), + destLP: destChain.LogPoller(), + sourceStartBlock: pluginConfig.SourceStartBlock, + destStartBlock: pluginConfig.DestStartBlock, + }, nil +} +func NewExecutionServices(lggr logger.Logger, jb job.Job, chainSet evm.LegacyChainContainer, new bool, argsNoPlugin libocr2.OCR2OracleArgs, logError func(string), qopts ...pg.QOpt) ([]job.ServiceCtx, error) { + execPluginConfig, backfillArgs, err := jobSpecToExecPluginConfig(lggr, jb, chainSet) + if err != nil { + return nil, err + } + wrappedPluginFactory := NewExecutionReportingPluginFactory(*execPluginConfig) err = wrappedPluginFactory.UpdateLogPollerFilters(utils.ZeroAddress, qopts...) if err != nil { return nil, err } - argsNoPlugin.ReportingPluginFactory = promwrapper.NewPromFactory(wrappedPluginFactory, "CCIPExecution", spec.Relay, destChain.ID()) - argsNoPlugin.Logger = relaylogger.NewOCRWrapper(execLggr, true, logError) + argsNoPlugin.ReportingPluginFactory = promwrapper.NewPromFactory(wrappedPluginFactory, "CCIPExecution", jb.OCR2OracleSpec.Relay, execPluginConfig.destChainEVMID) + argsNoPlugin.Logger = relaylogger.NewOCRWrapper(execPluginConfig.lggr, true, logError) oracle, err := libocr2.NewOracle(argsNoPlugin) if err != nil { return nil, err } - execLggr.Infow("Initialized exec plugin", - "pluginConfig", pluginConfig, - "onRampAddress", onRamp.Address(), - "sourcePriceRegistry", sourcePriceRegistry.Address(), - "dynamicOnRampConfig", dynamicOnRampConfig, - "sourceNative", sourceWrappedNative, - "sourceRouter", sourceRouter.Address()) // If this is a brand-new job, then we make use of the start blocks. If not then we're rebooting and log poller will pick up where we left off. if new { return []job.ServiceCtx{ oraclelib.NewBackfilledOracle( - execLggr, - sourceChain.LogPoller(), - destChain.LogPoller(), - pluginConfig.SourceStartBlock, - pluginConfig.DestStartBlock, + execPluginConfig.lggr, + backfillArgs.sourceLP, + backfillArgs.destLP, + backfillArgs.sourceStartBlock, + backfillArgs.destStartBlock, job.NewServiceAdapter(oracle)), }, nil } return []job.ServiceCtx{job.NewServiceAdapter(oracle)}, nil } -func getTokenDataProviders(lggr logger.Logger, pluginConfig ccipconfig.ExecutionPluginJobSpecConfig, onRampAddress common.Address, sourceChainEventClient *ccipdata.LogPollerReader) (map[common.Address]tokendata.Reader, error) { +func getTokenDataProviders(lggr logger.Logger, pluginConfig ccipconfig.ExecutionPluginJobSpecConfig, sourceLP logpoller.LogPoller) (map[common.Address]tokendata.Reader, error) { tokenDataProviders := make(map[common.Address]tokendata.Reader) if pluginConfig.USDCConfig.AttestationAPI != "" { @@ -183,18 +194,19 @@ func getTokenDataProviders(lggr logger.Logger, pluginConfig ccipconfig.Execution return nil, err } - attestationURI, err2 := url.ParseRequestURI(pluginConfig.USDCConfig.AttestationAPI) - if err2 != nil { - return nil, errors.Wrap(err2, "failed to parse USDC attestation API") + attestationURI, err := url.ParseRequestURI(pluginConfig.USDCConfig.AttestationAPI) + if err != nil { + return nil, errors.Wrap(err, "failed to parse USDC attestation API") } + usdcReader, err := ccipdata.NewUSDCReader(lggr, pluginConfig.USDCConfig.SourceMessageTransmitterAddress, sourceLP) + if err != nil { + return nil, err + } tokenDataProviders[pluginConfig.USDCConfig.SourceTokenAddress] = tokendata.NewCachedReader( usdc.NewUSDCTokenDataReader( lggr, - sourceChainEventClient, - pluginConfig.USDCConfig.SourceTokenAddress, - pluginConfig.USDCConfig.SourceMessageTransmitterAddress, - onRampAddress, + usdcReader, attestationURI, ), ) @@ -203,18 +215,8 @@ func getTokenDataProviders(lggr logger.Logger, pluginConfig ccipconfig.Execution return tokenDataProviders, nil } -func getExecutionPluginSourceLpChainFilters(onRamp, priceRegistry common.Address, tokenDataProviders map[common.Address]tokendata.Reader) []logpoller.Filter { - var filters []logpoller.Filter - for _, provider := range tokenDataProviders { - filters = append(filters, provider.GetSourceLogPollerFilters()...) - } - - return append(filters, []logpoller.Filter{ - { - Name: logpoller.FilterName(EXEC_CCIP_SENDS, onRamp.String()), - EventSigs: []common.Hash{abihelpers.EventSignatures.SendRequested}, - Addresses: []common.Address{onRamp}, - }, +func getExecutionPluginSourceLpChainFilters(priceRegistry common.Address) []logpoller.Filter { + return []logpoller.Filter{ { Name: logpoller.FilterName(FEE_TOKEN_ADDED, priceRegistry.String()), EventSigs: []common.Hash{abihelpers.EventSignatures.FeeTokenAdded}, @@ -225,7 +227,7 @@ func getExecutionPluginSourceLpChainFilters(onRamp, priceRegistry common.Address EventSigs: []common.Hash{abihelpers.EventSignatures.FeeTokenRemoved}, Addresses: []common.Address{priceRegistry}, }, - }...) + } } func getExecutionPluginDestLpChainFilters(commitStore, offRamp, priceRegistry common.Address) []logpoller.Filter { @@ -264,66 +266,32 @@ func getExecutionPluginDestLpChainFilters(commitStore, offRamp, priceRegistry co } // UnregisterExecPluginLpFilters unregisters all the registered filters for both source and dest chains. -func UnregisterExecPluginLpFilters(ctx context.Context, lggr logger.Logger, spec *job.OCR2OracleSpec, chainSet evm.LegacyChainContainer, qopts ...pg.QOpt) error { - if spec == nil { - return errors.New("spec is nil") - } - - var pluginConfig ccipconfig.ExecutionPluginJobSpecConfig - err := json.Unmarshal(spec.PluginConfig.Bytes(), &pluginConfig) - if err != nil { - return err - } - - destChainIDInterface, ok := spec.RelayConfig["chainID"] - if !ok { - return errors.New("chainID must be provided in relay config") - } - destChainIDf64, is := destChainIDInterface.(float64) - if !is { - return fmt.Errorf("chain id '%v' is not float64", destChainIDInterface) - } - destChain, err := chainSet.Get(strconv.FormatInt(int64(destChainIDf64), 10)) - if err != nil { - return err - } - - offRampAddress := common.HexToAddress(spec.ContractID) - offRamp, err := contractutil.LoadOffRamp(offRampAddress, ExecPluginLabel, destChain.Client()) +func UnregisterExecPluginLpFilters(ctx context.Context, lggr logger.Logger, jb job.Job, chainSet evm.LegacyChainContainer, qopts ...pg.QOpt) error { + execPluginConfig, _, err := jobSpecToExecPluginConfig(lggr, jb, chainSet) if err != nil { return err } - - offRampConfig, err := offRamp.GetStaticConfig(&bind.CallOpts{}) - if err != nil { + if err := execPluginConfig.onRampReader.Close(); err != nil { return err } - chainId, err := chainselectors.ChainIdFromSelector(offRampConfig.SourceChainSelector) - if err != nil { - return err - } - sourceChain, err := chainSet.Get(strconv.FormatUint(chainId, 10)) - if err != nil { - return errors.Wrap(err, "unable to open source chain") - } - sourceOnRamp, err := contractutil.LoadOnRamp(offRampConfig.OnRamp, ExecPluginLabel, sourceChain.Client()) - if err != nil { - return errors.Wrap(err, "failed loading onRamp") + for _, tokenReader := range execPluginConfig.tokenDataProviders { + if err := tokenReader.Close(); err != nil { + return err + } } - - return unregisterExecutionPluginLpFilters(ctx, lggr, sourceChain.LogPoller(), destChain.LogPoller(), offRamp, offRampConfig, sourceOnRamp, sourceChain.Client(), pluginConfig, qopts...) + // TODO: once offramp/commit/pricereg are abstracted, we can call Close on the offramp/commit readers to unregister filters. + return unregisterExecutionPluginLpFilters(ctx, execPluginConfig.sourceLP, execPluginConfig.destLP, execPluginConfig.offRamp, + execPluginConfig.commitStore.Address(), execPluginConfig.onRamp, execPluginConfig.sourceClient, qopts...) } func unregisterExecutionPluginLpFilters( ctx context.Context, - lggr logger.Logger, sourceLP logpoller.LogPoller, destLP logpoller.LogPoller, destOffRamp evm_2_evm_offramp.EVM2EVMOffRampInterface, - destOffRampConfig evm_2_evm_offramp.EVM2EVMOffRampStaticConfig, + commitStore common.Address, sourceOnRamp evm_2_evm_onramp.EVM2EVMOnRampInterface, sourceChainClient client.Client, - pluginConfig ccipconfig.ExecutionPluginJobSpecConfig, qopts ...pg.QOpt) error { destOffRampDynCfg, err := destOffRamp.GetDynamicConfig(&bind.CallOpts{Context: ctx}) if err != nil { @@ -335,15 +303,9 @@ func unregisterExecutionPluginLpFilters( return err } - // SourceChainEventClient can be nil because it is not used in unregisterExecutionPluginLpFilters - tokenDataProviders, err := getTokenDataProviders(lggr, pluginConfig, destOffRampConfig.OnRamp, nil) - if err != nil { - return err - } - if err = logpollerutil.UnregisterLpFilters( sourceLP, - getExecutionPluginSourceLpChainFilters(destOffRampConfig.OnRamp, onRampDynCfg.PriceRegistry, tokenDataProviders), + getExecutionPluginSourceLpChainFilters(onRampDynCfg.PriceRegistry), qopts..., ); err != nil { return err @@ -351,7 +313,7 @@ func unregisterExecutionPluginLpFilters( return logpollerutil.UnregisterLpFilters( destLP, - getExecutionPluginDestLpChainFilters(destOffRampConfig.CommitStore, destOffRamp.Address(), destOffRampDynCfg.PriceRegistry), + getExecutionPluginDestLpChainFilters(commitStore, destOffRamp.Address(), destOffRampDynCfg.PriceRegistry), qopts..., ) } diff --git a/core/services/ocr2/plugins/ccip/execution_plugin_test.go b/core/services/ocr2/plugins/ccip/execution_plugin_test.go index dec6e2f5ad..ab1b27331b 100644 --- a/core/services/ocr2/plugins/ccip/execution_plugin_test.go +++ b/core/services/ocr2/plugins/ccip/execution_plugin_test.go @@ -14,10 +14,7 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/evm_2_evm_onramp" "github.com/smartcontractkit/chainlink/v2/core/logger" "github.com/smartcontractkit/chainlink/v2/core/services/job" - "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/config" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/testhelpers" - "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/tokendata/usdc" - "github.com/smartcontractkit/chainlink/v2/core/utils" ) func TestGetExecutionPluginFilterNamesFromSpec(t *testing.T) { @@ -57,7 +54,7 @@ func TestGetExecutionPluginFilterNamesFromSpec(t *testing.T) { for _, tc := range testCases { chainSet := &mocks.LegacyChainContainer{} t.Run(tc.description, func(t *testing.T) { - err := UnregisterExecPluginLpFilters(context.Background(), logger.TestLogger(t), tc.spec, chainSet) + err := UnregisterExecPluginLpFilters(context.Background(), logger.TestLogger(t), job.Job{OCR2OracleSpec: tc.spec}, chainSet) if tc.expectingErr { assert.Error(t, err) } else { @@ -75,23 +72,13 @@ func TestGetExecutionPluginFilterNames(t *testing.T) { mockOffRamp, offRampAddr := testhelpers.NewFakeOffRamp(t) mockOffRamp.SetDynamicConfig(evm_2_evm_offramp.EVM2EVMOffRampDynamicConfig{PriceRegistry: dstPriceRegAddr}) - mockOnRamp, onRampAddr := testhelpers.NewFakeOnRamp(t) + mockOnRamp, _ := testhelpers.NewFakeOnRamp(t) mockOnRamp.SetDynamicCfg(evm_2_evm_onramp.EVM2EVMOnRampDynamicConfig{PriceRegistry: srcPriceRegAddr}) - pluginConfig := config.ExecutionPluginJobSpecConfig{ - USDCConfig: config.USDCConfig{ - SourceTokenAddress: utils.RandomAddress(), - SourceMessageTransmitterAddress: utils.RandomAddress(), - AttestationAPI: "http://localhost:8080", - }, - } - srcLP := mocklp.NewLogPoller(t) srcFilters := []string{ - "Exec ccip sends - " + onRampAddr.String(), "Fee token added - 0xdAFea492D9c6733aE3d56B7ed1ADb60692c98bC9", "Fee token removed - 0xdAFea492D9c6733aE3d56B7ed1ADb60692c98bC9", - usdc.MESSAGE_SENT_FILTER_NAME + " - " + pluginConfig.USDCConfig.SourceMessageTransmitterAddress.Hex(), } for _, f := range srcFilters { srcLP.On("UnregisterFilter", f, mock.Anything).Return(nil) @@ -112,18 +99,12 @@ func TestGetExecutionPluginFilterNames(t *testing.T) { err := unregisterExecutionPluginLpFilters( context.Background(), - logger.TestLogger(t), srcLP, dstLP, mockOffRamp, - evm_2_evm_offramp.EVM2EVMOffRampStaticConfig{ - CommitStore: commitStoreAddr, - OnRamp: onRampAddr, - SourceChainSelector: 5009297550715157269, - }, + commitStoreAddr, mockOnRamp, nil, - pluginConfig, ) assert.NoError(t, err) diff --git a/core/services/ocr2/plugins/ccip/execution_reporting_plugin.go b/core/services/ocr2/plugins/ccip/execution_reporting_plugin.go index 01472c6a0c..e9e3493b37 100644 --- a/core/services/ocr2/plugins/ccip/execution_reporting_plugin.go +++ b/core/services/ocr2/plugins/ccip/execution_reporting_plugin.go @@ -58,7 +58,7 @@ var ( type ExecutionPluginConfig struct { lggr logger.Logger sourceLP, destLP logpoller.LogPoller - sourceReader ccipdata.Reader + onRampReader ccipdata.OnRampReader destReader ccipdata.Reader onRamp evm_2_evm_onramp.EVM2EVMOnRampInterface offRamp evm_2_evm_offramp.EVM2EVMOffRampInterface @@ -67,8 +67,8 @@ type ExecutionPluginConfig struct { sourceWrappedNativeToken common.Address destClient evmclient.Client sourceClient evmclient.Client + destChainEVMID *big.Int destGasEstimator gas.EvmFeeEstimator - leafHasher hashlib.LeafHasherInterface[[32]byte] tokenDataProviders map[common.Address]tokendata.Reader } @@ -203,15 +203,14 @@ func (r *ExecutionReportingPlugin) Observation(ctx context.Context, timestamp ty // UpdateLogPollerFilters updates the log poller filters for the source and destination chains. // pass zeroAddress if dstPriceRegistry is unknown, filters with zero address are omitted. +// TODO: Should be able to Close and re-create readers to abstract filters. func (rf *ExecutionReportingPluginFactory) UpdateLogPollerFilters(destPriceRegistry common.Address, qopts ...pg.QOpt) error { rf.filtersMu.Lock() defer rf.filtersMu.Unlock() // source chain filters sourceFiltersBefore, sourceFiltersNow := rf.sourceChainFilters, getExecutionPluginSourceLpChainFilters( - rf.config.onRamp.Address(), rf.config.sourcePriceRegistry.Address(), - rf.config.tokenDataProviders, ) created, deleted := logpollerutil.FiltersDiff(sourceFiltersBefore, sourceFiltersNow) if err := logpollerutil.UnregisterLpFilters(rf.config.sourceLP, deleted, qopts...); err != nil { @@ -776,11 +775,10 @@ func (r *ExecutionReportingPlugin) getReportsWithSendRequests( // use errgroup to fetch send request logs and executed sequence numbers in parallel eg := &errgroup.Group{} - var sendRequests []ccipdata.Event[evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested] + var sendRequests []ccipdata.Event[ccipdata.EVM2EVMMessage] eg.Go(func() error { - sendReqs, err := r.config.sourceReader.GetSendRequestsBetweenSeqNums( + sendReqs, err := r.config.onRampReader.GetSendRequestsBetweenSeqNums( ctx, - r.config.onRamp.Address(), intervalMin, intervalMax, int(r.offchainConfig.SourceFinalityDepth), @@ -821,19 +819,22 @@ func (r *ExecutionReportingPlugin) getReportsWithSendRequests( } for _, sendReq := range sendRequests { - msg := abihelpers.OnRampMessageToOffRampMessage(sendReq.Data.Message) + msg, err := r.config.onRampReader.ToOffRampMessage(sendReq.Data) + if err != nil { + return nil, err + } // if value exists in the map then it's executed // if value exists, and it's true then it's considered finalized finalized, executed := executedSeqNums[msg.SequenceNumber] reqWithMeta := internal.EVM2EVMOnRampCCIPSendRequestedWithMeta{ - InternalEVM2EVMMessage: msg, + InternalEVM2EVMMessage: *msg, BlockTimestamp: sendReq.BlockTimestamp, Executed: executed, Finalized: finalized, - LogIndex: sendReq.Data.Raw.Index, - TxHash: sendReq.Data.Raw.TxHash, + LogIndex: sendReq.LogIndex, + TxHash: sendReq.TxHash, } // attach the msg to the appropriate reports @@ -871,15 +872,15 @@ func (r *ExecutionReportingPlugin) buildReport(ctx context.Context, lggr logger. } lggr.Infow("Building execution report", "observations", observedMessages, "merkleRoot", hexutil.Encode(commitReport.MerkleRoot[:]), "report", commitReport) - sendReqsInRoot, leaves, tree, err := getProofData(ctx, lggr, r.config.leafHasher, r.config.onRamp.Address(), r.config.sourceReader, commitReport.Interval) + sendReqsInRoot, leaves, tree, err := getProofData(ctx, r.config.onRampReader, commitReport.Interval) if err != nil { return nil, err } messages := make([]*evm_2_evm_offramp.InternalEVM2EVMMessage, len(sendReqsInRoot)) for i, msg := range sendReqsInRoot { - offRampMsg := abihelpers.OnRampMessageToOffRampMessage(msg.Data.Message) - messages[i] = &offRampMsg + offRampMsg, _ := r.config.onRampReader.ToOffRampMessage(msg.Data) + messages[i] = offRampMsg } // cap messages which fits MaxExecutionReportLength (after serialized) diff --git a/core/services/ocr2/plugins/ccip/execution_reporting_plugin_test.go b/core/services/ocr2/plugins/ccip/execution_reporting_plugin_test.go index 18e1db63fc..30ee04add9 100644 --- a/core/services/ocr2/plugins/ccip/execution_reporting_plugin_test.go +++ b/core/services/ocr2/plugins/ccip/execution_reporting_plugin_test.go @@ -55,7 +55,7 @@ func TestExecutionReportingPlugin_Observation(t *testing.T) { commitStorePaused bool inflightReports []InflightInternalExecutionReport unexpiredReports []ccipdata.Event[commit_store.CommitStoreReportAccepted] - sendRequests []ccipdata.Event[evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested] + sendRequests []ccipdata.Event[ccipdata.EVM2EVMMessage] executedSeqNums []uint64 blessedRoots map[[32]byte]bool senderNonce uint64 @@ -89,21 +89,15 @@ func TestExecutionReportingPlugin_Observation(t *testing.T) { IsEnabled: false, }, senderNonce: 9, - sendRequests: []ccipdata.Event[evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested]{ + sendRequests: []ccipdata.Event[ccipdata.EVM2EVMMessage]{ { - Data: evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested{ - Message: evm_2_evm_onramp.InternalEVM2EVMMessage{SequenceNumber: 10}, - }, + Data: ccipdata.EVM2EVMMessage{SequenceNumber: 10}, }, { - Data: evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested{ - Message: evm_2_evm_onramp.InternalEVM2EVMMessage{SequenceNumber: 11}, - }, + Data: ccipdata.EVM2EVMMessage{SequenceNumber: 11}, }, { - Data: evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested{ - Message: evm_2_evm_onramp.InternalEVM2EVMMessage{SequenceNumber: 12}, - }, + Data: ccipdata.EVM2EVMMessage{SequenceNumber: 12}, }, }, }, @@ -140,13 +134,18 @@ func TestExecutionReportingPlugin_Observation(t *testing.T) { Return(executionEvents, nil).Maybe() p.config.destReader = destReader - onRamp, onRampAddr := testhelpers.NewFakeOnRamp(t) + onRamp, _ := testhelpers.NewFakeOnRamp(t) p.config.onRamp = onRamp - sourceReader := ccipdata.NewMockReader(t) - sourceReader.On("GetSendRequestsBetweenSeqNums", ctx, onRampAddr, mock.Anything, mock.Anything, 0). + sourceReader := ccipdata.NewMockOnRampReader(t) + sourceReader.On("GetSendRequestsBetweenSeqNums", ctx, mock.Anything, mock.Anything, 0). Return(tc.sendRequests, nil).Maybe() - p.config.sourceReader = sourceReader + if !tc.expErr { + sourceReader.On("ToOffRampMessage", mock.Anything).Return(&evm_2_evm_offramp.InternalEVM2EVMMessage{SequenceNumber: 10}, nil) + sourceReader.On("ToOffRampMessage", mock.Anything).Return(&evm_2_evm_offramp.InternalEVM2EVMMessage{SequenceNumber: 11}, nil) + sourceReader.On("ToOffRampMessage", mock.Anything).Return(&evm_2_evm_offramp.InternalEVM2EVMMessage{SequenceNumber: 12}, nil) + } + p.config.onRampReader = sourceReader cachedDestTokens := cache.NewMockAutoSync[cache.CachedTokens](t) cachedDestTokens.On("Get", ctx).Return(cache.CachedTokens{ @@ -373,34 +372,36 @@ func TestExecutionReportingPlugin_buildReport(t *testing.T) { }, nil) p.config.destReader = destReader - p.config.leafHasher = leafHasher123{} - - onRamp, onRampAddr := testhelpers.NewFakeOnRamp(t) + onRamp, _ := testhelpers.NewFakeOnRamp(t) p.config.onRamp = onRamp - sendReqs := make([]ccipdata.Event[evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested], len(observations)) + sendReqs := make([]ccipdata.Event[ccipdata.EVM2EVMMessage], len(observations)) + sourceReader := ccipdata.NewMockOnRampReader(t) for i := range observations { - sendReqs[i] = ccipdata.Event[evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested]{ - Data: evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested{Message: evm_2_evm_onramp.InternalEVM2EVMMessage{ - SourceChainSelector: math.MaxUint64, - SequenceNumber: uint64(i + 1), - FeeTokenAmount: big.NewInt(math.MaxInt64), - Sender: utils.RandomAddress(), - Nonce: math.MaxUint64, - GasLimit: big.NewInt(math.MaxInt64), - Strict: false, - Receiver: utils.RandomAddress(), - Data: bytes.Repeat([]byte{0}, bytesPerMessage), - TokenAmounts: nil, - FeeToken: utils.RandomAddress(), - MessageId: [32]byte{12}, - }}, + msg := evm_2_evm_offramp.InternalEVM2EVMMessage{ + SourceChainSelector: math.MaxUint64, + SequenceNumber: uint64(i + 1), + FeeTokenAmount: big.NewInt(math.MaxInt64), + Sender: utils.RandomAddress(), + Nonce: math.MaxUint64, + GasLimit: big.NewInt(math.MaxInt64), + Strict: false, + Receiver: utils.RandomAddress(), + Data: bytes.Repeat([]byte{0}, bytesPerMessage), + TokenAmounts: nil, + FeeToken: utils.RandomAddress(), + MessageId: [32]byte{12}, } + sendReqs[i] = ccipdata.Event[ccipdata.EVM2EVMMessage]{ + Data: ccipdata.EVM2EVMMessage{ + SequenceNumber: msg.SequenceNumber, + }, + } + sourceReader.On("ToOffRampMessage", mock.Anything).Return(&msg, nil) } - sourceReader := ccipdata.NewMockReader(t) sourceReader.On("GetSendRequestsBetweenSeqNums", - ctx, onRampAddr, observations[0].SeqNr, observations[len(observations)-1].SeqNr, 0).Return(sendReqs, nil) - p.config.sourceReader = sourceReader + ctx, observations[0].SeqNr, observations[len(observations)-1].SeqNr, 0).Return(sendReqs, nil) + p.config.onRampReader = sourceReader execReport, err := p.buildReport(ctx, p.lggr, observations) assert.NoError(t, err) @@ -932,7 +933,7 @@ func TestExecutionReportingPlugin_getReportsWithSendRequests(t *testing.T) { reports []commit_store.CommitStoreCommitReport expQueryMin uint64 // expected min/max used in the query to get ccipevents expQueryMax uint64 - onchainEvents []ccipdata.Event[evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested] + onchainEvents []ccipdata.Event[ccipdata.EVM2EVMMessage] destLatestBlock int64 destExecutedSeqNums []uint64 @@ -959,16 +960,10 @@ func TestExecutionReportingPlugin_getReportsWithSendRequests(t *testing.T) { }, expQueryMin: 1, expQueryMax: 3, - onchainEvents: []ccipdata.Event[evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested]{ - {Data: evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested{ - Message: evm_2_evm_onramp.InternalEVM2EVMMessage{SequenceNumber: 1}, - }}, - {Data: evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested{ - Message: evm_2_evm_onramp.InternalEVM2EVMMessage{SequenceNumber: 2}, - }}, - {Data: evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested{ - Message: evm_2_evm_onramp.InternalEVM2EVMMessage{SequenceNumber: 3}, - }}, + onchainEvents: []ccipdata.Event[ccipdata.EVM2EVMMessage]{ + {Data: ccipdata.EVM2EVMMessage{SequenceNumber: 1}}, + {Data: ccipdata.EVM2EVMMessage{SequenceNumber: 2}}, + {Data: ccipdata.EVM2EVMMessage{SequenceNumber: 3}}, }, destLatestBlock: 10_000, destExecutedSeqNums: []uint64{1}, @@ -1016,24 +1011,29 @@ func TestExecutionReportingPlugin_getReportsWithSendRequests(t *testing.T) { p := &ExecutionReportingPlugin{} p.lggr = lggr - onRamp, onRampAddr := testhelpers.NewFakeOnRamp(t) + onRamp, _ := testhelpers.NewFakeOnRamp(t) p.config.onRamp = onRamp offRamp, offRampAddr := testhelpers.NewFakeOffRamp(t) p.config.offRamp = offRamp - sourceReader := ccipdata.NewMockReader(t) - sourceReader.On("GetSendRequestsBetweenSeqNums", ctx, onRampAddr, tc.expQueryMin, tc.expQueryMax, 0). + sourceReader := ccipdata.NewMockOnRampReader(t) + sourceReader.On("GetSendRequestsBetweenSeqNums", ctx, tc.expQueryMin, tc.expQueryMax, 0). Return(tc.onchainEvents, nil).Maybe() - p.config.sourceReader = sourceReader + if len(tc.expReports) > 1 { + sourceReader.On("ToOffRampMessage", mock.Anything).Return(&evm_2_evm_offramp.InternalEVM2EVMMessage{SequenceNumber: 1}, nil).Once() + sourceReader.On("ToOffRampMessage", mock.Anything).Return(&evm_2_evm_offramp.InternalEVM2EVMMessage{SequenceNumber: 2}, nil).Once() + sourceReader.On("ToOffRampMessage", mock.Anything).Return(&evm_2_evm_offramp.InternalEVM2EVMMessage{SequenceNumber: 3}, nil).Once() + } + p.config.onRampReader = sourceReader destReader := ccipdata.NewMockReader(t) destReader.On("LatestBlock", ctx).Return(tc.destLatestBlock, nil).Maybe() var executedEvents []ccipdata.Event[evm_2_evm_offramp.EVM2EVMOffRampExecutionStateChanged] for _, executedSeqNum := range tc.destExecutedSeqNums { executedEvents = append(executedEvents, ccipdata.Event[evm_2_evm_offramp.EVM2EVMOffRampExecutionStateChanged]{ - Data: evm_2_evm_offramp.EVM2EVMOffRampExecutionStateChanged{SequenceNumber: executedSeqNum}, - BlockMeta: ccipdata.BlockMeta{BlockNumber: tc.destLatestBlock - 10}, + Data: evm_2_evm_offramp.EVM2EVMOffRampExecutionStateChanged{SequenceNumber: executedSeqNum}, + Meta: ccipdata.Meta{BlockNumber: tc.destLatestBlock - 10}, }) } destReader.On("GetExecutionStateChangesBetweenSeqNums", ctx, offRampAddr, tc.expQueryMin, tc.expQueryMax, 0).Return(executedEvents, nil).Maybe() @@ -1097,7 +1097,7 @@ func TestExecutionReportingPluginFactory_UpdateLogPollerFilters(t *testing.T) { }, } - for _, f := range getExecutionPluginSourceLpChainFilters(onRamp.Address(), sourcePriceRegistry.Address(), tokenDataProviders) { + for _, f := range getExecutionPluginSourceLpChainFilters(sourcePriceRegistry.Address()) { sourceLP.On("RegisterFilter", f).Return(nil) } for _, f := range getExecutionPluginDestLpChainFilters(commitStore.Address(), offRamp.Address(), destPriceRegistryAddr) { diff --git a/core/services/ocr2/plugins/ccip/internal/ccipdata/logpoller.go b/core/services/ocr2/plugins/ccip/internal/ccipdata/logpoller.go index e7d16c7d8a..0672f90083 100644 --- a/core/services/ocr2/plugins/ccip/internal/ccipdata/logpoller.go +++ b/core/services/ocr2/plugins/ccip/internal/ccipdata/logpoller.go @@ -2,16 +2,11 @@ package ccipdata import ( "context" - "fmt" - "math/big" "sync" "time" "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/rpc" - "github.com/pkg/errors" evmclient "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller" @@ -43,99 +38,6 @@ func NewLogPollerReader(lp logpoller.LogPoller, lggr logger.Logger, client evmcl } } -func (c *LogPollerReader) GetSendRequestsGteSeqNum(ctx context.Context, onRampAddress common.Address, seqNum uint64, checkFinalityTags bool, confs int) (sendReqs []Event[evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested], err error) { - onRamp, err := c.loadOnRamp(onRampAddress) - if err != nil { - return nil, err - } - - if !checkFinalityTags { - logs, err2 := c.lp.LogsDataWordGreaterThan( - abihelpers.EventSignatures.SendRequested, - onRampAddress, - abihelpers.EventSignatures.SendRequestedSequenceNumberWord, - abihelpers.EvmWord(seqNum), - confs, - pg.WithParentCtx(ctx), - ) - if err2 != nil { - return nil, fmt.Errorf("logs data word greater than: %w", err2) - } - return parseLogs[evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested]( - logs, - c.lggr, - func(log types.Log) (*evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested, error) { - return onRamp.ParseCCIPSendRequested(log) - }, - ) - } - - // If the chain is based on explicit finality we only examine logs less than or equal to the latest finalized block number. - // NOTE: there appears to be a bug in ethclient whereby BlockByNumber fails with "unsupported txtype" when trying to parse the block - // when querying L2s, headers however work. - // TODO (CCIP-778): Migrate to core finalized tags, below doesn't work for some chains e.g. Celo. - latestFinalizedHeader, err := c.client.HeaderByNumber( - ctx, - big.NewInt(rpc.FinalizedBlockNumber.Int64()), - ) - if err != nil { - return nil, err - } - - if latestFinalizedHeader == nil { - return nil, errors.New("latest finalized header is nil") - } - if latestFinalizedHeader.Number == nil { - return nil, errors.New("latest finalized number is nil") - } - logs, err := c.lp.LogsUntilBlockHashDataWordGreaterThan( - abihelpers.EventSignatures.SendRequested, - onRampAddress, - abihelpers.EventSignatures.SendRequestedSequenceNumberWord, - abihelpers.EvmWord(seqNum), - latestFinalizedHeader.Hash(), - pg.WithParentCtx(ctx), - ) - if err != nil { - return nil, fmt.Errorf("logs until block hash data word greater than: %w", err) - } - - return parseLogs[evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested]( - logs, - c.lggr, - func(log types.Log) (*evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested, error) { - return onRamp.ParseCCIPSendRequested(log) - }, - ) -} - -func (c *LogPollerReader) GetSendRequestsBetweenSeqNums(ctx context.Context, onRampAddress common.Address, seqNumMin, seqNumMax uint64, confs int) ([]Event[evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested], error) { - onRamp, err := c.loadOnRamp(onRampAddress) - if err != nil { - return nil, err - } - - logs, err := c.lp.LogsDataWordRange( - abihelpers.EventSignatures.SendRequested, - onRampAddress, - abihelpers.EventSignatures.SendRequestedSequenceNumberWord, - logpoller.EvmWord(seqNumMin), - logpoller.EvmWord(seqNumMax), - confs, - pg.WithParentCtx(ctx)) - if err != nil { - return nil, err - } - - return parseLogs[evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested]( - logs, - c.lggr, - func(log types.Log) (*evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested, error) { - return onRamp.ParseCCIPSendRequested(log) - }, - ) -} - func (c *LogPollerReader) GetTokenPriceUpdatesCreatedAfter(ctx context.Context, priceRegistryAddress common.Address, ts time.Time, confs int) ([]Event[price_registry.PriceRegistryUsdPerTokenUpdated], error) { priceRegistry, err := c.loadPriceRegistry(priceRegistryAddress) if err != nil { @@ -218,26 +120,6 @@ func (c *LogPollerReader) GetExecutionStateChangesBetweenSeqNums(ctx context.Con ) } -func (c *LogPollerReader) GetLastUSDCMessagePriorToLogIndexInTx(ctx context.Context, logIndex int64, txHash common.Hash) ([]byte, error) { - logs, err := c.lp.IndexedLogsByTxHash( - abihelpers.EventSignatures.USDCMessageSent, - txHash, - pg.WithParentCtx(ctx), - ) - if err != nil { - return nil, err - } - - for i := range logs { - current := logs[len(logs)-i-1] - if current.LogIndex < logIndex { - c.lggr.Infow("Found USDC message", "logIndex", current.LogIndex, "txHash", current.TxHash.Hex(), "data", hexutil.Encode(current.Data)) - return current.Data, nil - } - } - return nil, errors.Errorf("no USDC message found prior to log index %d in tx %s", logIndex, txHash.Hex()) -} - func (c *LogPollerReader) LatestBlock(ctx context.Context) (int64, error) { return c.lp.LatestBlock(pg.WithParentCtx(ctx)) } @@ -302,9 +184,11 @@ func parseLogs[T any](logs []logpoller.Log, lggr logger.Logger, parseFunc func(l if err == nil { reqs = append(reqs, Event[T]{ Data: *data, - BlockMeta: BlockMeta{ + Meta: Meta{ BlockTimestamp: log.BlockTimestamp, BlockNumber: log.BlockNumber, + TxHash: log.TxHash, + LogIndex: uint(log.LogIndex), }, }) } diff --git a/core/services/ocr2/plugins/ccip/internal/ccipdata/logpoller_test.go b/core/services/ocr2/plugins/ccip/internal/ccipdata/logpoller_test.go index 48e10c87e1..cfd5de87ec 100644 --- a/core/services/ocr2/plugins/ccip/internal/ccipdata/logpoller_test.go +++ b/core/services/ocr2/plugins/ccip/internal/ccipdata/logpoller_test.go @@ -1,21 +1,15 @@ package ccipdata import ( - "context" "fmt" - "math/big" "testing" "time" "github.com/ethereum/go-ethereum/core/types" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - evmClientMocks "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client/mocks" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller" - "github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller/mocks" "github.com/smartcontractkit/chainlink/v2/core/logger" - "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/abihelpers" "github.com/smartcontractkit/chainlink/v2/core/utils" ) @@ -73,107 +67,3 @@ func Test_parseLogs(t *testing.T) { assert.Greater(t, ev.BlockTimestamp, time.Now().Add(-time.Minute)) } } - -func TestLogPollerClient_GetSendRequestsGteSeqNum(t *testing.T) { - onRampAddr := utils.RandomAddress() - seqNum := uint64(100) - confs := 4 - - t.Run("using confs", func(t *testing.T) { - lp := mocks.NewLogPoller(t) - lp.On("LogsDataWordGreaterThan", - abihelpers.EventSignatures.SendRequested, - onRampAddr, - abihelpers.EventSignatures.SendRequestedSequenceNumberWord, - abihelpers.EvmWord(seqNum), - confs, - mock.Anything, - ).Return([]logpoller.Log{}, nil) - - c := &LogPollerReader{lp: lp} - events, err := c.GetSendRequestsGteSeqNum( - context.Background(), - onRampAddr, - seqNum, - false, - confs, - ) - assert.NoError(t, err) - assert.Empty(t, events) - lp.AssertExpectations(t) - }) - - t.Run("using latest confirmed block", func(t *testing.T) { - h := &types.Header{Number: big.NewInt(100000)} - - lp := mocks.NewLogPoller(t) - lp.On("LogsUntilBlockHashDataWordGreaterThan", - abihelpers.EventSignatures.SendRequested, - onRampAddr, - abihelpers.EventSignatures.SendRequestedSequenceNumberWord, - abihelpers.EvmWord(seqNum), - h.Hash(), - mock.Anything, - ).Return([]logpoller.Log{}, nil) - - cl := evmClientMocks.NewClient(t) - cl.On("HeaderByNumber", mock.Anything, mock.Anything).Return(h, nil) - - c := &LogPollerReader{lp: lp, client: cl} - events, err := c.GetSendRequestsGteSeqNum( - context.Background(), - onRampAddr, - seqNum, - true, - confs, - ) - assert.NoError(t, err) - assert.Empty(t, events) - lp.AssertExpectations(t) - cl.AssertExpectations(t) - }) -} - -func TestLogPollerClient_GetLastUSDCMessagePriorToLogIndexInTx(t *testing.T) { - txHash := utils.RandomAddress().Hash() - ccipLogIndex := int64(100) - - expectedData := []byte("-1") - - t.Run("multiple found", func(t *testing.T) { - lp := mocks.NewLogPoller(t) - lp.On("IndexedLogsByTxHash", - abihelpers.EventSignatures.USDCMessageSent, - txHash, - mock.Anything, - ).Return([]logpoller.Log{ - {LogIndex: ccipLogIndex - 2, Data: []byte("-2")}, - {LogIndex: ccipLogIndex - 1, Data: expectedData}, - {LogIndex: ccipLogIndex, Data: []byte("0")}, - {LogIndex: ccipLogIndex + 1, Data: []byte("1")}, - }, nil) - - c := &LogPollerReader{lp: lp, lggr: logger.TestLogger(t)} - usdcMessageData, err := c.GetLastUSDCMessagePriorToLogIndexInTx(context.Background(), ccipLogIndex, txHash) - assert.NoError(t, err) - assert.Equal(t, expectedData, usdcMessageData) - - lp.AssertExpectations(t) - }) - - t.Run("none found", func(t *testing.T) { - lp := mocks.NewLogPoller(t) - lp.On("IndexedLogsByTxHash", - abihelpers.EventSignatures.USDCMessageSent, - txHash, - mock.Anything, - ).Return([]logpoller.Log{}, nil) - - c := &LogPollerReader{lp: lp} - usdcMessageData, err := c.GetLastUSDCMessagePriorToLogIndexInTx(context.Background(), ccipLogIndex, txHash) - assert.Errorf(t, err, fmt.Sprintf("no USDC message found prior to log index %d in tx %s", ccipLogIndex, txHash.Hex())) - assert.Nil(t, usdcMessageData) - - lp.AssertExpectations(t) - }) -} diff --git a/core/services/ocr2/plugins/ccip/internal/ccipdata/onramp_reader.go b/core/services/ocr2/plugins/ccip/internal/ccipdata/onramp_reader.go new file mode 100644 index 0000000000..4d24d8ed17 --- /dev/null +++ b/core/services/ocr2/plugins/ccip/internal/ccipdata/onramp_reader.go @@ -0,0 +1,106 @@ +package ccipdata + +import ( + "context" + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/rpc" + "github.com/pkg/errors" + + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/evm_2_evm_offramp" + "github.com/smartcontractkit/chainlink/v2/core/logger" + ccipconfig "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/config" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/hashlib" +) + +type LeafHasherInterface[H hashlib.Hash] interface { + HashLeaf(log types.Log) (H, error) +} + +const ( + COMMIT_CCIP_SENDS = "Commit ccip sends" +) + +type Hash [32]byte + +func (h Hash) String() string { + return hexutil.Encode(h[:]) +} + +// EVM2EVMMessage is the interface for a message sent from the offramp to the onramp +// Plugin can operate against any lane version which has a message satisfying this interface. +type EVM2EVMMessage struct { + SequenceNumber uint64 + GasLimit *big.Int + Nonce uint64 + MessageId Hash + Hash Hash + // TODO: add more fields as we abstract exec plugin + // also this Log can eventually go away with destchain abstractions + Log types.Log // Raw event data +} + +//go:generate mockery --quiet --name OnRampReader --output . --filename onramp_reader_mock.go --inpackage --case=underscore +type OnRampReader interface { + // GetSendRequestsGteSeqNum returns all the message send requests with sequence number greater than or equal to the provided. + // If checkFinalityTags is set to true then confs param is ignored, the latest finalized block is used in the query. + GetSendRequestsGteSeqNum(ctx context.Context, seqNum uint64, confs int) ([]Event[EVM2EVMMessage], error) + + // GetSendRequestsBetweenSeqNums returns all the message send requests in the provided sequence numbers range (inclusive). + GetSendRequestsBetweenSeqNums(ctx context.Context, seqNumMin, seqNumMax uint64, confs int) ([]Event[EVM2EVMMessage], error) + + // Get router configured in the onRamp + RouterAddress() common.Address + + // TODO: temporary until we abstract offramp as well + // (currently this works since all versions are compatible with the same offramp ABI) + ToOffRampMessage(message EVM2EVMMessage) (*evm_2_evm_offramp.InternalEVM2EVMMessage, error) + + // Reader cleanup i.e. unsubscribe from logs + Close() error +} + +// NewOnRampReader determines the appropriate version of the onramp and returns a reader for it +func NewOnRampReader(lggr logger.Logger, sourceSelector, destSelector uint64, onRampAddress common.Address, sourceLP logpoller.LogPoller, source client.Client, finalityTags bool) (OnRampReader, error) { + contractType, version, err := ccipconfig.TypeAndVersion(onRampAddress, source) + if err != nil { + return nil, errors.Errorf("expected %v got %v", ccipconfig.EVM2EVMOnRamp, contractType) + } + switch version.String() { + case "1.0.0": + return NewOnRampV1_0_0(lggr, sourceSelector, destSelector, onRampAddress, sourceLP, source, finalityTags) + case "1.1.0": + return NewOnRampV1_1_0(lggr, sourceSelector, destSelector, onRampAddress, sourceLP, source, finalityTags) + case "1.2.0": + return NewOnRampV1_2_0(lggr, sourceSelector, destSelector, onRampAddress, sourceLP, source, finalityTags) + default: + return nil, errors.Errorf("expected version 1.0.0 got %v", version.String()) + } +} + +func latestFinalizedBlockHash(ctx context.Context, client client.Client) (common.Hash, error) { + // If the chain is based on explicit finality we only examine logs less than or equal to the latest finalized block number. + // NOTE: there appears to be a bug in ethclient whereby BlockByNumber fails with "unsupported txtype" when trying to parse the block + // when querying L2s, headers however work. + // TODO (CCIP-778): Migrate to core finalized tags, below doesn't work for some chains e.g. Celo. + latestFinalizedHeader, err := client.HeaderByNumber( + ctx, + big.NewInt(rpc.FinalizedBlockNumber.Int64()), + ) + if err != nil { + return common.Hash{}, err + } + + if latestFinalizedHeader == nil { + return common.Hash{}, errors.New("latest finalized header is nil") + } + if latestFinalizedHeader.Number == nil { + return common.Hash{}, errors.New("latest finalized number is nil") + } + return latestFinalizedHeader.Hash(), nil +} diff --git a/core/services/ocr2/plugins/ccip/internal/ccipdata/onramp_reader_mock.go b/core/services/ocr2/plugins/ccip/internal/ccipdata/onramp_reader_mock.go new file mode 100644 index 0000000000..0d76638610 --- /dev/null +++ b/core/services/ocr2/plugins/ccip/internal/ccipdata/onramp_reader_mock.go @@ -0,0 +1,140 @@ +// Code generated by mockery v2.28.1. DO NOT EDIT. + +package ccipdata + +import ( + context "context" + + common "github.com/ethereum/go-ethereum/common" + + evm_2_evm_offramp "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/evm_2_evm_offramp" + mock "github.com/stretchr/testify/mock" +) + +// MockOnRampReader is an autogenerated mock type for the OnRampReader type +type MockOnRampReader struct { + mock.Mock +} + +// Close provides a mock function with given fields: +func (_m *MockOnRampReader) Close() error { + ret := _m.Called() + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// GetSendRequestsBetweenSeqNums provides a mock function with given fields: ctx, seqNumMin, seqNumMax, confs +func (_m *MockOnRampReader) GetSendRequestsBetweenSeqNums(ctx context.Context, seqNumMin uint64, seqNumMax uint64, confs int) ([]Event[EVM2EVMMessage], error) { + ret := _m.Called(ctx, seqNumMin, seqNumMax, confs) + + var r0 []Event[EVM2EVMMessage] + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, uint64, uint64, int) ([]Event[EVM2EVMMessage], error)); ok { + return rf(ctx, seqNumMin, seqNumMax, confs) + } + if rf, ok := ret.Get(0).(func(context.Context, uint64, uint64, int) []Event[EVM2EVMMessage]); ok { + r0 = rf(ctx, seqNumMin, seqNumMax, confs) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]Event[EVM2EVMMessage]) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, uint64, uint64, int) error); ok { + r1 = rf(ctx, seqNumMin, seqNumMax, confs) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetSendRequestsGteSeqNum provides a mock function with given fields: ctx, seqNum, confs +func (_m *MockOnRampReader) GetSendRequestsGteSeqNum(ctx context.Context, seqNum uint64, confs int) ([]Event[EVM2EVMMessage], error) { + ret := _m.Called(ctx, seqNum, confs) + + var r0 []Event[EVM2EVMMessage] + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, uint64, int) ([]Event[EVM2EVMMessage], error)); ok { + return rf(ctx, seqNum, confs) + } + if rf, ok := ret.Get(0).(func(context.Context, uint64, int) []Event[EVM2EVMMessage]); ok { + r0 = rf(ctx, seqNum, confs) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]Event[EVM2EVMMessage]) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, uint64, int) error); ok { + r1 = rf(ctx, seqNum, confs) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RouterAddress provides a mock function with given fields: +func (_m *MockOnRampReader) RouterAddress() common.Address { + ret := _m.Called() + + var r0 common.Address + if rf, ok := ret.Get(0).(func() common.Address); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(common.Address) + } + } + + return r0 +} + +// ToOffRampMessage provides a mock function with given fields: message +func (_m *MockOnRampReader) ToOffRampMessage(message EVM2EVMMessage) (*evm_2_evm_offramp.InternalEVM2EVMMessage, error) { + ret := _m.Called(message) + + var r0 *evm_2_evm_offramp.InternalEVM2EVMMessage + var r1 error + if rf, ok := ret.Get(0).(func(EVM2EVMMessage) (*evm_2_evm_offramp.InternalEVM2EVMMessage, error)); ok { + return rf(message) + } + if rf, ok := ret.Get(0).(func(EVM2EVMMessage) *evm_2_evm_offramp.InternalEVM2EVMMessage); ok { + r0 = rf(message) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*evm_2_evm_offramp.InternalEVM2EVMMessage) + } + } + + if rf, ok := ret.Get(1).(func(EVM2EVMMessage) error); ok { + r1 = rf(message) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +type mockConstructorTestingTNewMockOnRampReader interface { + mock.TestingT + Cleanup(func()) +} + +// NewMockOnRampReader creates a new instance of MockOnRampReader. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewMockOnRampReader(t mockConstructorTestingTNewMockOnRampReader) *MockOnRampReader { + mock := &MockOnRampReader{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/core/services/ocr2/plugins/ccip/internal/ccipdata/onramp_v1_0_0.go b/core/services/ocr2/plugins/ccip/internal/ccipdata/onramp_v1_0_0.go new file mode 100644 index 0000000000..49232422cf --- /dev/null +++ b/core/services/ocr2/plugins/ccip/internal/ccipdata/onramp_v1_0_0.go @@ -0,0 +1,254 @@ +package ccipdata + +import ( + "context" + "fmt" + "math/big" + "strings" + + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/math" + "github.com/ethereum/go-ethereum/core/types" + "github.com/pkg/errors" + + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/evm_2_evm_offramp" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/evm_2_evm_onramp_1_0_0" + "github.com/smartcontractkit/chainlink/v2/core/logger" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/abihelpers" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/hashlib" + "github.com/smartcontractkit/chainlink/v2/core/services/pg" + "github.com/smartcontractkit/chainlink/v2/core/utils" +) + +const ( + CCIPSendRequestedEventNameV1_0_0 = "CCIPSendRequested" + MetaDataHashPrefixV1_0_0 = "EVM2EVMMessageEvent" +) + +var leafDomainSeparator = [1]byte{0x00} + +type LeafHasherV1_0_0 struct { + metaDataHash [32]byte + ctx hashlib.Ctx[[32]byte] + onRamp *evm_2_evm_onramp_1_0_0.EVM2EVMOnRamp +} + +func getMetaDataHash[H hashlib.Hash](ctx hashlib.Ctx[H], prefix [32]byte, sourceChainSelector uint64, onRampId common.Address, destChainSelector uint64) H { + paddedOnRamp := onRampId.Hash() + return ctx.Hash(utils.ConcatBytes(prefix[:], math.U256Bytes(big.NewInt(0).SetUint64(sourceChainSelector)), math.U256Bytes(big.NewInt(0).SetUint64(destChainSelector)), paddedOnRamp[:])) +} + +func NewLeafHasherV1_0_0(sourceChainSelector uint64, destChainSelector uint64, onRampId common.Address, ctx hashlib.Ctx[[32]byte], onRamp *evm_2_evm_onramp_1_0_0.EVM2EVMOnRamp) *LeafHasherV1_0_0 { + return &LeafHasherV1_0_0{ + metaDataHash: getMetaDataHash(ctx, ctx.Hash([]byte(MetaDataHashPrefixV1_0_0)), sourceChainSelector, onRampId, destChainSelector), + ctx: ctx, + onRamp: onRamp, + } +} + +func (t *LeafHasherV1_0_0) HashLeaf(log types.Log) ([32]byte, error) { + message, err := t.onRamp.ParseCCIPSendRequested(log) + if err != nil { + return [32]byte{}, err + } + encodedTokens, err := utils.ABIEncode( + `[ +{"components": [{"name":"token","type":"address"},{"name":"amount","type":"uint256"}], "type":"tuple[]"}]`, message.Message.TokenAmounts) + if err != nil { + return [32]byte{}, err + } + + packedValues, err := utils.ABIEncode( + `[ +{"name": "leafDomainSeparator","type":"bytes1"}, +{"name": "metadataHash", "type":"bytes32"}, +{"name": "sequenceNumber", "type":"uint64"}, +{"name": "nonce", "type":"uint64"}, +{"name": "sender", "type":"address"}, +{"name": "receiver", "type":"address"}, +{"name": "dataHash", "type":"bytes32"}, +{"name": "tokenAmountsHash", "type":"bytes32"}, +{"name": "gasLimit", "type":"uint256"}, +{"name": "strict", "type":"bool"}, +{"name": "feeToken","type": "address"}, +{"name": "feeTokenAmount","type": "uint256"} +]`, + leafDomainSeparator, + t.metaDataHash, + message.Message.SequenceNumber, + message.Message.Nonce, + message.Message.Sender, + message.Message.Receiver, + t.ctx.Hash(message.Message.Data), + t.ctx.Hash(encodedTokens), + message.Message.GasLimit, + message.Message.Strict, + message.Message.FeeToken, + message.Message.FeeTokenAmount, + ) + if err != nil { + return [32]byte{}, err + } + return t.ctx.Hash(packedValues), nil +} + +var _ OnRampReader = &OnRampV1_0_0{} + +type OnRampV1_0_0 struct { + address common.Address + onRamp *evm_2_evm_onramp_1_0_0.EVM2EVMOnRamp + finalityTags bool + lp logpoller.LogPoller + lggr logger.Logger + client client.Client + leafHasher LeafHasherInterface[[32]byte] + filterName string + sendRequestedEventSig common.Hash + sendRequestedSeqNumberWord int +} + +func (o *OnRampV1_0_0) GetLastUSDCMessagePriorToLogIndexInTx(ctx context.Context, logIndex int64, txHash common.Hash) ([]byte, error) { + return nil, errors.New("USDC not supported in < 1.2.0") +} + +func NewOnRampV1_0_0(lggr logger.Logger, sourceSelector, destSelector uint64, onRampAddress common.Address, sourceLP logpoller.LogPoller, source client.Client, finalityTags bool) (*OnRampV1_0_0, error) { + onRamp, err := evm_2_evm_onramp_1_0_0.NewEVM2EVMOnRamp(onRampAddress, source) + if err != nil { + return nil, err + } + onRampABI, err := abi.JSON(strings.NewReader(evm_2_evm_onramp_1_0_0.EVM2EVMOnRampABI)) + if err != nil { + return nil, err + } + // Subscribe to the relevant logs + name := logpoller.FilterName(COMMIT_CCIP_SENDS, onRampAddress) + eventSig := abihelpers.GetIDOrPanic(CCIPSendRequestedEventNameV1_0_0, onRampABI) + err = sourceLP.RegisterFilter(logpoller.Filter{ + Name: name, + EventSigs: []common.Hash{eventSig}, + Addresses: []common.Address{onRampAddress}, + }) + if err != nil { + return nil, err + } + return &OnRampV1_0_0{ + lggr: lggr, + address: onRampAddress, + onRamp: onRamp, + lp: sourceLP, + finalityTags: finalityTags, + leafHasher: NewLeafHasherV1_0_0(sourceSelector, destSelector, onRampAddress, hashlib.NewKeccakCtx(), onRamp), + filterName: name, + // offset || sourceChainID || seqNum || ... + sendRequestedSeqNumberWord: 2, + sendRequestedEventSig: eventSig, + }, nil +} + +func (o *OnRampV1_0_0) logToMessage(log types.Log) (*EVM2EVMMessage, error) { + msg, err := o.onRamp.ParseCCIPSendRequested(log) + if err != nil { + return nil, err + } + h, err := o.leafHasher.HashLeaf(log) + if err != nil { + return nil, err + } + return &EVM2EVMMessage{ + SequenceNumber: msg.Message.SequenceNumber, + GasLimit: msg.Message.GasLimit, + Nonce: msg.Message.Nonce, + Hash: h, + Log: log, + }, nil +} + +func (o *OnRampV1_0_0) GetSendRequestsGteSeqNum(ctx context.Context, seqNum uint64, confs int) ([]Event[EVM2EVMMessage], error) { + if !o.finalityTags { + logs, err2 := o.lp.LogsDataWordGreaterThan( + o.sendRequestedEventSig, + o.address, + o.sendRequestedSeqNumberWord, + abihelpers.EvmWord(seqNum), + confs, + pg.WithParentCtx(ctx), + ) + if err2 != nil { + return nil, fmt.Errorf("logs data word greater than: %w", err2) + } + return parseLogs[EVM2EVMMessage](logs, o.lggr, o.logToMessage) + } + latestFinalizedHash, err := latestFinalizedBlockHash(ctx, o.client) + if err != nil { + return nil, err + } + logs, err := o.lp.LogsUntilBlockHashDataWordGreaterThan( + o.sendRequestedEventSig, + o.address, + o.sendRequestedSeqNumberWord, + abihelpers.EvmWord(seqNum), + latestFinalizedHash, + pg.WithParentCtx(ctx), + ) + if err != nil { + return nil, fmt.Errorf("logs until block hash data word greater than: %w", err) + } + return parseLogs[EVM2EVMMessage](logs, o.lggr, o.logToMessage) +} + +func (o *OnRampV1_0_0) RouterAddress() common.Address { + config, _ := o.onRamp.GetDynamicConfig(nil) + return config.Router +} + +func (o *OnRampV1_0_0) GetSendRequestsBetweenSeqNums(ctx context.Context, seqNumMin, seqNumMax uint64, confs int) ([]Event[EVM2EVMMessage], error) { + logs, err := o.lp.LogsDataWordRange( + o.sendRequestedEventSig, + o.address, + o.sendRequestedSeqNumberWord, + logpoller.EvmWord(seqNumMin), + logpoller.EvmWord(seqNumMax), + confs, + pg.WithParentCtx(ctx)) + if err != nil { + return nil, err + } + return parseLogs[EVM2EVMMessage](logs, o.lggr, o.logToMessage) +} + +// TODO: follow up with offramp version abstraction +func (o *OnRampV1_0_0) ToOffRampMessage(message EVM2EVMMessage) (*evm_2_evm_offramp.InternalEVM2EVMMessage, error) { + m, err := o.onRamp.ParseCCIPSendRequested(message.Log) + if err != nil { + return nil, err + } + tokensAndAmounts := make([]evm_2_evm_offramp.ClientEVMTokenAmount, len(m.Message.TokenAmounts)) + for i, tokenAndAmount := range m.Message.TokenAmounts { + tokensAndAmounts[i] = evm_2_evm_offramp.ClientEVMTokenAmount{ + Token: tokenAndAmount.Token, + Amount: tokenAndAmount.Amount, + } + } + return &evm_2_evm_offramp.InternalEVM2EVMMessage{ + SourceChainSelector: m.Message.SourceChainSelector, + Sender: m.Message.Sender, + Receiver: m.Message.Receiver, + SequenceNumber: m.Message.SequenceNumber, + GasLimit: m.Message.GasLimit, + Strict: m.Message.Strict, + Nonce: m.Message.Nonce, + FeeToken: m.Message.FeeToken, + FeeTokenAmount: m.Message.FeeTokenAmount, + Data: m.Message.Data, + TokenAmounts: tokensAndAmounts, + SourceTokenData: make([][]byte, len(m.Message.TokenAmounts)), + MessageId: m.Message.MessageId, + }, nil +} + +func (o *OnRampV1_0_0) Close() error { + return o.lp.UnregisterFilter(o.filterName) +} diff --git a/core/services/ocr2/plugins/ccip/internal/hashlib/leaf_hasher_test.go b/core/services/ocr2/plugins/ccip/internal/ccipdata/onramp_v1_0_0_test.go similarity index 54% rename from core/services/ocr2/plugins/ccip/internal/hashlib/leaf_hasher_test.go rename to core/services/ocr2/plugins/ccip/internal/ccipdata/onramp_v1_0_0_test.go index e2e0978f74..0dd306a68d 100644 --- a/core/services/ocr2/plugins/ccip/internal/hashlib/leaf_hasher_test.go +++ b/core/services/ocr2/plugins/ccip/internal/ccipdata/onramp_v1_0_0_test.go @@ -1,27 +1,33 @@ -package hashlib +package ccipdata import ( "encoding/hex" "math/big" + "strings" "testing" + "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/stretchr/testify/require" - "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/evm_2_evm_onramp" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/evm_2_evm_onramp_1_0_0" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/abihelpers" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/hashlib" ) -func TestHasher(t *testing.T) { +func TestHasherV1_0_0(t *testing.T) { sourceChainSelector, destChainSelector := uint64(1), uint64(4) onRampAddress := common.HexToAddress("0x5550000000000000000000000000000000000001") + onRampABI, err := abi.JSON(strings.NewReader(evm_2_evm_onramp_1_0_0.EVM2EVMOnRampABI)) + require.NoError(t, err) - hashingCtx := NewKeccakCtx() - - hasher := NewLeafHasher(sourceChainSelector, destChainSelector, onRampAddress, hashingCtx) + ramp, err := evm_2_evm_onramp_1_0_0.NewEVM2EVMOnRamp(onRampAddress, nil) + require.NoError(t, err) + hashingCtx := hashlib.NewKeccakCtx() + hasher := NewLeafHasherV1_0_0(sourceChainSelector, destChainSelector, onRampAddress, hashingCtx, ramp) - message := evm_2_evm_onramp.InternalEVM2EVMMessage{ + message := evm_2_evm_onramp_1_0_0.InternalEVM2EVMMessage{ SourceChainSelector: sourceChainSelector, Sender: common.HexToAddress("0x1110000000000000000000000000000000000001"), Receiver: common.HexToAddress("0x2220000000000000000000000000000000000001"), @@ -32,20 +38,19 @@ func TestHasher(t *testing.T) { FeeToken: common.Address{}, FeeTokenAmount: big.NewInt(1), Data: []byte{}, - TokenAmounts: []evm_2_evm_onramp.ClientEVMTokenAmount{{Token: common.HexToAddress("0x4440000000000000000000000000000000000001"), Amount: big.NewInt(12345678900)}}, - SourceTokenData: [][]byte{}, + TokenAmounts: []evm_2_evm_onramp_1_0_0.ClientEVMTokenAmount{{Token: common.HexToAddress("0x4440000000000000000000000000000000000001"), Amount: big.NewInt(12345678900)}}, MessageId: [32]byte{}, } - pack, err := abihelpers.MessageArgs.Pack(message) + data, err := onRampABI.Events[CCIPSendRequestedEventNameV1_0_0].Inputs.Pack(message) require.NoError(t, err) - hash, err := hasher.HashLeaf(types.Log{Topics: []common.Hash{abihelpers.EventSignatures.SendRequested}, Data: pack}) + hash, err := hasher.HashLeaf(types.Log{Topics: []common.Hash{abihelpers.GetIDOrPanic("CCIPSendRequested", onRampABI)}, Data: data}) require.NoError(t, err) // NOTE: Must match spec - require.Equal(t, "46ad031bfb052db2e4a2514fed8dc480b98e5ce4acb55d5640d91407e0d8a3e9", hex.EncodeToString(hash[:])) + require.Equal(t, "26f282c6ac8231933b1799648d01ff6cec792a33fb37408b4d135968f9168ace", hex.EncodeToString(hash[:])) - message = evm_2_evm_onramp.InternalEVM2EVMMessage{ + message = evm_2_evm_onramp_1_0_0.InternalEVM2EVMMessage{ SourceChainSelector: sourceChainSelector, Sender: common.HexToAddress("0x1110000000000000000000000000000000000001"), Receiver: common.HexToAddress("0x2220000000000000000000000000000000000001"), @@ -56,27 +61,26 @@ func TestHasher(t *testing.T) { FeeToken: common.Address{}, FeeTokenAmount: big.NewInt(1e12), Data: []byte("foo bar baz"), - TokenAmounts: []evm_2_evm_onramp.ClientEVMTokenAmount{ + TokenAmounts: []evm_2_evm_onramp_1_0_0.ClientEVMTokenAmount{ {Token: common.HexToAddress("0x4440000000000000000000000000000000000001"), Amount: big.NewInt(12345678900)}, {Token: common.HexToAddress("0x6660000000000000000000000000000000000001"), Amount: big.NewInt(4204242)}, }, - SourceTokenData: [][]byte{{0x2, 0x1}}, - MessageId: [32]byte{}, + MessageId: [32]byte{}, } - pack, err = abihelpers.MessageArgs.Pack(message) + data, err = onRampABI.Events[CCIPSendRequestedEventNameV1_0_0].Inputs.Pack(message) require.NoError(t, err) - hash, err = hasher.HashLeaf(types.Log{Topics: []common.Hash{abihelpers.EventSignatures.SendRequested}, Data: pack}) + hash, err = hasher.HashLeaf(types.Log{Topics: []common.Hash{abihelpers.GetIDOrPanic("CCIPSendRequested", onRampABI)}, Data: data}) require.NoError(t, err) // NOTE: Must match spec - require.Equal(t, "4362a13a42e52ff5ce4324e7184dc7aa41704c3146bc842d35d95b94b32a78b6", hex.EncodeToString(hash[:])) + require.Equal(t, "05cee92e7cb86a37b6536554828a5b21ff20ac3d4ef821ec47056f1d963313de", hex.EncodeToString(hash[:])) } func TestMetaDataHash(t *testing.T) { sourceChainSelector, destChainSelector := uint64(1), uint64(4) onRampAddress := common.HexToAddress("0x5550000000000000000000000000000000000001") - ctx := NewKeccakCtx() - hash := GetMetaDataHash(ctx, ctx.Hash([]byte("EVM2EVMSubscriptionMessagePlus")), sourceChainSelector, onRampAddress, destChainSelector) - require.Equal(t, "e8b93c9d01a7a72ec6c7235e238701cf1511b267a31fdb78dd342649ee58c08d", hex.EncodeToString(hash[:])) + ctx := hashlib.NewKeccakCtx() + hash := getMetaDataHash(ctx, ctx.Hash([]byte(MetaDataHashPrefixV1_0_0)), sourceChainSelector, onRampAddress, destChainSelector) + require.Equal(t, "1409948abde219f43870c3d6d1c16beabd8878eb5039a3fa765eb56e4b8ded9e", hex.EncodeToString(hash[:])) } diff --git a/core/services/ocr2/plugins/ccip/internal/ccipdata/onramp_v1_1_0.go b/core/services/ocr2/plugins/ccip/internal/ccipdata/onramp_v1_1_0.go new file mode 100644 index 0000000000..eae5758be4 --- /dev/null +++ b/core/services/ocr2/plugins/ccip/internal/ccipdata/onramp_v1_1_0.go @@ -0,0 +1,38 @@ +package ccipdata + +import ( + "github.com/ethereum/go-ethereum/common" + + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/evm_2_evm_onramp_1_1_0" + "github.com/smartcontractkit/chainlink/v2/core/logger" +) + +var _ OnRampReader = &OnRampV1_1_0{} + +// OnRampV1_1_0 The only difference that the plugins care about in 1.1 is that the dynamic config struct has changed. +type OnRampV1_1_0 struct { + *OnRampV1_0_0 + onRamp *evm_2_evm_onramp_1_1_0.EVM2EVMOnRamp +} + +func NewOnRampV1_1_0(lggr logger.Logger, sourceSelector, destSelector uint64, onRampAddress common.Address, sourceLP logpoller.LogPoller, source client.Client, finalityTags bool) (*OnRampV1_1_0, error) { + onRamp, err := evm_2_evm_onramp_1_1_0.NewEVM2EVMOnRamp(onRampAddress, source) + if err != nil { + panic(err) // ABI failure ok to panic + } + onRamp100, err := NewOnRampV1_0_0(lggr, sourceSelector, destSelector, onRampAddress, sourceLP, source, finalityTags) + if err != nil { + return nil, err + } + return &OnRampV1_1_0{ + OnRampV1_0_0: onRamp100, + onRamp: onRamp, + }, nil +} + +func (o *OnRampV1_1_0) RouterAddress() common.Address { + config, _ := o.onRamp.GetDynamicConfig(nil) + return config.Router +} diff --git a/core/services/ocr2/plugins/ccip/internal/ccipdata/onramp_v1_2_0.go b/core/services/ocr2/plugins/ccip/internal/ccipdata/onramp_v1_2_0.go new file mode 100644 index 0000000000..d3a97a71cd --- /dev/null +++ b/core/services/ocr2/plugins/ccip/internal/ccipdata/onramp_v1_2_0.go @@ -0,0 +1,349 @@ +package ccipdata + +import ( + "context" + "fmt" + "math/big" + "strings" + + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/evm_2_evm_offramp" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/evm_2_evm_onramp" + "github.com/smartcontractkit/chainlink/v2/core/logger" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/abihelpers" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/hashlib" + "github.com/smartcontractkit/chainlink/v2/core/services/pg" + "github.com/smartcontractkit/chainlink/v2/core/utils" +) + +var ( + // Backwards compat for integration tests + CCIPSendRequestEventSigV1_2_0 common.Hash +) + +const ( + CCIPSendRequestSeqNumIndexV1_2_0 = 4 + CCIPSendRequestedEventNameV1_2_0 = "CCIPSendRequested" + EVM2EVMOffRampEventNameV1_2_0 = "EVM2EVMMessage" + MetaDataHashPrefixV1_2_0 = "EVM2EVMMessageHashV2" +) + +func init() { + onRampABI, err := abi.JSON(strings.NewReader(evm_2_evm_onramp.EVM2EVMOnRampABI)) + if err != nil { + panic(err) + } + CCIPSendRequestEventSigV1_2_0 = abihelpers.GetIDOrPanic(CCIPSendRequestedEventNameV1_2_0, onRampABI) +} + +// Backwards compat for integration tests +func DecodeOffRampMessageV1_2_0(b []byte) (*evm_2_evm_offramp.InternalEVM2EVMMessage, error) { + offRampABI, err := abi.JSON(strings.NewReader(evm_2_evm_offramp.EVM2EVMOffRampABI)) + if err != nil { + panic(err) + } + event, ok := offRampABI.Events[EVM2EVMOffRampEventNameV1_2_0] + if !ok { + panic("no such event") + } + unpacked, err := event.Inputs.Unpack(b) + if err != nil { + return nil, err + } + if len(unpacked) == 0 { + return nil, fmt.Errorf("no message found when unpacking") + } + + // Note must use unnamed type here + receivedCp, ok := unpacked[0].(struct { + SourceChainSelector uint64 `json:"sourceChainSelector"` + Sender common.Address `json:"sender"` + Receiver common.Address `json:"receiver"` + SequenceNumber uint64 `json:"sequenceNumber"` + GasLimit *big.Int `json:"gasLimit"` + Strict bool `json:"strict"` + Nonce uint64 `json:"nonce"` + FeeToken common.Address `json:"feeToken"` + FeeTokenAmount *big.Int `json:"feeTokenAmount"` + Data []uint8 `json:"data"` + TokenAmounts []struct { + Token common.Address `json:"token"` + Amount *big.Int `json:"amount"` + } `json:"tokenAmounts"` + SourceTokenData [][]byte `json:"sourceTokenData"` + MessageId [32]byte `json:"messageId"` + }) + if !ok { + return nil, fmt.Errorf("invalid format have %T want %T", unpacked[0], receivedCp) + } + var tokensAndAmounts []evm_2_evm_offramp.ClientEVMTokenAmount + for _, tokenAndAmount := range receivedCp.TokenAmounts { + tokensAndAmounts = append(tokensAndAmounts, evm_2_evm_offramp.ClientEVMTokenAmount{ + Token: tokenAndAmount.Token, + Amount: tokenAndAmount.Amount, + }) + } + + return &evm_2_evm_offramp.InternalEVM2EVMMessage{ + SourceChainSelector: receivedCp.SourceChainSelector, + Sender: receivedCp.Sender, + Receiver: receivedCp.Receiver, + SequenceNumber: receivedCp.SequenceNumber, + GasLimit: receivedCp.GasLimit, + Strict: receivedCp.Strict, + Nonce: receivedCp.Nonce, + FeeToken: receivedCp.FeeToken, + FeeTokenAmount: receivedCp.FeeTokenAmount, + Data: receivedCp.Data, + TokenAmounts: tokensAndAmounts, + SourceTokenData: receivedCp.SourceTokenData, + MessageId: receivedCp.MessageId, + }, nil +} + +type LeafHasherV1_2_0 struct { + metaDataHash [32]byte + ctx hashlib.Ctx[[32]byte] + onRamp *evm_2_evm_onramp.EVM2EVMOnRamp +} + +func NewLeafHasherV1_2_0(sourceChainSelector uint64, destChainSelector uint64, onRampId common.Address, ctx hashlib.Ctx[[32]byte], onRamp *evm_2_evm_onramp.EVM2EVMOnRamp) *LeafHasherV1_2_0 { + return &LeafHasherV1_2_0{ + metaDataHash: getMetaDataHash(ctx, ctx.Hash([]byte(MetaDataHashPrefixV1_2_0)), sourceChainSelector, onRampId, destChainSelector), + ctx: ctx, + onRamp: onRamp, + } +} + +func (t *LeafHasherV1_2_0) HashLeaf(log types.Log) ([32]byte, error) { + msg, err := t.onRamp.ParseCCIPSendRequested(log) + if err != nil { + return [32]byte{}, err + } + message := msg.Message + encodedTokens, err := utils.ABIEncode( + `[ +{"components": [{"name":"token","type":"address"},{"name":"amount","type":"uint256"}], "type":"tuple[]"}]`, message.TokenAmounts) + if err != nil { + return [32]byte{}, err + } + + bytesArray, err := abi.NewType("bytes[]", "bytes[]", nil) + if err != nil { + return [32]byte{}, err + } + + encodedSourceTokenData, err := abi.Arguments{abi.Argument{Type: bytesArray}}.PackValues([]interface{}{message.SourceTokenData}) + if err != nil { + return [32]byte{}, err + } + + packedFixedSizeValues, err := utils.ABIEncode( + `[ +{"name": "sender", "type":"address"}, +{"name": "receiver", "type":"address"}, +{"name": "sequenceNumber", "type":"uint64"}, +{"name": "gasLimit", "type":"uint256"}, +{"name": "strict", "type":"bool"}, +{"name": "nonce", "type":"uint64"}, +{"name": "feeToken","type": "address"}, +{"name": "feeTokenAmount","type": "uint256"} +]`, + message.Sender, + message.Receiver, + message.SequenceNumber, + message.GasLimit, + message.Strict, + message.Nonce, + message.FeeToken, + message.FeeTokenAmount, + ) + if err != nil { + return [32]byte{}, err + } + fixedSizeValuesHash := t.ctx.Hash(packedFixedSizeValues) + + packedValues, err := utils.ABIEncode( + `[ +{"name": "leafDomainSeparator","type":"bytes1"}, +{"name": "metadataHash", "type":"bytes32"}, +{"name": "fixedSizeValuesHash", "type":"bytes32"}, +{"name": "dataHash", "type":"bytes32"}, +{"name": "tokenAmountsHash", "type":"bytes32"}, +{"name": "sourceTokenDataHash", "type":"bytes32"} +]`, + leafDomainSeparator, + t.metaDataHash, + fixedSizeValuesHash, + t.ctx.Hash(message.Data), + t.ctx.Hash(encodedTokens), + t.ctx.Hash(encodedSourceTokenData), + ) + if err != nil { + return [32]byte{}, err + } + return t.ctx.Hash(packedValues), nil +} + +var _ OnRampReader = &OnRampV1_2_0{} + +// Significant change in 1.2: +// - CCIPSendRequested event signature has changed +type OnRampV1_2_0 struct { + onRamp *evm_2_evm_onramp.EVM2EVMOnRamp + address common.Address + lggr logger.Logger + lp logpoller.LogPoller + leafHasher LeafHasherInterface[[32]byte] + client client.Client + finalityTags bool + filterName string + sendRequestedEventSig common.Hash + sendRequestedSeqNumberWord int +} + +func (o *OnRampV1_2_0) logToMessage(log types.Log) (*EVM2EVMMessage, error) { + msg, err := o.onRamp.ParseCCIPSendRequested(log) + if err != nil { + return nil, err + } + h, err := o.leafHasher.HashLeaf(log) + if err != nil { + return nil, err + } + return &EVM2EVMMessage{ + SequenceNumber: msg.Message.SequenceNumber, + GasLimit: msg.Message.GasLimit, + Nonce: msg.Message.Nonce, + Hash: h, + Log: log, + }, nil +} + +func (o *OnRampV1_2_0) GetSendRequestsGteSeqNum(ctx context.Context, seqNum uint64, confs int) ([]Event[EVM2EVMMessage], error) { + if !o.finalityTags { + logs, err2 := o.lp.LogsDataWordGreaterThan( + o.sendRequestedEventSig, + o.address, + o.sendRequestedSeqNumberWord, + abihelpers.EvmWord(seqNum), + confs, + pg.WithParentCtx(ctx), + ) + if err2 != nil { + return nil, fmt.Errorf("logs data word greater than: %w", err2) + } + return parseLogs[EVM2EVMMessage](logs, o.lggr, o.logToMessage) + } + latestFinalizedHash, err := latestFinalizedBlockHash(ctx, o.client) + if err != nil { + return nil, err + } + logs, err := o.lp.LogsUntilBlockHashDataWordGreaterThan( + o.sendRequestedEventSig, + o.address, + o.sendRequestedSeqNumberWord, + abihelpers.EvmWord(seqNum), + latestFinalizedHash, + pg.WithParentCtx(ctx), + ) + if err != nil { + return nil, fmt.Errorf("logs until block hash data word greater than: %w", err) + } + return parseLogs[EVM2EVMMessage](logs, o.lggr, o.logToMessage) +} + +func (o *OnRampV1_2_0) GetSendRequestsBetweenSeqNums(ctx context.Context, seqNumMin, seqNumMax uint64, confs int) ([]Event[EVM2EVMMessage], error) { + logs, err := o.lp.LogsDataWordRange( + o.sendRequestedEventSig, + o.address, + o.sendRequestedSeqNumberWord, + logpoller.EvmWord(seqNumMin), + logpoller.EvmWord(seqNumMax), + confs, + pg.WithParentCtx(ctx)) + if err != nil { + return nil, err + } + return parseLogs[EVM2EVMMessage](logs, o.lggr, o.logToMessage) +} + +func (o *OnRampV1_2_0) RouterAddress() common.Address { + config, _ := o.onRamp.GetDynamicConfig(nil) + return config.Router +} + +func (o *OnRampV1_2_0) ToOffRampMessage(message EVM2EVMMessage) (*evm_2_evm_offramp.InternalEVM2EVMMessage, error) { + m, err := o.onRamp.ParseCCIPSendRequested(message.Log) + if err != nil { + return nil, err + } + tokensAndAmounts := make([]evm_2_evm_offramp.ClientEVMTokenAmount, len(m.Message.TokenAmounts)) + for i, tokenAndAmount := range m.Message.TokenAmounts { + tokensAndAmounts[i] = evm_2_evm_offramp.ClientEVMTokenAmount{ + Token: tokenAndAmount.Token, + Amount: tokenAndAmount.Amount, + } + } + return &evm_2_evm_offramp.InternalEVM2EVMMessage{ + SourceChainSelector: m.Message.SourceChainSelector, + Sender: m.Message.Sender, + Receiver: m.Message.Receiver, + SequenceNumber: m.Message.SequenceNumber, + GasLimit: m.Message.GasLimit, + Strict: m.Message.Strict, + Nonce: m.Message.Nonce, + FeeToken: m.Message.FeeToken, + FeeTokenAmount: m.Message.FeeTokenAmount, + Data: m.Message.Data, + TokenAmounts: tokensAndAmounts, + SourceTokenData: m.Message.SourceTokenData, // BREAKING CHANGE IN 1.2 + MessageId: m.Message.MessageId, + }, nil +} + +func (o *OnRampV1_2_0) Close() error { + return o.lp.UnregisterFilter(o.filterName) +} + +func NewOnRampV1_2_0( + lggr logger.Logger, + sourceSelector, + destSelector uint64, + onRampAddress common.Address, + sourceLP logpoller.LogPoller, + source client.Client, + finalityTags bool, +) (*OnRampV1_2_0, error) { + onRamp, err := evm_2_evm_onramp.NewEVM2EVMOnRamp(onRampAddress, source) + if err != nil { + panic(err) // ABI failure ok to panic + } + // Subscribe to the relevant logs + // Note we can keep the same prefix across 1.0/1.1 and 1.2 because the onramp addresses will be different + name := logpoller.FilterName(COMMIT_CCIP_SENDS, onRampAddress) + if err = sourceLP.RegisterFilter(logpoller.Filter{ + Name: name, + EventSigs: []common.Hash{CCIPSendRequestEventSigV1_2_0}, + Addresses: []common.Address{onRampAddress}, + }); err != nil { + return nil, err + } + return &OnRampV1_2_0{ + finalityTags: finalityTags, + lggr: lggr, + client: source, + lp: sourceLP, + leafHasher: NewLeafHasherV1_2_0(sourceSelector, destSelector, onRampAddress, hashlib.NewKeccakCtx(), onRamp), + onRamp: onRamp, + filterName: name, + address: onRampAddress, + sendRequestedSeqNumberWord: CCIPSendRequestSeqNumIndexV1_2_0, + sendRequestedEventSig: CCIPSendRequestEventSigV1_2_0, + }, nil +} diff --git a/core/services/ocr2/plugins/ccip/internal/ccipdata/onramp_v1_2_0_test.go b/core/services/ocr2/plugins/ccip/internal/ccipdata/onramp_v1_2_0_test.go new file mode 100644 index 0000000000..d271b87654 --- /dev/null +++ b/core/services/ocr2/plugins/ccip/internal/ccipdata/onramp_v1_2_0_test.go @@ -0,0 +1,147 @@ +package ccipdata + +import ( + "context" + "encoding/hex" + "math/big" + "strings" + "testing" + + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + + evmClientMocks "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client/mocks" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller/mocks" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/evm_2_evm_onramp" + "github.com/smartcontractkit/chainlink/v2/core/logger" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/abihelpers" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/hashlib" + "github.com/smartcontractkit/chainlink/v2/core/utils" +) + +func TestHasherV1_2_0(t *testing.T) { + sourceChainSelector, destChainSelector := uint64(1), uint64(4) + onRampAddress := common.HexToAddress("0x5550000000000000000000000000000000000001") + onRampABI, err := abi.JSON(strings.NewReader(evm_2_evm_onramp.EVM2EVMOnRampABI)) + require.NoError(t, err) + + hashingCtx := hashlib.NewKeccakCtx() + ramp, err := evm_2_evm_onramp.NewEVM2EVMOnRamp(onRampAddress, nil) + require.NoError(t, err) + hasher := NewLeafHasherV1_2_0(sourceChainSelector, destChainSelector, onRampAddress, hashingCtx, ramp) + + message := evm_2_evm_onramp.InternalEVM2EVMMessage{ + SourceChainSelector: sourceChainSelector, + Sender: common.HexToAddress("0x1110000000000000000000000000000000000001"), + Receiver: common.HexToAddress("0x2220000000000000000000000000000000000001"), + SequenceNumber: 1337, + GasLimit: big.NewInt(100), + Strict: false, + Nonce: 1337, + FeeToken: common.Address{}, + FeeTokenAmount: big.NewInt(1), + Data: []byte{}, + TokenAmounts: []evm_2_evm_onramp.ClientEVMTokenAmount{{Token: common.HexToAddress("0x4440000000000000000000000000000000000001"), Amount: big.NewInt(12345678900)}}, + SourceTokenData: [][]byte{}, + MessageId: [32]byte{}, + } + + data, err := onRampABI.Events[CCIPSendRequestedEventNameV1_2_0].Inputs.Pack(message) + require.NoError(t, err) + hash, err := hasher.HashLeaf(types.Log{Topics: []common.Hash{CCIPSendRequestEventSigV1_2_0}, Data: data}) + require.NoError(t, err) + + // NOTE: Must match spec + require.Equal(t, "46ad031bfb052db2e4a2514fed8dc480b98e5ce4acb55d5640d91407e0d8a3e9", hex.EncodeToString(hash[:])) + + message = evm_2_evm_onramp.InternalEVM2EVMMessage{ + SourceChainSelector: sourceChainSelector, + Sender: common.HexToAddress("0x1110000000000000000000000000000000000001"), + Receiver: common.HexToAddress("0x2220000000000000000000000000000000000001"), + SequenceNumber: 1337, + GasLimit: big.NewInt(100), + Strict: false, + Nonce: 1337, + FeeToken: common.Address{}, + FeeTokenAmount: big.NewInt(1e12), + Data: []byte("foo bar baz"), + TokenAmounts: []evm_2_evm_onramp.ClientEVMTokenAmount{ + {Token: common.HexToAddress("0x4440000000000000000000000000000000000001"), Amount: big.NewInt(12345678900)}, + {Token: common.HexToAddress("0x6660000000000000000000000000000000000001"), Amount: big.NewInt(4204242)}, + }, + SourceTokenData: [][]byte{{0x2, 0x1}}, + MessageId: [32]byte{}, + } + + data, err = onRampABI.Events[CCIPSendRequestedEventNameV1_2_0].Inputs.Pack(message) + require.NoError(t, err) + hash, err = hasher.HashLeaf(types.Log{Topics: []common.Hash{CCIPSendRequestEventSigV1_2_0}, Data: data}) + require.NoError(t, err) + + // NOTE: Must match spec + require.Equal(t, "4362a13a42e52ff5ce4324e7184dc7aa41704c3146bc842d35d95b94b32a78b6", hex.EncodeToString(hash[:])) +} + +func TestLogPollerClient_GetSendRequestsGteSeqNum(t *testing.T) { + onRampAddr := utils.RandomAddress() + seqNum := uint64(100) + confs := 4 + lggr := logger.TestLogger(t) + t.Run("using confs", func(t *testing.T) { + lp := mocks.NewLogPoller(t) + lp.On("RegisterFilter", mock.Anything).Return(nil) + onRampV2, err := NewOnRampV1_2_0(lggr, 1, 1, onRampAddr, lp, nil, false) + require.NoError(t, err) + lp.On("LogsDataWordGreaterThan", + onRampV2.sendRequestedEventSig, + onRampAddr, + onRampV2.sendRequestedSeqNumberWord, + abihelpers.EvmWord(seqNum), + confs, + mock.Anything, + ).Return([]logpoller.Log{}, nil) + + //c := &LogPollerReader{lp: lp} + events, err := onRampV2.GetSendRequestsGteSeqNum( + context.Background(), + seqNum, + confs, + ) + assert.NoError(t, err) + assert.Empty(t, events) + lp.AssertExpectations(t) + }) + + t.Run("using latest confirmed block", func(t *testing.T) { + h := &types.Header{Number: big.NewInt(100000)} + cl := evmClientMocks.NewClient(t) + cl.On("HeaderByNumber", mock.Anything, mock.Anything).Return(h, nil) + lp := mocks.NewLogPoller(t) + lp.On("RegisterFilter", mock.Anything).Return(nil) + onRampV2, err := NewOnRampV1_2_0(lggr, 1, 1, onRampAddr, lp, cl, true) + require.NoError(t, err) + lp.On("LogsUntilBlockHashDataWordGreaterThan", + onRampV2.sendRequestedEventSig, + onRampAddr, + onRampV2.sendRequestedSeqNumberWord, + abihelpers.EvmWord(seqNum), + h.Hash(), + mock.Anything, + ).Return([]logpoller.Log{}, nil) + + events, err := onRampV2.GetSendRequestsGteSeqNum( + context.Background(), + seqNum, + confs, + ) + assert.NoError(t, err) + assert.Empty(t, events) + lp.AssertExpectations(t) + cl.AssertExpectations(t) + }) +} diff --git a/core/services/ocr2/plugins/ccip/internal/ccipdata/reader.go b/core/services/ocr2/plugins/ccip/internal/ccipdata/reader.go index ffae5ec7ca..f670d4da8c 100644 --- a/core/services/ocr2/plugins/ccip/internal/ccipdata/reader.go +++ b/core/services/ocr2/plugins/ccip/internal/ccipdata/reader.go @@ -8,31 +8,25 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/commit_store" "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/evm_2_evm_offramp" - "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/evm_2_evm_onramp" "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/price_registry" ) type Event[T any] struct { Data T - BlockMeta + Meta } -type BlockMeta struct { +type Meta struct { BlockTimestamp time.Time BlockNumber int64 + TxHash common.Hash + LogIndex uint } // Client can be used to fetch CCIP related parsed on-chain data. // -//go:generate mockery --quiet --name Reader --output . --filename mock.go --inpackage --case=underscore +//go:generate mockery --quiet --name Reader --output . --filename reader_mock.go --inpackage --case=underscore type Reader interface { - // GetSendRequestsGteSeqNum returns all the message send requests with sequence number greater than or equal to the provided. - // If checkFinalityTags is set to true then confs param is ignored, the latest finalized block is used in the query. - GetSendRequestsGteSeqNum(ctx context.Context, onRamp common.Address, seqNum uint64, checkFinalityTags bool, confs int) ([]Event[evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested], error) - - // GetSendRequestsBetweenSeqNums returns all the message send requests in the provided sequence numbers range (inclusive). - GetSendRequestsBetweenSeqNums(ctx context.Context, onRamp common.Address, seqNumMin, seqNumMax uint64, confs int) ([]Event[evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested], error) - // GetTokenPriceUpdatesCreatedAfter returns all the token price updates that happened after the provided timestamp. GetTokenPriceUpdatesCreatedAfter(ctx context.Context, priceRegistry common.Address, ts time.Time, confs int) ([]Event[price_registry.PriceRegistryUsdPerTokenUpdated], error) @@ -48,9 +42,6 @@ type Reader interface { // GetAcceptedCommitReportsGteTimestamp returns all the commit reports with timestamp greater than or equal to the provided. GetAcceptedCommitReportsGteTimestamp(ctx context.Context, commitStoreAddress common.Address, ts time.Time, confs int) ([]Event[commit_store.CommitStoreReportAccepted], error) - // GetLastUSDCMessagePriorToLogIndexInTx returns the last USDC message that was sent before the provided log index in the given transaction. - GetLastUSDCMessagePriorToLogIndexInTx(ctx context.Context, logIndex int64, txHash common.Hash) ([]byte, error) - // LatestBlock returns the latest known/parsed block of the underlying implementation. LatestBlock(ctx context.Context) (int64, error) } diff --git a/core/services/ocr2/plugins/ccip/internal/ccipdata/mock.go b/core/services/ocr2/plugins/ccip/internal/ccipdata/reader_mock.go similarity index 69% rename from core/services/ocr2/plugins/ccip/internal/ccipdata/mock.go rename to core/services/ocr2/plugins/ccip/internal/ccipdata/reader_mock.go index 5790158c37..daabf4d95f 100644 --- a/core/services/ocr2/plugins/ccip/internal/ccipdata/mock.go +++ b/core/services/ocr2/plugins/ccip/internal/ccipdata/reader_mock.go @@ -10,8 +10,6 @@ import ( evm_2_evm_offramp "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/evm_2_evm_offramp" - evm_2_evm_onramp "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/evm_2_evm_onramp" - mock "github.com/stretchr/testify/mock" price_registry "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/price_registry" @@ -128,84 +126,6 @@ func (_m *MockReader) GetGasPriceUpdatesCreatedAfter(ctx context.Context, priceR return r0, r1 } -// GetLastUSDCMessagePriorToLogIndexInTx provides a mock function with given fields: ctx, logIndex, txHash -func (_m *MockReader) GetLastUSDCMessagePriorToLogIndexInTx(ctx context.Context, logIndex int64, txHash common.Hash) ([]byte, error) { - ret := _m.Called(ctx, logIndex, txHash) - - var r0 []byte - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, int64, common.Hash) ([]byte, error)); ok { - return rf(ctx, logIndex, txHash) - } - if rf, ok := ret.Get(0).(func(context.Context, int64, common.Hash) []byte); ok { - r0 = rf(ctx, logIndex, txHash) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]byte) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, int64, common.Hash) error); ok { - r1 = rf(ctx, logIndex, txHash) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// GetSendRequestsBetweenSeqNums provides a mock function with given fields: ctx, onRamp, seqNumMin, seqNumMax, confs -func (_m *MockReader) GetSendRequestsBetweenSeqNums(ctx context.Context, onRamp common.Address, seqNumMin uint64, seqNumMax uint64, confs int) ([]Event[evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested], error) { - ret := _m.Called(ctx, onRamp, seqNumMin, seqNumMax, confs) - - var r0 []Event[evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested] - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, common.Address, uint64, uint64, int) ([]Event[evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested], error)); ok { - return rf(ctx, onRamp, seqNumMin, seqNumMax, confs) - } - if rf, ok := ret.Get(0).(func(context.Context, common.Address, uint64, uint64, int) []Event[evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested]); ok { - r0 = rf(ctx, onRamp, seqNumMin, seqNumMax, confs) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]Event[evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested]) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, common.Address, uint64, uint64, int) error); ok { - r1 = rf(ctx, onRamp, seqNumMin, seqNumMax, confs) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// GetSendRequestsGteSeqNum provides a mock function with given fields: ctx, onRamp, seqNum, checkFinalityTags, confs -func (_m *MockReader) GetSendRequestsGteSeqNum(ctx context.Context, onRamp common.Address, seqNum uint64, checkFinalityTags bool, confs int) ([]Event[evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested], error) { - ret := _m.Called(ctx, onRamp, seqNum, checkFinalityTags, confs) - - var r0 []Event[evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested] - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, common.Address, uint64, bool, int) ([]Event[evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested], error)); ok { - return rf(ctx, onRamp, seqNum, checkFinalityTags, confs) - } - if rf, ok := ret.Get(0).(func(context.Context, common.Address, uint64, bool, int) []Event[evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested]); ok { - r0 = rf(ctx, onRamp, seqNum, checkFinalityTags, confs) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]Event[evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested]) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, common.Address, uint64, bool, int) error); ok { - r1 = rf(ctx, onRamp, seqNum, checkFinalityTags, confs) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - // GetTokenPriceUpdatesCreatedAfter provides a mock function with given fields: ctx, priceRegistry, ts, confs func (_m *MockReader) GetTokenPriceUpdatesCreatedAfter(ctx context.Context, priceRegistry common.Address, ts time.Time, confs int) ([]Event[price_registry.PriceRegistryUsdPerTokenUpdated], error) { ret := _m.Called(ctx, priceRegistry, ts, confs) diff --git a/core/services/ocr2/plugins/ccip/internal/ccipdata/usdc_reader.go b/core/services/ocr2/plugins/ccip/internal/ccipdata/usdc_reader.go new file mode 100644 index 0000000000..3c2a792e61 --- /dev/null +++ b/core/services/ocr2/plugins/ccip/internal/ccipdata/usdc_reader.go @@ -0,0 +1,97 @@ +package ccipdata + +import ( + "context" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/pkg/errors" + + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller" + "github.com/smartcontractkit/chainlink/v2/core/logger" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/abihelpers" + "github.com/smartcontractkit/chainlink/v2/core/services/pg" + "github.com/smartcontractkit/chainlink/v2/core/utils" +) + +const ( + MESSAGE_SENT_FILTER_NAME = "USDC message sent" +) + +//go:generate mockery --quiet --name USDCReader --output . --filename usdc_reader_mock.go --inpackage --case=underscore +type USDCReader interface { + // GetLastUSDCMessagePriorToLogIndexInTx returns the last USDC message that was sent before the provided log index in the given transaction. + GetLastUSDCMessagePriorToLogIndexInTx(ctx context.Context, logIndex int64, txHash common.Hash) ([]byte, error) + Close() error +} + +type USDCReaderImpl struct { + usdcMessageSent common.Hash + lp logpoller.LogPoller + filterName string + lggr logger.Logger +} + +func (u *USDCReaderImpl) Close() error { + return u.lp.UnregisterFilter(u.filterName) +} + +// usdcPayload has to match the onchain event emitted by the USDC message transmitter +type usdcPayload []byte + +func (d usdcPayload) AbiString() string { + return `[{"type": "bytes"}]` +} + +func (d usdcPayload) Validate() error { + if len(d) == 0 { + return errors.New("must be non-empty") + } + return nil +} + +func parseUSDCMessageSent(logData []byte) ([]byte, error) { + decodeAbiStruct, err := abihelpers.DecodeAbiStruct[usdcPayload](logData) + if err != nil { + return nil, err + } + return decodeAbiStruct, nil +} + +func (u *USDCReaderImpl) GetLastUSDCMessagePriorToLogIndexInTx(ctx context.Context, logIndex int64, txHash common.Hash) ([]byte, error) { + logs, err := u.lp.IndexedLogsByTxHash( + u.usdcMessageSent, + txHash, + pg.WithParentCtx(ctx), + ) + if err != nil { + return nil, err + } + + for i := range logs { + current := logs[len(logs)-i-1] + if current.LogIndex < logIndex { + u.lggr.Infow("Found USDC message", "logIndex", current.LogIndex, "txHash", current.TxHash.Hex(), "data", hexutil.Encode(current.Data)) + return parseUSDCMessageSent(current.Data) + } + } + return nil, errors.Errorf("no USDC message found prior to log index %d in tx %s", logIndex, txHash.Hex()) +} + +func NewUSDCReader(lggr logger.Logger, transmitter common.Address, lp logpoller.LogPoller) (*USDCReaderImpl, error) { + filterName := logpoller.FilterName(MESSAGE_SENT_FILTER_NAME, transmitter.Hex()) + eventSig := utils.Keccak256Fixed([]byte("MessageSent(bytes)")) + if err := lp.RegisterFilter(logpoller.Filter{ + Name: filterName, + EventSigs: []common.Hash{eventSig}, + Addresses: []common.Address{transmitter}, + }); err != nil { + return nil, err + } + return &USDCReaderImpl{ + lggr: lggr, + lp: lp, + usdcMessageSent: eventSig, + filterName: filterName, + }, nil +} diff --git a/core/services/ocr2/plugins/ccip/internal/ccipdata/usdc_reader_mock.go b/core/services/ocr2/plugins/ccip/internal/ccipdata/usdc_reader_mock.go new file mode 100644 index 0000000000..cf3623e208 --- /dev/null +++ b/core/services/ocr2/plugins/ccip/internal/ccipdata/usdc_reader_mock.go @@ -0,0 +1,71 @@ +// Code generated by mockery v2.28.1. DO NOT EDIT. + +package ccipdata + +import ( + context "context" + + common "github.com/ethereum/go-ethereum/common" + + mock "github.com/stretchr/testify/mock" +) + +// MockUSDCReader is an autogenerated mock type for the USDCReader type +type MockUSDCReader struct { + mock.Mock +} + +// Close provides a mock function with given fields: +func (_m *MockUSDCReader) Close() error { + ret := _m.Called() + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// GetLastUSDCMessagePriorToLogIndexInTx provides a mock function with given fields: ctx, logIndex, txHash +func (_m *MockUSDCReader) GetLastUSDCMessagePriorToLogIndexInTx(ctx context.Context, logIndex int64, txHash common.Hash) ([]byte, error) { + ret := _m.Called(ctx, logIndex, txHash) + + var r0 []byte + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, int64, common.Hash) ([]byte, error)); ok { + return rf(ctx, logIndex, txHash) + } + if rf, ok := ret.Get(0).(func(context.Context, int64, common.Hash) []byte); ok { + r0 = rf(ctx, logIndex, txHash) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]byte) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, int64, common.Hash) error); ok { + r1 = rf(ctx, logIndex, txHash) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +type mockConstructorTestingTNewMockUSDCReader interface { + mock.TestingT + Cleanup(func()) +} + +// NewMockUSDCReader creates a new instance of MockUSDCReader. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewMockUSDCReader(t mockConstructorTestingTNewMockUSDCReader) *MockUSDCReader { + mock := &MockUSDCReader{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/core/services/ocr2/plugins/ccip/internal/ccipdata/usdc_reader_test.go b/core/services/ocr2/plugins/ccip/internal/ccipdata/usdc_reader_test.go new file mode 100644 index 0000000000..88aa699382 --- /dev/null +++ b/core/services/ocr2/plugins/ccip/internal/ccipdata/usdc_reader_test.go @@ -0,0 +1,78 @@ +package ccipdata + +import ( + "context" + "fmt" + "testing" + + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller/mocks" + "github.com/smartcontractkit/chainlink/v2/core/logger" + "github.com/smartcontractkit/chainlink/v2/core/utils" +) + +func TestLogPollerClient_GetLastUSDCMessagePriorToLogIndexInTx(t *testing.T) { + txHash := utils.RandomAddress().Hash() + ccipLogIndex := int64(100) + + expectedData := "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000f80000000000000001000000020000000000048d71000000000000000000000000eb08f243e5d3fcff26a9e38ae5520a669f4019d000000000000000000000000023a04d5935ed8bc8e3eb78db3541f0abfb001c6e0000000000000000000000006cb3ed9b441eb674b58495c8b3324b59faff5243000000000000000000000000000000005425890298aed601595a70ab815c96711a31bc65000000000000000000000000ab4f961939bfe6a93567cc57c59eed7084ce2131000000000000000000000000000000000000000000000000000000000000271000000000000000000000000035e08285cfed1ef159236728f843286c55fc08610000000000000000" + expectedPostParse := "0x0000000000000001000000020000000000048d71000000000000000000000000eb08f243e5d3fcff26a9e38ae5520a669f4019d000000000000000000000000023a04d5935ed8bc8e3eb78db3541f0abfb001c6e0000000000000000000000006cb3ed9b441eb674b58495c8b3324b59faff5243000000000000000000000000000000005425890298aed601595a70ab815c96711a31bc65000000000000000000000000ab4f961939bfe6a93567cc57c59eed7084ce2131000000000000000000000000000000000000000000000000000000000000271000000000000000000000000035e08285cfed1ef159236728f843286c55fc0861" + lggr := logger.TestLogger(t) + + t.Run("multiple found", func(t *testing.T) { + lp := mocks.NewLogPoller(t) + lp.On("RegisterFilter", mock.Anything).Return(nil) + u, err := NewUSDCReader(lggr, utils.RandomAddress(), lp) + require.NoError(t, err) + lp.On("IndexedLogsByTxHash", + u.usdcMessageSent, + txHash, + mock.Anything, + ).Return([]logpoller.Log{ + {LogIndex: ccipLogIndex - 2, Data: []byte("-2")}, + {LogIndex: ccipLogIndex - 1, Data: hexutil.MustDecode(expectedData)}, + {LogIndex: ccipLogIndex, Data: []byte("0")}, + {LogIndex: ccipLogIndex + 1, Data: []byte("1")}, + }, nil) + + usdcMessageData, err := u.GetLastUSDCMessagePriorToLogIndexInTx(context.Background(), ccipLogIndex, txHash) + assert.NoError(t, err) + assert.Equal(t, expectedPostParse, hexutil.Encode(usdcMessageData)) + lp.AssertExpectations(t) + }) + + t.Run("none found", func(t *testing.T) { + lp := mocks.NewLogPoller(t) + lp.On("RegisterFilter", mock.Anything).Return(nil) + u, err := NewUSDCReader(lggr, utils.RandomAddress(), lp) + require.NoError(t, err) + lp.On("IndexedLogsByTxHash", + u.usdcMessageSent, + txHash, + mock.Anything, + ).Return([]logpoller.Log{}, nil) + + usdcMessageData, err := u.GetLastUSDCMessagePriorToLogIndexInTx(context.Background(), ccipLogIndex, txHash) + assert.Errorf(t, err, fmt.Sprintf("no USDC message found prior to log index %d in tx %s", ccipLogIndex, txHash.Hex())) + assert.Nil(t, usdcMessageData) + + lp.AssertExpectations(t) + }) +} + +func TestParse(t *testing.T) { + expectedBody, err := hexutil.Decode("0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000f80000000000000001000000020000000000048d71000000000000000000000000eb08f243e5d3fcff26a9e38ae5520a669f4019d000000000000000000000000023a04d5935ed8bc8e3eb78db3541f0abfb001c6e0000000000000000000000006cb3ed9b441eb674b58495c8b3324b59faff5243000000000000000000000000000000005425890298aed601595a70ab815c96711a31bc65000000000000000000000000ab4f961939bfe6a93567cc57c59eed7084ce2131000000000000000000000000000000000000000000000000000000000000271000000000000000000000000035e08285cfed1ef159236728f843286c55fc08610000000000000000") + require.NoError(t, err) + + parsedBody, err := parseUSDCMessageSent(expectedBody) + require.NoError(t, err) + + expectedPostParse := "0x0000000000000001000000020000000000048d71000000000000000000000000eb08f243e5d3fcff26a9e38ae5520a669f4019d000000000000000000000000023a04d5935ed8bc8e3eb78db3541f0abfb001c6e0000000000000000000000006cb3ed9b441eb674b58495c8b3324b59faff5243000000000000000000000000000000005425890298aed601595a70ab815c96711a31bc65000000000000000000000000ab4f961939bfe6a93567cc57c59eed7084ce2131000000000000000000000000000000000000000000000000000000000000271000000000000000000000000035e08285cfed1ef159236728f843286c55fc0861" + + require.Equal(t, expectedPostParse, hexutil.Encode(parsedBody)) +} diff --git a/core/services/ocr2/plugins/ccip/internal/hashlib/common.go b/core/services/ocr2/plugins/ccip/internal/hashlib/common.go index aab70e5e0c..28a4405bb4 100644 --- a/core/services/ocr2/plugins/ccip/internal/hashlib/common.go +++ b/core/services/ocr2/plugins/ccip/internal/hashlib/common.go @@ -1,13 +1,6 @@ package hashlib import ( - "github.com/pkg/errors" - - "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/commit_store" - "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/evm_2_evm_onramp" - "github.com/smartcontractkit/chainlink/v2/core/logger" - "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipcalc" - "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipdata" "github.com/smartcontractkit/chainlink/v2/core/utils" ) @@ -23,31 +16,3 @@ func BytesOfBytesKeccak(b [][]byte) ([32]byte, error) { } return h, nil } - -// LeavesFromIntervals Extracts the hashed leaves from a given set of logs -func LeavesFromIntervals( - lggr logger.Logger, - interval commit_store.CommitStoreInterval, - hasher LeafHasherInterface[[32]byte], - sendReqs []ccipdata.Event[evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested], -) ([][32]byte, error) { - var seqNrs []uint64 - for _, req := range sendReqs { - seqNrs = append(seqNrs, req.Data.Message.SequenceNumber) - } - - if !ccipcalc.ContiguousReqs(lggr, interval.Min, interval.Max, seqNrs) { - return nil, errors.Errorf("do not have full range [%v, %v] have %v", interval.Min, interval.Max, seqNrs) - } - var leaves [][32]byte - - for _, sendReq := range sendReqs { - hash, err2 := hasher.HashLeaf(sendReq.Data.Raw) - if err2 != nil { - return nil, err2 - } - leaves = append(leaves, hash) - } - - return leaves, nil -} diff --git a/core/services/ocr2/plugins/ccip/internal/hashlib/leaf_hasher.go b/core/services/ocr2/plugins/ccip/internal/hashlib/leaf_hasher.go deleted file mode 100644 index ac875d7965..0000000000 --- a/core/services/ocr2/plugins/ccip/internal/hashlib/leaf_hasher.go +++ /dev/null @@ -1,106 +0,0 @@ -package hashlib - -import ( - "math/big" - - "github.com/ethereum/go-ethereum/accounts/abi" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/math" - "github.com/ethereum/go-ethereum/core/types" - - "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/abihelpers" - "github.com/smartcontractkit/chainlink/v2/core/utils" -) - -type LeafHasherInterface[H Hash] interface { - HashLeaf(log types.Log) (H, error) -} - -var LeafDomainSeparator = [1]byte{0x00} - -func GetMetaDataHash[H Hash](ctx Ctx[H], prefix [32]byte, sourceChainSelector uint64, onRampId common.Address, destChainSelector uint64) H { - paddedOnRamp := onRampId.Hash() - return ctx.Hash(utils.ConcatBytes(prefix[:], math.U256Bytes(big.NewInt(0).SetUint64(sourceChainSelector)), math.U256Bytes(big.NewInt(0).SetUint64(destChainSelector)), paddedOnRamp[:])) -} - -type LeafHasher struct { - metaDataHash [32]byte - ctx Ctx[[32]byte] -} - -func NewLeafHasher(sourceChainSelector uint64, destChainSelector uint64, onRampId common.Address, ctx Ctx[[32]byte]) *LeafHasher { - return &LeafHasher{ - metaDataHash: GetMetaDataHash(ctx, ctx.Hash([]byte("EVM2EVMMessageHashV2")), sourceChainSelector, onRampId, destChainSelector), - ctx: ctx, - } -} - -var _ LeafHasherInterface[[32]byte] = &LeafHasher{} - -func (t *LeafHasher) HashLeaf(log types.Log) ([32]byte, error) { - message, err := abihelpers.DecodeOffRampMessage(log.Data) - if err != nil { - return [32]byte{}, err - } - - encodedTokens, err := abihelpers.TokenAmountsArgs.PackValues([]interface{}{message.TokenAmounts}) - if err != nil { - return [32]byte{}, err - } - - bytesArray, err := abi.NewType("bytes[]", "bytes[]", nil) - if err != nil { - return [32]byte{}, err - } - - encodedSourceTokenData, err := abi.Arguments{abi.Argument{Type: bytesArray}}.PackValues([]interface{}{message.SourceTokenData}) - if err != nil { - return [32]byte{}, err - } - - packedFixedSizeValues, err := utils.ABIEncode( - `[ -{"name": "sender", "type":"address"}, -{"name": "receiver", "type":"address"}, -{"name": "sequenceNumber", "type":"uint64"}, -{"name": "gasLimit", "type":"uint256"}, -{"name": "strict", "type":"bool"}, -{"name": "nonce", "type":"uint64"}, -{"name": "feeToken","type": "address"}, -{"name": "feeTokenAmount","type": "uint256"} -]`, - message.Sender, - message.Receiver, - message.SequenceNumber, - message.GasLimit, - message.Strict, - message.Nonce, - message.FeeToken, - message.FeeTokenAmount, - ) - if err != nil { - return [32]byte{}, err - } - fixedSizeValuesHash := t.ctx.Hash(packedFixedSizeValues) - - packedValues, err := utils.ABIEncode( - `[ -{"name": "leafDomainSeparator","type":"bytes1"}, -{"name": "metadataHash", "type":"bytes32"}, -{"name": "fixedSizeValuesHash", "type":"bytes32"}, -{"name": "dataHash", "type":"bytes32"}, -{"name": "tokenAmountsHash", "type":"bytes32"}, -{"name": "sourceTokenDataHash", "type":"bytes32"} -]`, - LeafDomainSeparator, - t.metaDataHash, - fixedSizeValuesHash, - t.ctx.Hash(message.Data), - t.ctx.Hash(encodedTokens), - t.ctx.Hash(encodedSourceTokenData), - ) - if err != nil { - return [32]byte{}, err - } - return t.ctx.Hash(packedValues), nil -} diff --git a/core/services/ocr2/plugins/ccip/testhelpers/ccip_contracts.go b/core/services/ocr2/plugins/ccip/testhelpers/ccip_contracts.go index 092650d0d2..aee8f196c6 100644 --- a/core/services/ocr2/plugins/ccip/testhelpers/ccip_contracts.go +++ b/core/services/ocr2/plugins/ccip/testhelpers/ccip_contracts.go @@ -38,6 +38,7 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/shared/generated/burn_mint_erc677" "github.com/smartcontractkit/chainlink/v2/core/logger" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/abihelpers" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipdata" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/hashlib" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/merklemulti" "github.com/smartcontractkit/chainlink/v2/core/utils" @@ -1358,7 +1359,7 @@ func (args *ManualExecArgs) execute(report *commit_store.CommitStoreCommitReport seqNr := args.seqNr // Build a merkle tree for the report mctx := hashlib.NewKeccakCtx() - leafHasher := hashlib.NewLeafHasher(args.SourceChainID, args.DestChainID, common.HexToAddress(args.OnRamp), mctx) + leafHasher := ccipdata.NewLeafHasherV1_2_0(args.SourceChainID, args.DestChainID, common.HexToAddress(args.OnRamp), mctx, &evm_2_evm_onramp.EVM2EVMOnRamp{}) onRampContract, err := evm_2_evm_onramp.NewEVM2EVMOnRamp(common.HexToAddress(args.OnRamp), args.SourceChain) if err != nil { return nil, err @@ -1385,7 +1386,7 @@ func (args *ManualExecArgs) execute(report *commit_store.CommitStoreCommitReport leaves = append(leaves, hash) if sendRequestedIterator.Event.Message.SequenceNumber == seqNr { fmt.Printf("Found proving %d %+v\n", curr, sendRequestedIterator.Event.Message) - msg, err2 := abihelpers.DecodeOffRampMessage(sendRequestedIterator.Event.Raw.Data) + msg, err2 := ccipdata.DecodeOffRampMessageV1_2_0(sendRequestedIterator.Event.Raw.Data) if err2 != nil { return nil, err2 } @@ -1475,13 +1476,3 @@ func GetBalance(t *testing.T, chain bind.ContractBackend, tokenAddr common.Addre require.NoError(t, err) return bal } - -func GenerateCCIPSendLog(t *testing.T, message evm_2_evm_onramp.InternalEVM2EVMMessage) types.Log { - pack, err := abihelpers.MessageArgs.Pack(message) - require.NoError(t, err) - - return types.Log{ - Topics: []common.Hash{abihelpers.EventSignatures.SendRequested}, - Data: pack, - } -} diff --git a/core/services/ocr2/plugins/ccip/testhelpers/integration/chainlink.go b/core/services/ocr2/plugins/ccip/testhelpers/integration/chainlink.go index 1b92a30996..3cb6ce5766 100644 --- a/core/services/ocr2/plugins/ccip/testhelpers/integration/chainlink.go +++ b/core/services/ocr2/plugins/ccip/testhelpers/integration/chainlink.go @@ -28,6 +28,7 @@ import ( "github.com/smartcontractkit/chainlink-relay/pkg/loop" ctfClient "github.com/smartcontractkit/chainlink/integration-tests/client" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client" v2 "github.com/smartcontractkit/chainlink/v2/core/chains/evm/config/toml" @@ -47,6 +48,7 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/services/keystore/keys/ocr2key" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/abihelpers" ccipconfig "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/config" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipdata" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/testhelpers" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/validate" "github.com/smartcontractkit/chainlink/v2/core/services/ocrbootstrap" @@ -155,9 +157,9 @@ func (node *Node) EventuallyHasReqSeqNum(t *testing.T, ccipContracts *CCIPIntegr ccipContracts.Source.Chain.Commit() ccipContracts.Dest.Chain.Commit() lgs, err := c.LogPoller().LogsDataWordRange( - abihelpers.EventSignatures.SendRequested, + ccipdata.CCIPSendRequestEventSigV1_2_0, onRamp, - abihelpers.EventSignatures.SendRequestedSequenceNumberWord, + ccipdata.CCIPSendRequestSeqNumIndexV1_2_0, abihelpers.EvmWord(uint64(seqNum)), abihelpers.EvmWord(uint64(seqNum)), 1, diff --git a/core/services/ocr2/plugins/ccip/tokendata/reader.go b/core/services/ocr2/plugins/ccip/tokendata/reader.go index ec3a2d7497..f6260116e3 100644 --- a/core/services/ocr2/plugins/ccip/tokendata/reader.go +++ b/core/services/ocr2/plugins/ccip/tokendata/reader.go @@ -4,7 +4,6 @@ import ( "context" "errors" - "github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal" ) @@ -18,7 +17,5 @@ var ( type Reader interface { // ReadTokenData returns the attestation bytes if ready, and throws an error if not ready. ReadTokenData(ctx context.Context, msg internal.EVM2EVMOnRampCCIPSendRequestedWithMeta) (tokenData []byte, err error) - - // GetSourceLogPollerFilters returns the filters that should be used for the source chain log poller - GetSourceLogPollerFilters() []logpoller.Filter + Close() error } diff --git a/core/services/ocr2/plugins/ccip/tokendata/reader_mock.go b/core/services/ocr2/plugins/ccip/tokendata/reader_mock.go index 9a87fafb34..54638a9d7a 100644 --- a/core/services/ocr2/plugins/ccip/tokendata/reader_mock.go +++ b/core/services/ocr2/plugins/ccip/tokendata/reader_mock.go @@ -5,9 +5,7 @@ package tokendata import ( context "context" - logpoller "github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller" internal "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal" - mock "github.com/stretchr/testify/mock" ) @@ -16,17 +14,15 @@ type MockReader struct { mock.Mock } -// GetSourceLogPollerFilters provides a mock function with given fields: -func (_m *MockReader) GetSourceLogPollerFilters() []logpoller.Filter { +// Close provides a mock function with given fields: +func (_m *MockReader) Close() error { ret := _m.Called() - var r0 []logpoller.Filter - if rf, ok := ret.Get(0).(func() []logpoller.Filter); ok { + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { r0 = rf() } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]logpoller.Filter) - } + r0 = ret.Error(0) } return r0 diff --git a/core/services/ocr2/plugins/ccip/tokendata/usdc/usdc.go b/core/services/ocr2/plugins/ccip/tokendata/usdc/usdc.go index 18e2479ca0..e9bfce3a54 100644 --- a/core/services/ocr2/plugins/ccip/tokendata/usdc/usdc.go +++ b/core/services/ocr2/plugins/ccip/tokendata/usdc/usdc.go @@ -11,11 +11,9 @@ import ( "strings" "sync" - "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/pkg/errors" - "github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller" "github.com/smartcontractkit/chainlink/v2/core/logger" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/abihelpers" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal" @@ -25,9 +23,8 @@ import ( ) const ( - apiVersion = "v1" - attestationPath = "attestations" - MESSAGE_SENT_FILTER_NAME = "USDC message sent" + apiVersion = "v1" + attestationPath = "attestations" ) type attestationStatus string @@ -37,20 +34,6 @@ const ( attestationStatusPending attestationStatus = "pending_confirmations" ) -// usdcPayload has to match the onchain event emitted by the USDC message transmitter -type usdcPayload []byte - -func (d usdcPayload) AbiString() string { - return `[{"type": "bytes"}]` -} - -func (d usdcPayload) Validate() error { - if len(d) == 0 { - return errors.New("must be non-empty") - } - return nil -} - // messageAndAttestation has to match the onchain struct `MessageAndAttestation` in the // USDC token pool. type messageAndAttestation struct { @@ -80,13 +63,9 @@ func (m messageAndAttestation) Validate() error { } type TokenDataReader struct { - lggr logger.Logger - sourceChainEvents ccipdata.Reader - attestationApi *url.URL - messageTransmitter common.Address - sourceToken common.Address - onRampAddress common.Address - + lggr logger.Logger + usdcReader ccipdata.USDCReader + attestationApi *url.URL // Cache of sequence number -> usdc message body usdcMessageHashCache map[uint64][]byte usdcMessageHashCacheMutex sync.Mutex @@ -99,14 +78,11 @@ type attestationResponse struct { var _ tokendata.Reader = &TokenDataReader{} -func NewUSDCTokenDataReader(lggr logger.Logger, sourceChainEvents ccipdata.Reader, usdcTokenAddress, usdcMessageTransmitterAddress, onRampAddress common.Address, usdcAttestationApi *url.URL) *TokenDataReader { +func NewUSDCTokenDataReader(lggr logger.Logger, usdcReader ccipdata.USDCReader, usdcAttestationApi *url.URL) *TokenDataReader { return &TokenDataReader{ - lggr: lggr.With("tokenDataProvider", "usdc"), - sourceChainEvents: sourceChainEvents, + lggr: lggr, + usdcReader: usdcReader, attestationApi: usdcAttestationApi, - messageTransmitter: usdcMessageTransmitterAddress, - onRampAddress: onRampAddress, - sourceToken: usdcTokenAddress, usdcMessageHashCache: make(map[uint64][]byte), } } @@ -160,16 +136,10 @@ func (s *TokenDataReader) getUSDCMessageBody(ctx context.Context, msg internal.E return body, nil } - usdcMessageBody, err := s.sourceChainEvents.GetLastUSDCMessagePriorToLogIndexInTx(ctx, int64(msg.LogIndex), msg.TxHash) + parsedMsgBody, err := s.usdcReader.GetLastUSDCMessagePriorToLogIndexInTx(ctx, int64(msg.LogIndex), msg.TxHash) if err != nil { return []byte{}, err } - - parsedMsgBody, err := decodeUSDCMessageSent(usdcMessageBody) - if err != nil { - return []byte{}, errors.Wrap(err, "failed parsing solidity struct") - } - s.lggr.Infow("Got USDC message body", "messageBody", hexutil.Encode(parsedMsgBody), "messageID", hexutil.Encode(msg.MessageId[:])) // Save the attempt in the cache in case the external call fails @@ -177,14 +147,6 @@ func (s *TokenDataReader) getUSDCMessageBody(ctx context.Context, msg internal.E return parsedMsgBody, nil } -func decodeUSDCMessageSent(logData []byte) ([]byte, error) { - decodeAbiStruct, err := abihelpers.DecodeAbiStruct[usdcPayload](logData) - if err != nil { - return nil, err - } - return decodeAbiStruct, nil -} - func (s *TokenDataReader) callAttestationApi(ctx context.Context, usdcMessageHash [32]byte) (attestationResponse, error) { fullAttestationUrl := fmt.Sprintf("%s/%s/%s/0x%x", s.attestationApi, apiVersion, attestationPath, usdcMessageHash) req, err := http.NewRequestWithContext(ctx, "GET", fullAttestationUrl, nil) @@ -215,12 +177,6 @@ func (s *TokenDataReader) callAttestationApi(ctx context.Context, usdcMessageHas return response, nil } -func (s *TokenDataReader) GetSourceLogPollerFilters() []logpoller.Filter { - return []logpoller.Filter{ - { - Name: logpoller.FilterName(MESSAGE_SENT_FILTER_NAME, s.messageTransmitter.Hex()), - EventSigs: []common.Hash{abihelpers.EventSignatures.USDCMessageSent}, - Addresses: []common.Address{s.messageTransmitter}, - }, - } +func (s *TokenDataReader) Close() error { + return s.usdcReader.Close() } diff --git a/core/services/ocr2/plugins/ccip/tokendata/usdc/usdc_blackbox_test.go b/core/services/ocr2/plugins/ccip/tokendata/usdc/usdc_blackbox_test.go index 26ba789cf1..6d47d94f74 100644 --- a/core/services/ocr2/plugins/ccip/tokendata/usdc/usdc_blackbox_test.go +++ b/core/services/ocr2/plugins/ccip/tokendata/usdc/usdc_blackbox_test.go @@ -7,78 +7,39 @@ import ( "net/http" "net/http/httptest" "net/url" - "strings" "testing" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" - "github.com/ethereum/go-ethereum/core/types" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/evm_2_evm_offramp" - "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/evm_2_evm_onramp" "github.com/smartcontractkit/chainlink/v2/core/logger" - "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/abihelpers" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipdata" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/tokendata/usdc" "github.com/smartcontractkit/chainlink/v2/core/utils" ) -var ( - mockOnRampAddress = utils.RandomAddress() - mockUSDCTokenAddress = utils.RandomAddress() - mockMsgTransmitter = utils.RandomAddress() -) - type attestationResponse struct { Status string `json:"status"` Attestation string `json:"attestation"` } -type messageAndAttestation struct { - Message []byte - Attestation []byte -} - -func (m messageAndAttestation) AbiString() string { - return ` - [{ - "components": [ - {"name": "message", "type": "bytes"}, - {"name": "attestation", "type": "bytes"} - ], - "type": "tuple" - }]` -} - -func (m messageAndAttestation) Validate() error { - return nil -} - -type usdcPayload []byte - -func (d usdcPayload) AbiString() string { - return `[{"type": "bytes"}]` -} - -func (d usdcPayload) Validate() error { - return nil -} - func TestUSDCReader_ReadTokenData(t *testing.T) { response := attestationResponse{ Status: "complete", Attestation: "0x9049623e91719ef2aa63c55f357be2529b0e7122ae552c18aff8db58b4633c4d3920ff03d3a6d1ddf11f06bf64d7fd60d45447ac81f527ba628877dc5ca759651b08ffae25a6d3b1411749765244f0a1c131cbfe04430d687a2e12fd9d2e6dc08e118ad95d94ad832332cf3c4f7a4f3da0baa803b7be024b02db81951c0f0714de1b", } - abiEncodedMessageBody, err := hexutil.Decode("0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000f80000000000000001000000020000000000048d71000000000000000000000000eb08f243e5d3fcff26a9e38ae5520a669f4019d000000000000000000000000023a04d5935ed8bc8e3eb78db3541f0abfb001c6e0000000000000000000000006cb3ed9b441eb674b58495c8b3324b59faff5243000000000000000000000000000000005425890298aed601595a70ab815c96711a31bc65000000000000000000000000ab4f961939bfe6a93567cc57c59eed7084ce2131000000000000000000000000000000000000000000000000000000000000271000000000000000000000000035e08285cfed1ef159236728f843286c55fc08610000000000000000") - require.NoError(t, err) - rawMessageBody, err := abihelpers.DecodeAbiStruct[usdcPayload](abiEncodedMessageBody) - require.NoError(t, err) + // Message is the bytes itself from MessageSend(bytes message) + // i.e. ABI parsed. + message := "0x0000000000000001000000020000000000048d71000000000000000000000000eb08f243e5d3fcff26a9e38ae5520a669f4019d000000000000000000000000023a04d5935ed8bc8e3eb78db3541f0abfb001c6e0000000000000000000000006cb3ed9b441eb674b58495c8b3324b59faff5243000000000000000000000000000000005425890298aed601595a70ab815c96711a31bc65000000000000000000000000ab4f961939bfe6a93567cc57c59eed7084ce2131000000000000000000000000000000000000000000000000000000000000271000000000000000000000000035e08285cfed1ef159236728f843286c55fc0861" + expectedMessageAndAttestation := "0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000000f80000000000000001000000020000000000048d71000000000000000000000000eb08f243e5d3fcff26a9e38ae5520a669f4019d000000000000000000000000023a04d5935ed8bc8e3eb78db3541f0abfb001c6e0000000000000000000000006cb3ed9b441eb674b58495c8b3324b59faff5243000000000000000000000000000000005425890298aed601595a70ab815c96711a31bc65000000000000000000000000ab4f961939bfe6a93567cc57c59eed7084ce2131000000000000000000000000000000000000000000000000000000000000271000000000000000000000000035e08285cfed1ef159236728f843286c55fc0861000000000000000000000000000000000000000000000000000000000000000000000000000000829049623e91719ef2aa63c55f357be2529b0e7122ae552c18aff8db58b4633c4d3920ff03d3a6d1ddf11f06bf64d7fd60d45447ac81f527ba628877dc5ca759651b08ffae25a6d3b1411749765244f0a1c131cbfe04430d687a2e12fd9d2e6dc08e118ad95d94ad832332cf3c4f7a4f3da0baa803b7be024b02db81951c0f0714de1b000000000000000000000000000000000000000000000000000000000000" + lggr := logger.TestLogger(t) ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - messageHash := utils.Keccak256Fixed(rawMessageBody) + messageHash := utils.Keccak256Fixed(hexutil.MustDecode(message)) expectedUrl := "/v1/attestations/0x" + hex.EncodeToString(messageHash[:]) require.Equal(t, expectedUrl, r.URL.Path) @@ -95,33 +56,16 @@ func TestUSDCReader_ReadTokenData(t *testing.T) { txHash := utils.RandomBytes32() logIndex := int64(4) - eventsClient := ccipdata.MockReader{} - eventsClient.On("GetSendRequestsBetweenSeqNums", - mock.Anything, - mockOnRampAddress, - seqNum, - seqNum, - 0, - ).Return([]ccipdata.Event[evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested]{ - { - Data: evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested{ - Raw: types.Log{ - TxHash: txHash, - Index: uint(logIndex), - }, - }, - }, - }, nil) - - eventsClient.On("GetLastUSDCMessagePriorToLogIndexInTx", + usdcReader := ccipdata.MockUSDCReader{} + usdcReader.On("GetLastUSDCMessagePriorToLogIndexInTx", mock.Anything, logIndex, common.Hash(txHash), - ).Return(abiEncodedMessageBody, nil) + ).Return(hexutil.MustDecode(message), nil) attestationURI, err := url.ParseRequestURI(ts.URL) require.NoError(t, err) - usdcService := usdc.NewUSDCTokenDataReader(logger.TestLogger(t), &eventsClient, mockUSDCTokenAddress, mockMsgTransmitter, mockOnRampAddress, attestationURI) + usdcService := usdc.NewUSDCTokenDataReader(lggr, &usdcReader, attestationURI) msgAndAttestation, err := usdcService.ReadTokenData(context.Background(), internal.EVM2EVMOnRampCCIPSendRequestedWithMeta{ InternalEVM2EVMMessage: evm_2_evm_offramp.InternalEVM2EVMMessage{ SequenceNumber: seqNum, @@ -130,15 +74,6 @@ func TestUSDCReader_ReadTokenData(t *testing.T) { LogIndex: uint(logIndex), }) require.NoError(t, err) - - attestationBytes, err := hex.DecodeString(strings.TrimPrefix(response.Attestation, "0x")) - require.NoError(t, err) - - encodeAbiStruct, err := abihelpers.EncodeAbiStruct[messageAndAttestation](messageAndAttestation{ - Message: rawMessageBody, - Attestation: attestationBytes, - }) - require.NoError(t, err) - - require.Equal(t, encodeAbiStruct, msgAndAttestation) + // Expected attestation for parsed body. + require.Equal(t, expectedMessageAndAttestation, hexutil.Encode(msgAndAttestation)) } diff --git a/core/services/ocr2/plugins/ccip/tokendata/usdc/usdc_test.go b/core/services/ocr2/plugins/ccip/tokendata/usdc/usdc_test.go index b383b5e228..226b7a8e1c 100644 --- a/core/services/ocr2/plugins/ccip/tokendata/usdc/usdc_test.go +++ b/core/services/ocr2/plugins/ccip/tokendata/usdc/usdc_test.go @@ -9,11 +9,10 @@ import ( "testing" "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/hexutil" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" - "github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller/mocks" "github.com/smartcontractkit/chainlink/v2/core/logger" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipdata" @@ -31,7 +30,10 @@ func TestUSDCReader_callAttestationApi(t *testing.T) { usdcMessageHash := "912f22a13e9ccb979b621500f6952b2afd6e75be7eadaed93fc2625fe11c52a2" attestationURI, err := url.ParseRequestURI("https://iris-api-sandbox.circle.com") require.NoError(t, err) - usdcService := NewUSDCTokenDataReader(logger.TestLogger(t), nil, mockUSDCTokenAddress, mockMsgTransmitter, mockOnRampAddress, attestationURI) + lggr := logger.TestLogger(t) + usdcReader, err := ccipdata.NewUSDCReader(lggr, mockMsgTransmitter, nil) + require.NoError(t, err) + usdcService := NewUSDCTokenDataReader(lggr, usdcReader, attestationURI) attestation, err := usdcService.callAttestationApi(context.Background(), [32]byte(common.FromHex(usdcMessageHash))) require.NoError(t, err) @@ -51,7 +53,12 @@ func TestUSDCReader_callAttestationApiMock(t *testing.T) { attestationURI, err := url.ParseRequestURI(ts.URL) require.NoError(t, err) - usdcService := NewUSDCTokenDataReader(logger.TestLogger(t), nil, mockUSDCTokenAddress, mockMsgTransmitter, mockOnRampAddress, attestationURI) + lggr := logger.TestLogger(t) + lp := mocks.NewLogPoller(t) + lp.On("RegisterFilter", mock.Anything).Return(nil) + usdcReader, err := ccipdata.NewUSDCReader(lggr, mockMsgTransmitter, lp) + require.NoError(t, err) + usdcService := NewUSDCTokenDataReader(lggr, usdcReader, attestationURI) attestation, err := usdcService.callAttestationApi(context.Background(), utils.RandomBytes32()) require.NoError(t, err) @@ -67,7 +74,12 @@ func TestUSDCReader_callAttestationApiMockError(t *testing.T) { attestationURI, err := url.ParseRequestURI(ts.URL) require.NoError(t, err) - usdcService := NewUSDCTokenDataReader(logger.TestLogger(t), nil, mockUSDCTokenAddress, mockMsgTransmitter, mockOnRampAddress, attestationURI) + lggr := logger.TestLogger(t) + lp := mocks.NewLogPoller(t) + lp.On("RegisterFilter", mock.Anything).Return(nil) + usdcReader, err := ccipdata.NewUSDCReader(lggr, mockMsgTransmitter, lp) + require.NoError(t, err) + usdcService := NewUSDCTokenDataReader(lggr, usdcReader, attestationURI) _, err = usdcService.callAttestationApi(context.Background(), utils.RandomBytes32()) require.Error(t, err) } @@ -82,47 +94,24 @@ func getMockUSDCEndpoint(t *testing.T, response attestationResponse) *httptest.S })) } -// Asserts the hard coded event signature matches Keccak256("MessageSent(bytes)") -func TestGetUSDCReaderSourceLPFilters(t *testing.T) { - usdcService := NewUSDCTokenDataReader(logger.TestLogger(t), nil, mockUSDCTokenAddress, mockMsgTransmitter, mockOnRampAddress, nil) - - filters := usdcService.GetSourceLogPollerFilters() - - require.Equal(t, 1, len(filters)) - filter := filters[0] - require.Equal(t, logpoller.FilterName(MESSAGE_SENT_FILTER_NAME, mockMsgTransmitter.Hex()), filter.Name) - hash, err := utils.Keccak256([]byte("MessageSent(bytes)")) - require.NoError(t, err) - require.Equal(t, hash, filter.EventSigs[0].Bytes()) - require.Equal(t, mockMsgTransmitter, filter.Addresses[0]) -} - func TestGetUSDCMessageBody(t *testing.T) { - expectedBody, err := hexutil.Decode("0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000f80000000000000001000000020000000000048d71000000000000000000000000eb08f243e5d3fcff26a9e38ae5520a669f4019d000000000000000000000000023a04d5935ed8bc8e3eb78db3541f0abfb001c6e0000000000000000000000006cb3ed9b441eb674b58495c8b3324b59faff5243000000000000000000000000000000005425890298aed601595a70ab815c96711a31bc65000000000000000000000000ab4f961939bfe6a93567cc57c59eed7084ce2131000000000000000000000000000000000000000000000000000000000000271000000000000000000000000035e08285cfed1ef159236728f843286c55fc08610000000000000000") - require.NoError(t, err) - - parsedBody, err := decodeUSDCMessageSent(expectedBody) - require.NoError(t, err) - - expectedPostParse := "0x0000000000000001000000020000000000048d71000000000000000000000000eb08f243e5d3fcff26a9e38ae5520a669f4019d000000000000000000000000023a04d5935ed8bc8e3eb78db3541f0abfb001c6e0000000000000000000000006cb3ed9b441eb674b58495c8b3324b59faff5243000000000000000000000000000000005425890298aed601595a70ab815c96711a31bc65000000000000000000000000ab4f961939bfe6a93567cc57c59eed7084ce2131000000000000000000000000000000000000000000000000000000000000271000000000000000000000000035e08285cfed1ef159236728f843286c55fc0861" - - require.Equal(t, expectedPostParse, hexutil.Encode(parsedBody)) - - sourceChainEventsMock := ccipdata.MockReader{} - sourceChainEventsMock.On("GetLastUSDCMessagePriorToLogIndexInTx", mock.Anything, mock.Anything, mock.Anything).Return(expectedBody, nil) + expectedBody := []byte("0x0000000000000001000000020000000000048d71000000000000000000000000eb08f243e5d3fcff26a9e38ae5520a669f4019d000000000000000000000000023a04d5935ed8bc8e3eb78db3541f0abfb001c6e0000000000000000000000006cb3ed9b441eb674b58495c8b3324b59faff5243000000000000000000000000000000005425890298aed601595a70ab815c96711a31bc65000000000000000000000000ab4f961939bfe6a93567cc57c59eed7084ce2131000000000000000000000000000000000000000000000000000000000000271000000000000000000000000035e08285cfed1ef159236728f843286c55fc0861") + usdcReader := ccipdata.MockUSDCReader{} + usdcReader.On("GetLastUSDCMessagePriorToLogIndexInTx", mock.Anything, mock.Anything, mock.Anything).Return(expectedBody, nil) - usdcService := NewUSDCTokenDataReader(logger.TestLogger(t), &sourceChainEventsMock, mockUSDCTokenAddress, mockMsgTransmitter, mockOnRampAddress, nil) + lggr := logger.TestLogger(t) + usdcService := NewUSDCTokenDataReader(lggr, &usdcReader, nil) // Make the first call and assert the underlying function is called body, err := usdcService.getUSDCMessageBody(context.Background(), internal.EVM2EVMOnRampCCIPSendRequestedWithMeta{}) require.NoError(t, err) - require.Equal(t, body, parsedBody) + require.Equal(t, body, expectedBody) - sourceChainEventsMock.AssertNumberOfCalls(t, "GetLastUSDCMessagePriorToLogIndexInTx", 1) + usdcReader.AssertNumberOfCalls(t, "GetLastUSDCMessagePriorToLogIndexInTx", 1) // Make another call and assert that the cache is used body, err = usdcService.getUSDCMessageBody(context.Background(), internal.EVM2EVMOnRampCCIPSendRequestedWithMeta{}) require.NoError(t, err) - require.Equal(t, body, parsedBody) - sourceChainEventsMock.AssertNumberOfCalls(t, "GetLastUSDCMessagePriorToLogIndexInTx", 1) + require.Equal(t, body, expectedBody) + usdcReader.AssertNumberOfCalls(t, "GetLastUSDCMessagePriorToLogIndexInTx", 1) }