From f478a0119cd183d3be840f73a232f237052d3978 Mon Sep 17 00:00:00 2001 From: Rens Rooimans Date: Tue, 5 Sep 2023 10:53:44 +0200 Subject: [PATCH 01/20] basic attestation service --- .../ocr2/plugins/ccip/config/config.go | 1 + .../ocr2/plugins/ccip/customtokens/usdc.go | 70 +++++++++++++++++ .../plugins/ccip/customtokens/usdc_test.go | 77 +++++++++++++++++++ .../ocr2/plugins/ccip/execution_plugin.go | 2 + .../ccip/execution_reporting_plugin.go | 2 + 5 files changed, 152 insertions(+) create mode 100644 core/services/ocr2/plugins/ccip/customtokens/usdc.go create mode 100644 core/services/ocr2/plugins/ccip/customtokens/usdc_test.go diff --git a/core/services/ocr2/plugins/ccip/config/config.go b/core/services/ocr2/plugins/ccip/config/config.go index 12e31dbf56..d46b50045d 100644 --- a/core/services/ocr2/plugins/ccip/config/config.go +++ b/core/services/ocr2/plugins/ccip/config/config.go @@ -14,4 +14,5 @@ type CommitPluginJobSpecConfig struct { // ExecutionPluginJobSpecConfig contains the plugin specific variables for the ccip.CCIPExecution plugin. type ExecutionPluginJobSpecConfig struct { SourceStartBlock, DestStartBlock int64 // Only for first time job add. + USDCAttestationApi string } diff --git a/core/services/ocr2/plugins/ccip/customtokens/usdc.go b/core/services/ocr2/plugins/ccip/customtokens/usdc.go new file mode 100644 index 0000000000..d2b1b8657f --- /dev/null +++ b/core/services/ocr2/plugins/ccip/customtokens/usdc.go @@ -0,0 +1,70 @@ +package customtokens + +import ( + "encoding/json" + "fmt" + "io" + "net/http" +) + +type USDCService struct { + attestationApi string +} + +type USDCAttestationResponse struct { + Status USDCAttestationStatus `json:"status"` + Attestation string `json:"attestation"` +} + +const ( + version = "v1" + attestationPath = "attestations" +) + +type USDCAttestationStatus string + +const ( + USDCAttestationStatusSuccess USDCAttestationStatus = "complete" + USDCAttestationStatusPending USDCAttestationStatus = "pending_confirmations" +) + +func NewUSDCService(usdcAttestationApi string) *USDCService { + return &USDCService{attestationApi: usdcAttestationApi} +} + +func (usdc *USDCService) TryGetAttestation(messageHash string) (USDCAttestationResponse, error) { + fullAttestationUrl := fmt.Sprintf("%s/%s/%s/%s", usdc.attestationApi, version, attestationPath, messageHash) + req, err := http.NewRequest("GET", fullAttestationUrl, nil) + if err != nil { + return USDCAttestationResponse{}, err + } + req.Header.Add("accept", "application/json") + res, err := http.DefaultClient.Do(req) + if err != nil { + return USDCAttestationResponse{}, err + } + defer res.Body.Close() + body, err := io.ReadAll(res.Body) + if err != nil { + return USDCAttestationResponse{}, err + } + + var response USDCAttestationResponse + err = json.Unmarshal(body, &response) + if err != nil { + return USDCAttestationResponse{}, err + } + + return response, nil +} + +func (usdc *USDCService) IsAttestationComplete(messageHash string) (bool, string, error) { + response, err := usdc.TryGetAttestation(messageHash) + if err != nil { + return false, "", err + } + if response.Status == USDCAttestationStatusSuccess { + return true, response.Attestation, nil + } + return false, "", nil +} diff --git a/core/services/ocr2/plugins/ccip/customtokens/usdc_test.go b/core/services/ocr2/plugins/ccip/customtokens/usdc_test.go new file mode 100644 index 0000000000..3b9c0c5e41 --- /dev/null +++ b/core/services/ocr2/plugins/ccip/customtokens/usdc_test.go @@ -0,0 +1,77 @@ +package customtokens + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestUSDCService_TryGetAttestation(t *testing.T) { + t.Skipf("Skipping test because it uses the real USDC attestation API") + usdcMessageHash := "0x912f22a13e9ccb979b621500f6952b2afd6e75be7eadaed93fc2625fe11c52a2" + usdcService := NewUSDCService("https://iris-api-sandbox.circle.com") + + attestation, err := usdcService.TryGetAttestation(usdcMessageHash) + require.NoError(t, err) + + require.Equal(t, USDCAttestationStatusPending, attestation.Status) + require.Equal(t, "PENDING", attestation.Attestation) +} + +func TestUSDCService_TryGetAttestationMock(t *testing.T) { + response := USDCAttestationResponse{ + Status: USDCAttestationStatusSuccess, + Attestation: "720502893578a89a8a87982982ef781c18b193", + } + + ts := getMockUSDCEndpoint(t, response) + defer ts.Close() + + usdcService := NewUSDCService(ts.URL) + attestation, err := usdcService.TryGetAttestation("0x912f22a13e9ccb979b621500f6952b2afd6e75be7eadaed93fc2625fe11c52a2") + require.NoError(t, err) + + require.Equal(t, response.Status, attestation.Status) + require.Equal(t, response.Attestation, attestation.Attestation) +} + +func TestUSDCService_TryGetAttestationMockError(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + })) + defer ts.Close() + + usdcService := NewUSDCService(ts.URL) + _, err := usdcService.TryGetAttestation("0x912f22a13e9ccb979b621500f6952b2afd6e75be7eadaed93fc2625fe11c52a2") + require.Error(t, err) +} + +func TestUSDCService_IsAttestationComplete(t *testing.T) { + response := USDCAttestationResponse{ + Status: USDCAttestationStatusSuccess, + Attestation: "720502893578a89a8a87982982ef781c18b193", + } + + ts := getMockUSDCEndpoint(t, response) + defer ts.Close() + + usdcService := NewUSDCService(ts.URL) + isReady, attestation, err := usdcService.IsAttestationComplete("0x912f22a13e9ccb979b621500f6952b2afd6e75be7eadaed93fc2625fe11c52a2") + require.NoError(t, err) + + require.True(t, isReady) + require.Equal(t, response.Attestation, attestation) +} + +func getMockUSDCEndpoint(t *testing.T, response USDCAttestationResponse) *httptest.Server { + responseBytes, err := json.Marshal(response) + require.NoError(t, err) + + return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, err := w.Write(responseBytes) + require.NoError(t, err) + })) +} diff --git a/core/services/ocr2/plugins/ccip/execution_plugin.go b/core/services/ocr2/plugins/ccip/execution_plugin.go index 45185988a7..6a4506b45c 100644 --- a/core/services/ocr2/plugins/ccip/execution_plugin.go +++ b/core/services/ocr2/plugins/ccip/execution_plugin.go @@ -28,6 +28,7 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/abihelpers" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/ccipevents" ccipconfig "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/config" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/customtokens" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/hasher" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/observability" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/promwrapper" @@ -122,6 +123,7 @@ func NewExecutionServices(lggr logger.Logger, jb job.Job, chainSet evm.LegacyCha sourceClient: sourceChain.Client(), destGasEstimator: destChain.GasEstimator(), leafHasher: hasher.NewLeafHasher(offRampConfig.SourceChainSelector, offRampConfig.ChainSelector, onRamp.Address(), hasher.NewKeccakCtx()), + usdcService: customtokens.NewUSDCService(pluginConfig.USDCAttestationApi), }) err = wrappedPluginFactory.UpdateLogPollerFilters(zeroAddress) diff --git a/core/services/ocr2/plugins/ccip/execution_reporting_plugin.go b/core/services/ocr2/plugins/ccip/execution_reporting_plugin.go index 21224cdc87..fbcd518351 100644 --- a/core/services/ocr2/plugins/ccip/execution_reporting_plugin.go +++ b/core/services/ocr2/plugins/ccip/execution_reporting_plugin.go @@ -34,6 +34,7 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/cache" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/ccipevents" ccipconfig "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/config" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/customtokens" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/hasher" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/observability" "github.com/smartcontractkit/chainlink/v2/core/services/pg" @@ -66,6 +67,7 @@ type ExecutionPluginConfig struct { sourceClient evmclient.Client destGasEstimator gas.EvmFeeEstimator leafHasher hasher.LeafHasherInterface[[32]byte] + usdcService *customtokens.USDCService } type ExecutionReportingPlugin struct { From 7a883b7920055a248a2ebb5b24a8c3c688a13903 Mon Sep 17 00:00:00 2001 From: Rens Rooimans Date: Tue, 5 Sep 2023 13:10:24 +0200 Subject: [PATCH 02/20] add hard coded LP subscription logic --- .../ocr2/plugins/ccip/customtokens/usdc.go | 38 ++++++++++++++++--- .../plugins/ccip/customtokens/usdc_test.go | 26 +++++++++++-- .../ocr2/plugins/ccip/execution_plugin.go | 20 +++++++--- .../plugins/ccip/execution_plugin_test.go | 6 ++- .../ccip/execution_reporting_plugin.go | 6 ++- .../ccip/execution_reporting_plugin_test.go | 7 +++- 6 files changed, 85 insertions(+), 18 deletions(-) diff --git a/core/services/ocr2/plugins/ccip/customtokens/usdc.go b/core/services/ocr2/plugins/ccip/customtokens/usdc.go index d2b1b8657f..f8c0a13f17 100644 --- a/core/services/ocr2/plugins/ccip/customtokens/usdc.go +++ b/core/services/ocr2/plugins/ccip/customtokens/usdc.go @@ -5,10 +5,16 @@ import ( "fmt" "io" "net/http" + + "github.com/ethereum/go-ethereum/common" + + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller" ) type USDCService struct { - attestationApi string + attestationApi string + sourceChainId uint64 + SourceUSDCToken common.Address } type USDCAttestationResponse struct { @@ -16,9 +22,21 @@ type USDCAttestationResponse struct { Attestation string `json:"attestation"` } +// Hard coded mapping of chain id to USDC token addresses +// Will be removed in favour of more flexible solution. +var USDCTokenMapping = map[uint64]common.Address{ + 420: common.HexToAddress("0xe05606174bac4A6364B31bd0eCA4bf4dD368f8C6"), + 43113: common.HexToAddress("0x5425890298aed601595a70ab815c96711a31bc65"), + 80001: common.HexToAddress("0x9999f7fea5938fd3b1e26a12c3f2fb024e194f97"), + 84531: common.HexToAddress("0xf175520c52418dfe19c8098071a252da48cd1c19"), + 421613: common.HexToAddress("0xfd064A18f3BF249cf1f87FC203E90D8f650f2d63"), +} + const ( - version = "v1" - attestationPath = "attestations" + version = "v1" + attestationPath = "attestations" + eventSignature = "8c5261668696ce22758910d05bab8f186d6eb247ceac2af2e82c7dc17669b036" + USDC_MESSAGE_SENT = "USDC message sent" ) type USDCAttestationStatus string @@ -28,8 +46,8 @@ const ( USDCAttestationStatusPending USDCAttestationStatus = "pending_confirmations" ) -func NewUSDCService(usdcAttestationApi string) *USDCService { - return &USDCService{attestationApi: usdcAttestationApi} +func NewUSDCService(usdcAttestationApi string, sourceChainId uint64) *USDCService { + return &USDCService{attestationApi: usdcAttestationApi, sourceChainId: sourceChainId, SourceUSDCToken: USDCTokenMapping[sourceChainId]} } func (usdc *USDCService) TryGetAttestation(messageHash string) (USDCAttestationResponse, error) { @@ -68,3 +86,13 @@ func (usdc *USDCService) IsAttestationComplete(messageHash string) (bool, string } return false, "", nil } + +func GetUSDCServiceSourceLPFilters(usdcTokenAddress common.Address) []logpoller.Filter { + return []logpoller.Filter{ + { + Name: logpoller.FilterName(USDC_MESSAGE_SENT, usdcTokenAddress.Hex()), + EventSigs: []common.Hash{common.HexToHash(eventSignature)}, + Addresses: []common.Address{usdcTokenAddress}, + }, + } +} diff --git a/core/services/ocr2/plugins/ccip/customtokens/usdc_test.go b/core/services/ocr2/plugins/ccip/customtokens/usdc_test.go index 3b9c0c5e41..e2678d506e 100644 --- a/core/services/ocr2/plugins/ccip/customtokens/usdc_test.go +++ b/core/services/ocr2/plugins/ccip/customtokens/usdc_test.go @@ -6,13 +6,17 @@ import ( "net/http/httptest" "testing" + "github.com/ethereum/go-ethereum/common" "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller" + "github.com/smartcontractkit/chainlink/v2/core/utils" ) func TestUSDCService_TryGetAttestation(t *testing.T) { t.Skipf("Skipping test because it uses the real USDC attestation API") usdcMessageHash := "0x912f22a13e9ccb979b621500f6952b2afd6e75be7eadaed93fc2625fe11c52a2" - usdcService := NewUSDCService("https://iris-api-sandbox.circle.com") + usdcService := NewUSDCService("https://iris-api-sandbox.circle.com", 420) attestation, err := usdcService.TryGetAttestation(usdcMessageHash) require.NoError(t, err) @@ -30,7 +34,7 @@ func TestUSDCService_TryGetAttestationMock(t *testing.T) { ts := getMockUSDCEndpoint(t, response) defer ts.Close() - usdcService := NewUSDCService(ts.URL) + usdcService := NewUSDCService(ts.URL, 420) attestation, err := usdcService.TryGetAttestation("0x912f22a13e9ccb979b621500f6952b2afd6e75be7eadaed93fc2625fe11c52a2") require.NoError(t, err) @@ -44,7 +48,7 @@ func TestUSDCService_TryGetAttestationMockError(t *testing.T) { })) defer ts.Close() - usdcService := NewUSDCService(ts.URL) + usdcService := NewUSDCService(ts.URL, 420) _, err := usdcService.TryGetAttestation("0x912f22a13e9ccb979b621500f6952b2afd6e75be7eadaed93fc2625fe11c52a2") require.Error(t, err) } @@ -58,7 +62,7 @@ func TestUSDCService_IsAttestationComplete(t *testing.T) { ts := getMockUSDCEndpoint(t, response) defer ts.Close() - usdcService := NewUSDCService(ts.URL) + usdcService := NewUSDCService(ts.URL, 420) isReady, attestation, err := usdcService.IsAttestationComplete("0x912f22a13e9ccb979b621500f6952b2afd6e75be7eadaed93fc2625fe11c52a2") require.NoError(t, err) @@ -75,3 +79,17 @@ func getMockUSDCEndpoint(t *testing.T, response USDCAttestationResponse) *httpte require.NoError(t, err) })) } + +// Asserts the hard coded event signature matches Keccak256("MessageSent(bytes)") +func TestGetUSDCServiceSourceLPFilters(t *testing.T) { + usdcTokenAddress := common.HexToAddress("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48") + filters := GetUSDCServiceSourceLPFilters(usdcTokenAddress) + + require.Equal(t, 1, len(filters)) + filter := filters[0] + require.Equal(t, logpoller.FilterName(USDC_MESSAGE_SENT, usdcTokenAddress.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, usdcTokenAddress, filter.Addresses[0]) +} diff --git a/core/services/ocr2/plugins/ccip/execution_plugin.go b/core/services/ocr2/plugins/ccip/execution_plugin.go index 6a4506b45c..6ed8171625 100644 --- a/core/services/ocr2/plugins/ccip/execution_plugin.go +++ b/core/services/ocr2/plugins/ccip/execution_plugin.go @@ -123,7 +123,7 @@ func NewExecutionServices(lggr logger.Logger, jb job.Job, chainSet evm.LegacyCha sourceClient: sourceChain.Client(), destGasEstimator: destChain.GasEstimator(), leafHasher: hasher.NewLeafHasher(offRampConfig.SourceChainSelector, offRampConfig.ChainSelector, onRamp.Address(), hasher.NewKeccakCtx()), - usdcService: customtokens.NewUSDCService(pluginConfig.USDCAttestationApi), + usdcService: customtokens.NewUSDCService(pluginConfig.USDCAttestationApi, chainId), }) err = wrappedPluginFactory.UpdateLogPollerFilters(zeroAddress) @@ -159,8 +159,13 @@ func NewExecutionServices(lggr logger.Logger, jb job.Job, chainSet evm.LegacyCha return []job.ServiceCtx{job.NewServiceAdapter(oracle)}, nil } -func getExecutionPluginSourceLpChainFilters(onRamp, priceRegistry common.Address) []logpoller.Filter { - return []logpoller.Filter{ +func getExecutionPluginSourceLpChainFilters(onRamp, priceRegistry, usdcToken common.Address) []logpoller.Filter { + var filters []logpoller.Filter + if usdcToken != common.HexToAddress("") { + filters = customtokens.GetUSDCServiceSourceLPFilters(usdcToken) + } + + return append(filters, []logpoller.Filter{ { Name: logpoller.FilterName(EXEC_CCIP_SENDS, onRamp.String()), EventSigs: []common.Hash{abihelpers.EventSignatures.SendRequested}, @@ -176,7 +181,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 { @@ -284,10 +289,15 @@ func unregisterExecutionPluginLpFilters( return err } + chainId, err := chainselectors.ChainIdFromSelector(destOffRampConfig.SourceChainSelector) + if err != nil { + return err + } + if err := unregisterLpFilters( q, sourceLP, - getExecutionPluginSourceLpChainFilters(destOffRampConfig.OnRamp, onRampDynCfg.PriceRegistry), + getExecutionPluginSourceLpChainFilters(destOffRampConfig.OnRamp, onRampDynCfg.PriceRegistry, customtokens.USDCTokenMapping[chainId]), ); err != nil { return err } diff --git a/core/services/ocr2/plugins/ccip/execution_plugin_test.go b/core/services/ocr2/plugins/ccip/execution_plugin_test.go index 85728682b6..9e1f49c640 100644 --- a/core/services/ocr2/plugins/ccip/execution_plugin_test.go +++ b/core/services/ocr2/plugins/ccip/execution_plugin_test.go @@ -116,8 +116,10 @@ func TestGetExecutionPluginFilterNames(t *testing.T) { dstLP, mockOffRamp, evm_2_evm_offramp.EVM2EVMOffRampStaticConfig{ - CommitStore: commitStoreAddr, - OnRamp: onRampAddr, + CommitStore: commitStoreAddr, + OnRamp: onRampAddr, + ChainSelector: 5790810961207155433, + SourceChainSelector: 16015286601757825753, }, mockOnRamp, nil, diff --git a/core/services/ocr2/plugins/ccip/execution_reporting_plugin.go b/core/services/ocr2/plugins/ccip/execution_reporting_plugin.go index fbcd518351..b1be55f36d 100644 --- a/core/services/ocr2/plugins/ccip/execution_reporting_plugin.go +++ b/core/services/ocr2/plugins/ccip/execution_reporting_plugin.go @@ -202,7 +202,11 @@ func (rf *ExecutionReportingPluginFactory) UpdateLogPollerFilters(destPriceRegis defer rf.filtersMu.Unlock() // source chain filters - sourceFiltersBefore, sourceFiltersNow := rf.sourceChainFilters, getExecutionPluginSourceLpChainFilters(rf.config.onRamp.Address(), rf.config.sourcePriceRegistry.Address()) + sourceFiltersBefore, sourceFiltersNow := rf.sourceChainFilters, getExecutionPluginSourceLpChainFilters( + rf.config.onRamp.Address(), + rf.config.sourcePriceRegistry.Address(), + rf.config.usdcService.SourceUSDCToken, + ) created, deleted := filtersDiff(sourceFiltersBefore, sourceFiltersNow) if err := unregisterLpFilters(nilQueryer, rf.config.sourceLP, deleted); err != nil { return err 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 2c2d8c7a3d..ab8a6f2aaf 100644 --- a/core/services/ocr2/plugins/ccip/execution_reporting_plugin_test.go +++ b/core/services/ocr2/plugins/ccip/execution_reporting_plugin_test.go @@ -30,6 +30,7 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/cache" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/ccipevents" ccipconfig "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/config" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/customtokens" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/testhelpers" plugintesthelpers "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/testhelpers/plugins" "github.com/smartcontractkit/chainlink/v2/core/utils" @@ -1142,6 +1143,9 @@ func TestExecutionReportingPluginFactory_UpdateLogPollerFilters(t *testing.T) { offRamp := mock_contracts.NewEVM2EVMOffRampInterface(t) offRamp.On("Address").Return(utils.RandomAddress(), nil) + sourceChainId := uint64(420) + sourceUSDCTokenAddress := customtokens.USDCTokenMapping[sourceChainId] + destPriceRegistryAddr := utils.RandomAddress() rf := &ExecutionReportingPluginFactory{ @@ -1155,10 +1159,11 @@ func TestExecutionReportingPluginFactory_UpdateLogPollerFilters(t *testing.T) { commitStore: commitStore, offRamp: offRamp, sourcePriceRegistry: sourcePriceRegistry, + usdcService: customtokens.NewUSDCService("", sourceChainId), }, } - for _, f := range getExecutionPluginSourceLpChainFilters(onRamp.Address(), sourcePriceRegistry.Address()) { + for _, f := range getExecutionPluginSourceLpChainFilters(onRamp.Address(), sourcePriceRegistry.Address(), sourceUSDCTokenAddress) { sourceLP.On("RegisterFilter", f).Return(nil) } for _, f := range getExecutionPluginDestLpChainFilters(commitStore.Address(), offRamp.Address(), destPriceRegistryAddr) { From 546871371c5ef267a0c90a63a206f8b02f6270f6 Mon Sep 17 00:00:00 2001 From: Rens Rooimans Date: Tue, 5 Sep 2023 13:39:46 +0200 Subject: [PATCH 03/20] check usdc messges attestation in batch building (mock) --- .../ccip/execution_reporting_plugin.go | 32 +++++++++++++++---- .../ccip/execution_reporting_plugin_test.go | 1 + 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/core/services/ocr2/plugins/ccip/execution_reporting_plugin.go b/core/services/ocr2/plugins/ccip/execution_reporting_plugin.go index b1be55f36d..7e4e866d82 100644 --- a/core/services/ocr2/plugins/ccip/execution_reporting_plugin.go +++ b/core/services/ocr2/plugins/ccip/execution_reporting_plugin.go @@ -38,6 +38,7 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/hasher" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/observability" "github.com/smartcontractkit/chainlink/v2/core/services/pg" + "github.com/smartcontractkit/chainlink/v2/core/utils" ) const ( @@ -525,11 +526,35 @@ func (r *ExecutionReportingPlugin) buildBatch( msgLggr.Errorw("Skipping message unable to compute aggregate value", "err", err) continue } + // if token limit is smaller than message value skip message if tokensLeft, hasCapacity := hasEnoughTokens(aggregateTokenLimit, msgValue, inflightAggregateValue); !hasCapacity { msgLggr.Warnw("token limit is smaller than message value", "aggregateTokenLimit", tokensLeft.String(), "msgValue", msgValue.String()) continue } + + // Any tokens that require offchain token data should be added here. + var tokenData [][]byte + for _, token := range msg.TokenAmounts { + switch token.Token { + case r.config.usdcService.SourceUSDCToken: + usdcMessageBody := "" + msgHash := utils.Keccak256Fixed([]byte(usdcMessageBody)) + success, attestation, err := r.config.usdcService.IsAttestationComplete(string(msgHash[:])) + if err != nil { + msgLggr.Errorw("Skipping message unable to check USDC attestation", "err", err) + continue + } + if !success { + msgLggr.Warnw("Skipping message USDC attestation not ready") + continue + } + tokenData = append(tokenData, []byte(attestation)) + default: + tokenData = append(tokenData, []byte{}) + } + } + // Fee boosting execGasPriceEstimateValue, err := execGasPriceEstimate() if err != nil { @@ -599,13 +624,6 @@ func (r *ExecutionReportingPlugin) buildBatch( } } - var tokenData [][]byte - - // TODO add attestation data for USDC here - for range msg.TokenAmounts { - tokenData = append(tokenData, []byte{}) - } - msgLggr.Infow("Adding msg to batch", "seqNum", msg.SequenceNumber, "nonce", msg.Nonce, "value", msgValue, "aggregateTokenLimit", aggregateTokenLimit) executableMessages = append(executableMessages, NewObservedMessage(msg.SequenceNumber, tokenData)) 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 ab8a6f2aaf..10f6605318 100644 --- a/core/services/ocr2/plugins/ccip/execution_reporting_plugin_test.go +++ b/core/services/ocr2/plugins/ccip/execution_reporting_plugin_test.go @@ -93,6 +93,7 @@ func setupExecTestHarness(t *testing.T) execTestHarness { sourceWrappedNativeToken: th.Source.WrappedNative.Address(), leafHasher: hasher.NewLeafHasher(th.Source.ChainSelector, th.Dest.ChainSelector, th.Source.OnRamp.Address(), hasher.NewKeccakCtx()), destGasEstimator: destFeeEstimator, + usdcService: customtokens.NewUSDCService("", th.Source.ChainSelector), }, onchainConfig: th.ExecOnchainConfig, offchainConfig: offchainConfig, From 1a7b5a9b243a1a9403046485cb9bd2dffd0a6899 Mon Sep 17 00:00:00 2001 From: Rens Rooimans Date: Wed, 6 Sep 2023 15:46:09 +0200 Subject: [PATCH 04/20] add logpoller IndexedLogsByTxHash --- core/chains/evm/logpoller/disabled.go | 4 ++ core/chains/evm/logpoller/log_poller.go | 5 +++ core/chains/evm/logpoller/mocks/log_poller.go | 33 ++++++++++++++ core/chains/evm/logpoller/observability.go | 4 ++ core/chains/evm/logpoller/orm.go | 16 +++++++ .../ocr2/plugins/ccip/ccipevents/logpoller.go | 20 +++++++++ .../plugins/ccip/ccipevents/logpoller_test.go | 45 +++++++++++++++++++ .../ocr2/plugins/ccip/customtokens/usdc.go | 14 +++--- .../plugins/ccip/customtokens/usdc_test.go | 2 +- .../ccip/execution_reporting_plugin.go | 6 +-- 10 files changed, 139 insertions(+), 10 deletions(-) diff --git a/core/chains/evm/logpoller/disabled.go b/core/chains/evm/logpoller/disabled.go index b48ac2fbfc..ac9d14b361 100644 --- a/core/chains/evm/logpoller/disabled.go +++ b/core/chains/evm/logpoller/disabled.go @@ -102,3 +102,7 @@ func (d disabled) IndexedLogsCreatedAfter(eventSig common.Hash, address common.A func (d disabled) LatestBlockByEventSigsAddrsWithConfs(fromBlock int64, eventSigs []common.Hash, addresses []common.Address, confs int, qopts ...pg.QOpt) (int64, error) { return 0, ErrDisabled } + +func (d disabled) IndexedLogsByTxHash(eventSig common.Hash, txHash common.Hash, qopts ...pg.QOpt) ([]Log, error) { + return nil, ErrDisabled +} diff --git a/core/chains/evm/logpoller/log_poller.go b/core/chains/evm/logpoller/log_poller.go index 2b1f7aebef..1a75688ba1 100644 --- a/core/chains/evm/logpoller/log_poller.go +++ b/core/chains/evm/logpoller/log_poller.go @@ -50,6 +50,7 @@ type LogPoller interface { // Content based querying IndexedLogs(eventSig common.Hash, address common.Address, topicIndex int, topicValues []common.Hash, confs int, qopts ...pg.QOpt) ([]Log, error) IndexedLogsByBlockRange(start, end int64, eventSig common.Hash, address common.Address, topicIndex int, topicValues []common.Hash, qopts ...pg.QOpt) ([]Log, error) + IndexedLogsByTxHash(eventSig common.Hash, txHash common.Hash, qopts ...pg.QOpt) ([]Log, error) IndexedLogsCreatedAfter(eventSig common.Hash, address common.Address, topicIndex int, topicValues []common.Hash, after time.Time, confs int, qopts ...pg.QOpt) ([]Log, error) IndexedLogsTopicGreaterThan(eventSig common.Hash, address common.Address, topicIndex int, topicValueMin common.Hash, confs int, qopts ...pg.QOpt) ([]Log, error) IndexedLogsTopicRange(eventSig common.Hash, address common.Address, topicIndex int, topicValueMin common.Hash, topicValueMax common.Hash, confs int, qopts ...pg.QOpt) ([]Log, error) @@ -1005,6 +1006,10 @@ func (lp *logPoller) LatestBlockByEventSigsAddrsWithConfs(fromBlock int64, event return lp.orm.SelectLatestBlockNumberEventSigsAddrsWithConfs(fromBlock, eventSigs, addresses, confs, qopts...) } +func (lp *logPoller) IndexedLogsByTxHash(eventSig common.Hash, txHash common.Hash, qopts ...pg.QOpt) ([]Log, error) { + return lp.orm.SelectIndexedLogsByTxHash(eventSig, txHash, qopts...) +} + // GetBlocksRange tries to get the specified block numbers from the log pollers // blocks table. It falls back to the RPC for any unfulfilled requested blocks. func (lp *logPoller) GetBlocksRange(ctx context.Context, numbers []uint64, qopts ...pg.QOpt) ([]LogPollerBlock, error) { diff --git a/core/chains/evm/logpoller/mocks/log_poller.go b/core/chains/evm/logpoller/mocks/log_poller.go index 7c76c448ab..e4c731f28c 100644 --- a/core/chains/evm/logpoller/mocks/log_poller.go +++ b/core/chains/evm/logpoller/mocks/log_poller.go @@ -150,6 +150,39 @@ func (_m *LogPoller) IndexedLogsByBlockRange(start int64, end int64, eventSig co return r0, r1 } +// IndexedLogsByTxHash provides a mock function with given fields: eventSig, txHash, qopts +func (_m *LogPoller) IndexedLogsByTxHash(eventSig common.Hash, txHash common.Hash, qopts ...pg.QOpt) ([]logpoller.Log, error) { + _va := make([]interface{}, len(qopts)) + for _i := range qopts { + _va[_i] = qopts[_i] + } + var _ca []interface{} + _ca = append(_ca, eventSig, txHash) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 []logpoller.Log + var r1 error + if rf, ok := ret.Get(0).(func(common.Hash, common.Hash, ...pg.QOpt) ([]logpoller.Log, error)); ok { + return rf(eventSig, txHash, qopts...) + } + if rf, ok := ret.Get(0).(func(common.Hash, common.Hash, ...pg.QOpt) []logpoller.Log); ok { + r0 = rf(eventSig, txHash, qopts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]logpoller.Log) + } + } + + if rf, ok := ret.Get(1).(func(common.Hash, common.Hash, ...pg.QOpt) error); ok { + r1 = rf(eventSig, txHash, qopts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // IndexedLogsCreatedAfter provides a mock function with given fields: eventSig, address, topicIndex, topicValues, after, confs, qopts func (_m *LogPoller) IndexedLogsCreatedAfter(eventSig common.Hash, address common.Address, topicIndex int, topicValues []common.Hash, after time.Time, confs int, qopts ...pg.QOpt) ([]logpoller.Log, error) { _va := make([]interface{}, len(qopts)) diff --git a/core/chains/evm/logpoller/observability.go b/core/chains/evm/logpoller/observability.go index f52d287859..270b35e64b 100644 --- a/core/chains/evm/logpoller/observability.go +++ b/core/chains/evm/logpoller/observability.go @@ -92,6 +92,10 @@ func (o *ObservedLogPoller) LatestBlockByEventSigsAddrsWithConfs(fromBlock int64 }) } +func (o *ObservedLogPoller) IndexedLogsByTxHash(eventSig common.Hash, txHash common.Hash, qopts ...pg.QOpt) ([]Log, error) { + return o.LogPoller.IndexedLogsByTxHash(eventSig, txHash, qopts...) +} + func (o *ObservedLogPoller) IndexedLogs(eventSig common.Hash, address common.Address, topicIndex int, topicValues []common.Hash, confs int, qopts ...pg.QOpt) ([]Log, error) { return withObservedQueryAndResults(o, "IndexedLogs", func() ([]Log, error) { return o.LogPoller.IndexedLogs(eventSig, address, topicIndex, topicValues, confs, qopts...) diff --git a/core/chains/evm/logpoller/orm.go b/core/chains/evm/logpoller/orm.go index b4a44ef4f0..94eba93833 100644 --- a/core/chains/evm/logpoller/orm.go +++ b/core/chains/evm/logpoller/orm.go @@ -482,6 +482,22 @@ func (o *ORM) SelectIndexedLogsByBlockRangeFilter(start, end int64, address comm return logs, nil } +func (o *ORM) SelectIndexedLogsByTxHash(eventSig common.Hash, txHash common.Hash, qopts ...pg.QOpt) ([]Log, error) { + q := o.q.WithOpts(qopts...) + var logs []Log + err := q.Select(&logs, ` + SELECT * FROM evm_logs + WHERE evm_logs.evm_chain_id = $1 + AND tx_hash = $3 + AND event_sig = $4 + ORDER BY (evm_logs.block_number, evm_logs.log_index)`, + utils.NewBig(o.chainID), txHash.Bytes(), eventSig.Bytes()) + if err != nil { + return nil, err + } + return logs, nil +} + func validateTopicIndex(index int) error { // Only topicIndex 1 through 3 is valid. 0 is the event sig and only 4 total topics are allowed if !(index == 1 || index == 2 || index == 3) { diff --git a/core/services/ocr2/plugins/ccip/ccipevents/logpoller.go b/core/services/ocr2/plugins/ccip/ccipevents/logpoller.go index 2fe7411d01..ee256ca8f8 100644 --- a/core/services/ocr2/plugins/ccip/ccipevents/logpoller.go +++ b/core/services/ocr2/plugins/ccip/ccipevents/logpoller.go @@ -19,6 +19,7 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/price_registry" "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/customtokens" "github.com/smartcontractkit/chainlink/v2/core/services/pg" ) @@ -216,6 +217,25 @@ func (c *LogPollerClient) GetExecutionStateChangesBetweenSeqNums(ctx context.Con ) } +func (c *LogPollerClient) GetLastUSDCMessagePriorToLogIndexInTx(ctx context.Context, logIndex int64, txHash common.Hash) ([]byte, error) { + logs, err := c.lp.IndexedLogsByTxHash( + customtokens.USDC_MESSAGE_SENT, + txHash, + pg.WithParentCtx(ctx), + ) + if err != nil { + return nil, err + } + + for i := range logs { + current := logs[len(logs)-i-1] + if current.LogIndex < logIndex { + 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 *LogPollerClient) LatestBlock(ctx context.Context) (int64, error) { return c.lp.LatestBlock(pg.WithParentCtx(ctx)) } diff --git a/core/services/ocr2/plugins/ccip/ccipevents/logpoller_test.go b/core/services/ocr2/plugins/ccip/ccipevents/logpoller_test.go index 600de7634b..a9ce802b0c 100644 --- a/core/services/ocr2/plugins/ccip/ccipevents/logpoller_test.go +++ b/core/services/ocr2/plugins/ccip/ccipevents/logpoller_test.go @@ -12,6 +12,7 @@ import ( "github.com/stretchr/testify/mock" evmClientMocks "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client/mocks" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/customtokens" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller/mocks" @@ -134,3 +135,47 @@ func TestLogPollerClient_GetSendRequestsGteSeqNum(t *testing.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", + customtokens.USDC_MESSAGE_SENT, + 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 := &LogPollerClient{lp: lp} + 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", + customtokens.USDC_MESSAGE_SENT, + txHash, + mock.Anything, + ).Return([]logpoller.Log{}, nil) + + c := &LogPollerClient{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/customtokens/usdc.go b/core/services/ocr2/plugins/ccip/customtokens/usdc.go index f8c0a13f17..fc1f03f90d 100644 --- a/core/services/ocr2/plugins/ccip/customtokens/usdc.go +++ b/core/services/ocr2/plugins/ccip/customtokens/usdc.go @@ -33,12 +33,14 @@ var USDCTokenMapping = map[uint64]common.Address{ } const ( - version = "v1" - attestationPath = "attestations" - eventSignature = "8c5261668696ce22758910d05bab8f186d6eb247ceac2af2e82c7dc17669b036" - USDC_MESSAGE_SENT = "USDC message sent" + version = "v1" + attestationPath = "attestations" + eventSignatureString = "8c5261668696ce22758910d05bab8f186d6eb247ceac2af2e82c7dc17669b036" + USDC_MESSAGE_SENT_FILTER_NAME = "USDC message sent" ) +var USDC_MESSAGE_SENT = common.HexToHash(eventSignatureString) + type USDCAttestationStatus string const ( @@ -90,8 +92,8 @@ func (usdc *USDCService) IsAttestationComplete(messageHash string) (bool, string func GetUSDCServiceSourceLPFilters(usdcTokenAddress common.Address) []logpoller.Filter { return []logpoller.Filter{ { - Name: logpoller.FilterName(USDC_MESSAGE_SENT, usdcTokenAddress.Hex()), - EventSigs: []common.Hash{common.HexToHash(eventSignature)}, + Name: logpoller.FilterName(USDC_MESSAGE_SENT_FILTER_NAME, usdcTokenAddress.Hex()), + EventSigs: []common.Hash{common.HexToHash(eventSignatureString)}, Addresses: []common.Address{usdcTokenAddress}, }, } diff --git a/core/services/ocr2/plugins/ccip/customtokens/usdc_test.go b/core/services/ocr2/plugins/ccip/customtokens/usdc_test.go index e2678d506e..513522a5cc 100644 --- a/core/services/ocr2/plugins/ccip/customtokens/usdc_test.go +++ b/core/services/ocr2/plugins/ccip/customtokens/usdc_test.go @@ -87,7 +87,7 @@ func TestGetUSDCServiceSourceLPFilters(t *testing.T) { require.Equal(t, 1, len(filters)) filter := filters[0] - require.Equal(t, logpoller.FilterName(USDC_MESSAGE_SENT, usdcTokenAddress.Hex()), filter.Name) + require.Equal(t, logpoller.FilterName(USDC_MESSAGE_SENT_FILTER_NAME, usdcTokenAddress.Hex()), filter.Name) hash, err := utils.Keccak256([]byte("MessageSent(bytes)")) require.NoError(t, err) require.Equal(t, hash, filter.EventSigs[0].Bytes()) diff --git a/core/services/ocr2/plugins/ccip/execution_reporting_plugin.go b/core/services/ocr2/plugins/ccip/execution_reporting_plugin.go index 7e4e866d82..9309ae9373 100644 --- a/core/services/ocr2/plugins/ccip/execution_reporting_plugin.go +++ b/core/services/ocr2/plugins/ccip/execution_reporting_plugin.go @@ -540,9 +540,9 @@ func (r *ExecutionReportingPlugin) buildBatch( case r.config.usdcService.SourceUSDCToken: usdcMessageBody := "" msgHash := utils.Keccak256Fixed([]byte(usdcMessageBody)) - success, attestation, err := r.config.usdcService.IsAttestationComplete(string(msgHash[:])) - if err != nil { - msgLggr.Errorw("Skipping message unable to check USDC attestation", "err", err) + success, attestation, err2 := r.config.usdcService.IsAttestationComplete(string(msgHash[:])) + if err2 != nil { + msgLggr.Errorw("Skipping message unable to check USDC attestation", "err", err2) continue } if !success { From 1577e567ddfdaab86762938d77cc0684ad87efa6 Mon Sep 17 00:00:00 2001 From: Rens Rooimans Date: Wed, 6 Sep 2023 18:46:02 +0200 Subject: [PATCH 05/20] refactor USDC service to be self contained --- .../plugins/ccip/abihelpers/abi_helpers.go | 4 + .../ocr2/plugins/ccip/ccipevents/client.go | 3 + .../ocr2/plugins/ccip/ccipevents/logpoller.go | 3 +- .../plugins/ccip/ccipevents/logpoller_test.go | 6 +- .../ocr2/plugins/ccip/customtokens/usdc.go | 154 ++++++++++++++---- .../plugins/ccip/customtokens/usdc_test.go | 92 ++++++----- .../ocr2/plugins/ccip/execution_plugin.go | 6 +- .../ccip/execution_reporting_plugin.go | 18 +- .../ccip/execution_reporting_plugin_test.go | 7 +- 9 files changed, 200 insertions(+), 93 deletions(-) diff --git a/core/services/ocr2/plugins/ccip/abihelpers/abi_helpers.go b/core/services/ocr2/plugins/ccip/abihelpers/abi_helpers.go index 52b8d983f5..1ed681f271 100644 --- a/core/services/ocr2/plugins/ccip/abihelpers/abi_helpers.go +++ b/core/services/ocr2/plugins/ccip/abihelpers/abi_helpers.go @@ -45,6 +45,8 @@ var EventSignatures struct { FeeTokenAdded common.Hash FeeTokenRemoved common.Hash + USDCMessageSent common.Hash + // offset || sourceChainID || seqNum || ... SendRequestedSequenceNumberWord int // offset || priceUpdatesOffset || minSeqNum || maxSeqNum || merkleRoot @@ -128,6 +130,8 @@ 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) { diff --git a/core/services/ocr2/plugins/ccip/ccipevents/client.go b/core/services/ocr2/plugins/ccip/ccipevents/client.go index d3419b744a..62ffd4fe0d 100644 --- a/core/services/ocr2/plugins/ccip/ccipevents/client.go +++ b/core/services/ocr2/plugins/ccip/ccipevents/client.go @@ -39,6 +39,9 @@ type Client interface { // GetExecutionStateChangesBetweenSeqNums returns all the execution state change events for the provided message sequence numbers (inclusive). GetExecutionStateChangesBetweenSeqNums(ctx context.Context, offRamp common.Address, seqNumMin, seqNumMax uint64, confs int) ([]Event[evm_2_evm_offramp.EVM2EVMOffRampExecutionStateChanged], 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/ccipevents/logpoller.go b/core/services/ocr2/plugins/ccip/ccipevents/logpoller.go index ee256ca8f8..8273da7928 100644 --- a/core/services/ocr2/plugins/ccip/ccipevents/logpoller.go +++ b/core/services/ocr2/plugins/ccip/ccipevents/logpoller.go @@ -19,7 +19,6 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/price_registry" "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/customtokens" "github.com/smartcontractkit/chainlink/v2/core/services/pg" ) @@ -219,7 +218,7 @@ func (c *LogPollerClient) GetExecutionStateChangesBetweenSeqNums(ctx context.Con func (c *LogPollerClient) GetLastUSDCMessagePriorToLogIndexInTx(ctx context.Context, logIndex int64, txHash common.Hash) ([]byte, error) { logs, err := c.lp.IndexedLogsByTxHash( - customtokens.USDC_MESSAGE_SENT, + abihelpers.EventSignatures.USDCMessageSent, txHash, pg.WithParentCtx(ctx), ) diff --git a/core/services/ocr2/plugins/ccip/ccipevents/logpoller_test.go b/core/services/ocr2/plugins/ccip/ccipevents/logpoller_test.go index a9ce802b0c..771201297b 100644 --- a/core/services/ocr2/plugins/ccip/ccipevents/logpoller_test.go +++ b/core/services/ocr2/plugins/ccip/ccipevents/logpoller_test.go @@ -12,8 +12,6 @@ import ( "github.com/stretchr/testify/mock" evmClientMocks "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client/mocks" - "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/customtokens" - "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" @@ -145,7 +143,7 @@ func TestLogPollerClient_GetLastUSDCMessagePriorToLogIndexInTx(t *testing.T) { t.Run("multiple found", func(t *testing.T) { lp := mocks.NewLogPoller(t) lp.On("IndexedLogsByTxHash", - customtokens.USDC_MESSAGE_SENT, + abihelpers.EventSignatures.USDCMessageSent, txHash, mock.Anything, ).Return([]logpoller.Log{ @@ -166,7 +164,7 @@ func TestLogPollerClient_GetLastUSDCMessagePriorToLogIndexInTx(t *testing.T) { t.Run("none found", func(t *testing.T) { lp := mocks.NewLogPoller(t) lp.On("IndexedLogsByTxHash", - customtokens.USDC_MESSAGE_SENT, + abihelpers.EventSignatures.USDCMessageSent, txHash, mock.Anything, ).Return([]logpoller.Log{}, nil) diff --git a/core/services/ocr2/plugins/ccip/customtokens/usdc.go b/core/services/ocr2/plugins/ccip/customtokens/usdc.go index fc1f03f90d..e968da15af 100644 --- a/core/services/ocr2/plugins/ccip/customtokens/usdc.go +++ b/core/services/ocr2/plugins/ccip/customtokens/usdc.go @@ -1,6 +1,8 @@ package customtokens import ( + "context" + "encoding/hex" "encoding/json" "fmt" "io" @@ -9,17 +11,32 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/ccipevents" + "github.com/smartcontractkit/chainlink/v2/core/utils" ) type USDCService struct { - attestationApi string - sourceChainId uint64 - SourceUSDCToken common.Address + sourceChainEvents ccipevents.Client + attestationApi string + sourceChainId uint64 + SourceUSDCToken common.Address + OnRampAddress common.Address + + // Cache of sequence number -> attestation attempt + attestationCache map[uint64]USDCAttestationAttempt } -type USDCAttestationResponse struct { - Status USDCAttestationStatus `json:"status"` - Attestation string `json:"attestation"` +type USDCAttestationAttempt struct { + USDCMessageBody []byte + USDCMessageHash [32]byte + CCIPSendTxHash common.Hash + CCIPSendLogIndex int64 + USDCAttestationResponse AttestationResponse +} + +type AttestationResponse struct { + Status AttestationStatus `json:"status"` + Attestation string `json:"attestation"` } // Hard coded mapping of chain id to USDC token addresses @@ -39,54 +56,125 @@ const ( USDC_MESSAGE_SENT_FILTER_NAME = "USDC message sent" ) -var USDC_MESSAGE_SENT = common.HexToHash(eventSignatureString) - -type USDCAttestationStatus string +type AttestationStatus string const ( - USDCAttestationStatusSuccess USDCAttestationStatus = "complete" - USDCAttestationStatusPending USDCAttestationStatus = "pending_confirmations" + USDCAttestationStatusSuccess AttestationStatus = "complete" + USDCAttestationStatusPending AttestationStatus = "pending_confirmations" + USDCAttestationStatusUnchecked AttestationStatus = "unchecked" ) -func NewUSDCService(usdcAttestationApi string, sourceChainId uint64) *USDCService { - return &USDCService{attestationApi: usdcAttestationApi, sourceChainId: sourceChainId, SourceUSDCToken: USDCTokenMapping[sourceChainId]} +func NewUSDCService(sourceChainEvents ccipevents.Client, onRampAddress common.Address, usdcAttestationApi string, sourceChainId uint64) *USDCService { + return &USDCService{ + sourceChainEvents: sourceChainEvents, + attestationApi: usdcAttestationApi, + sourceChainId: sourceChainId, + SourceUSDCToken: USDCTokenMapping[sourceChainId], + OnRampAddress: onRampAddress, + attestationCache: make(map[uint64]USDCAttestationAttempt), + } } -func (usdc *USDCService) TryGetAttestation(messageHash string) (USDCAttestationResponse, error) { - fullAttestationUrl := fmt.Sprintf("%s/%s/%s/%s", usdc.attestationApi, version, attestationPath, messageHash) - req, err := http.NewRequest("GET", fullAttestationUrl, nil) +func (usdc *USDCService) IsAttestationComplete(ctx context.Context, seqNum uint64) (bool, string, error) { + response, err := usdc.GetUpdatedAttestation(ctx, seqNum) if err != nil { - return USDCAttestationResponse{}, err + return false, "", err } - req.Header.Add("accept", "application/json") - res, err := http.DefaultClient.Do(req) + + if response.Status == USDCAttestationStatusSuccess { + return true, response.Attestation, nil + } + return false, "", nil +} + +func (usdc *USDCService) GetUpdatedAttestation(ctx context.Context, seqNum uint64) (AttestationResponse, error) { + // Try to get information from cache to reduce the number of database and external calls + attestationAttempt, ok := usdc.attestationCache[seqNum] + if ok && attestationAttempt.USDCAttestationResponse.Status == USDCAttestationStatusSuccess { + // If successful, return the cached response + return attestationAttempt.USDCAttestationResponse, nil + } + + // If no attempt for this message id exists, get the required data to make one + if !ok { + var err error + attestationAttempt, err = usdc.getAttemptInfoFromCCIPMessageId(ctx, seqNum) + if err != nil { + return AttestationResponse{}, err + } + // Save the attempt in the cache in case the external call fails + usdc.attestationCache[seqNum] = attestationAttempt + } + + response, err := usdc.callAttestationApi(ctx, attestationAttempt.USDCMessageHash) if err != nil { - return USDCAttestationResponse{}, err + return AttestationResponse{}, err } - defer res.Body.Close() - body, err := io.ReadAll(res.Body) + + // Save the response in the cache + attestationAttempt.USDCAttestationResponse = response + usdc.attestationCache[seqNum] = attestationAttempt + + return response, nil +} + +func (usdc *USDCService) getAttemptInfoFromCCIPMessageId(ctx context.Context, seqNum uint64) (USDCAttestationAttempt, error) { + // Get the CCIP message send event from the log poller + ccipSendRequests, err := usdc.sourceChainEvents.GetSendRequestsBetweenSeqNums(ctx, usdc.OnRampAddress, seqNum, seqNum, 0) if err != nil { - return USDCAttestationResponse{}, err + return USDCAttestationAttempt{}, err } - var response USDCAttestationResponse - err = json.Unmarshal(body, &response) + if len(ccipSendRequests) != 1 { + return USDCAttestationAttempt{}, fmt.Errorf("expected 1 CCIP send request, got %d", len(ccipSendRequests)) + } + + ccipSendRequest := ccipSendRequests[0] + + ccipSendTxHash := ccipSendRequest.Data.Raw.TxHash + ccipSendLogIndex := int64(ccipSendRequest.Data.Raw.Index) + + // Get the USDC message body + usdcMessageBody, err := usdc.sourceChainEvents.GetLastUSDCMessagePriorToLogIndexInTx(ctx, ccipSendLogIndex, ccipSendTxHash) if err != nil { - return USDCAttestationResponse{}, err + return USDCAttestationAttempt{}, err } - return response, nil + return USDCAttestationAttempt{ + USDCMessageBody: usdcMessageBody, + USDCMessageHash: utils.Keccak256Fixed(usdcMessageBody), + CCIPSendTxHash: ccipSendTxHash, + CCIPSendLogIndex: ccipSendLogIndex, + USDCAttestationResponse: AttestationResponse{ + Status: USDCAttestationStatusUnchecked, + Attestation: "", + }, + }, nil } -func (usdc *USDCService) IsAttestationComplete(messageHash string) (bool, string, error) { - response, err := usdc.TryGetAttestation(messageHash) +func (usdc *USDCService) callAttestationApi(ctx context.Context, usdcMessageHash [32]byte) (AttestationResponse, error) { + fullAttestationUrl := fmt.Sprintf("%s/%s/%s/0x%s", usdc.attestationApi, version, attestationPath, hex.EncodeToString(usdcMessageHash[:])) + req, err := http.NewRequestWithContext(ctx, "GET", fullAttestationUrl, nil) if err != nil { - return false, "", err + return AttestationResponse{}, err } - if response.Status == USDCAttestationStatusSuccess { - return true, response.Attestation, nil + req.Header.Add("accept", "application/json") + res, err := http.DefaultClient.Do(req) + if err != nil { + return AttestationResponse{}, err } - return false, "", nil + defer res.Body.Close() + body, err := io.ReadAll(res.Body) + if err != nil { + return AttestationResponse{}, err + } + + var response AttestationResponse + err = json.Unmarshal(body, &response) + if err != nil { + return AttestationResponse{}, err + } + return response, nil } func GetUSDCServiceSourceLPFilters(usdcTokenAddress common.Address) []logpoller.Filter { diff --git a/core/services/ocr2/plugins/ccip/customtokens/usdc_test.go b/core/services/ocr2/plugins/ccip/customtokens/usdc_test.go index 513522a5cc..7b9c8f2050 100644 --- a/core/services/ocr2/plugins/ccip/customtokens/usdc_test.go +++ b/core/services/ocr2/plugins/ccip/customtokens/usdc_test.go @@ -1,6 +1,7 @@ package customtokens import ( + "context" "encoding/json" "net/http" "net/http/httptest" @@ -9,24 +10,27 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/stretchr/testify/require" - "github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller" "github.com/smartcontractkit/chainlink/v2/core/utils" ) -func TestUSDCService_TryGetAttestation(t *testing.T) { +var ( + mockOnRampAddress = utils.RandomAddress() +) + +func TestUSDCService_callAttestationApi(t *testing.T) { t.Skipf("Skipping test because it uses the real USDC attestation API") - usdcMessageHash := "0x912f22a13e9ccb979b621500f6952b2afd6e75be7eadaed93fc2625fe11c52a2" - usdcService := NewUSDCService("https://iris-api-sandbox.circle.com", 420) + usdcMessageHash := "912f22a13e9ccb979b621500f6952b2afd6e75be7eadaed93fc2625fe11c52a2" + usdcService := NewUSDCService(nil, mockOnRampAddress, "https://iris-api-sandbox.circle.com", 420) - attestation, err := usdcService.TryGetAttestation(usdcMessageHash) + attestation, err := usdcService.callAttestationApi(context.Background(), [32]byte(common.FromHex(usdcMessageHash))) require.NoError(t, err) require.Equal(t, USDCAttestationStatusPending, attestation.Status) require.Equal(t, "PENDING", attestation.Attestation) } -func TestUSDCService_TryGetAttestationMock(t *testing.T) { - response := USDCAttestationResponse{ +func TestUSDCService_callAttestationApiMock(t *testing.T) { + response := AttestationResponse{ Status: USDCAttestationStatusSuccess, Attestation: "720502893578a89a8a87982982ef781c18b193", } @@ -34,43 +38,45 @@ func TestUSDCService_TryGetAttestationMock(t *testing.T) { ts := getMockUSDCEndpoint(t, response) defer ts.Close() - usdcService := NewUSDCService(ts.URL, 420) - attestation, err := usdcService.TryGetAttestation("0x912f22a13e9ccb979b621500f6952b2afd6e75be7eadaed93fc2625fe11c52a2") + usdcService := NewUSDCService(nil, mockOnRampAddress, ts.URL, 420) + attestation, err := usdcService.callAttestationApi(context.Background(), utils.RandomBytes32()) require.NoError(t, err) require.Equal(t, response.Status, attestation.Status) require.Equal(t, response.Attestation, attestation.Attestation) } -func TestUSDCService_TryGetAttestationMockError(t *testing.T) { +func TestUSDCService_callAttestationApiMockError(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusInternalServerError) })) defer ts.Close() - usdcService := NewUSDCService(ts.URL, 420) - _, err := usdcService.TryGetAttestation("0x912f22a13e9ccb979b621500f6952b2afd6e75be7eadaed93fc2625fe11c52a2") + usdcService := NewUSDCService(nil, mockOnRampAddress, ts.URL, 420) + _, err := usdcService.callAttestationApi(context.Background(), utils.RandomBytes32()) require.Error(t, err) } -func TestUSDCService_IsAttestationComplete(t *testing.T) { - response := USDCAttestationResponse{ - Status: USDCAttestationStatusSuccess, - Attestation: "720502893578a89a8a87982982ef781c18b193", - } - - ts := getMockUSDCEndpoint(t, response) - defer ts.Close() - - usdcService := NewUSDCService(ts.URL, 420) - isReady, attestation, err := usdcService.IsAttestationComplete("0x912f22a13e9ccb979b621500f6952b2afd6e75be7eadaed93fc2625fe11c52a2") - require.NoError(t, err) - - require.True(t, isReady) - require.Equal(t, response.Attestation, attestation) -} - -func getMockUSDCEndpoint(t *testing.T, response USDCAttestationResponse) *httptest.Server { +//func TestUSDCService_IsAttestationComplete(t *testing.T) { +// response := AttestationResponse{ +// Status: USDCAttestationStatusSuccess, +// Attestation: "720502893578a89a8a87982982ef781c18b193", +// } +// +// ts := getMockUSDCEndpoint(t, response) +// defer ts.Close() +// +// +// +// usdcService := NewUSDCService(nil, mockOnRampAddress, ts.URL, 420) +// isReady, attestation, err := usdcService.IsAttestationComplete(context.Background(), 245235) +// require.NoError(t, err) +// +// require.True(t, isReady) +// require.Equal(t, response.Attestation, attestation) +//} + +func getMockUSDCEndpoint(t *testing.T, response AttestationResponse) *httptest.Server { responseBytes, err := json.Marshal(response) require.NoError(t, err) @@ -80,16 +86,16 @@ func getMockUSDCEndpoint(t *testing.T, response USDCAttestationResponse) *httpte })) } -// Asserts the hard coded event signature matches Keccak256("MessageSent(bytes)") -func TestGetUSDCServiceSourceLPFilters(t *testing.T) { - usdcTokenAddress := common.HexToAddress("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48") - filters := GetUSDCServiceSourceLPFilters(usdcTokenAddress) - - require.Equal(t, 1, len(filters)) - filter := filters[0] - require.Equal(t, logpoller.FilterName(USDC_MESSAGE_SENT_FILTER_NAME, usdcTokenAddress.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, usdcTokenAddress, filter.Addresses[0]) -} +//// Asserts the hard coded event signature matches Keccak256("MessageSent(bytes)") +//func TestGetUSDCServiceSourceLPFilters(t *testing.T) { +// usdcTokenAddress := common.HexToAddress("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48") +// filters := GetUSDCServiceSourceLPFilters(usdcTokenAddress) +// +// require.Equal(t, 1, len(filters)) +// filter := filters[0] +// require.Equal(t, logpoller.FilterName(USDC_MESSAGE_SENT_FILTER_NAME, usdcTokenAddress.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, usdcTokenAddress, filter.Addresses[0]) +//} diff --git a/core/services/ocr2/plugins/ccip/execution_plugin.go b/core/services/ocr2/plugins/ccip/execution_plugin.go index 6ed8171625..0414dbf648 100644 --- a/core/services/ocr2/plugins/ccip/execution_plugin.go +++ b/core/services/ocr2/plugins/ccip/execution_plugin.go @@ -107,12 +107,14 @@ func NewExecutionServices(lggr logger.Logger, jb job.Job, chainSet evm.LegacyCha "sourceChain", ChainName(int64(chainId)), "destChain", ChainName(destChainID)) + sourceChainEventClient := ccipevents.NewLogPollerClient(sourceChain.LogPoller(), execLggr, sourceChain.Client()) + wrappedPluginFactory := NewExecutionReportingPluginFactory( ExecutionPluginConfig{ lggr: execLggr, sourceLP: sourceChain.LogPoller(), destLP: destChain.LogPoller(), - sourceEvents: ccipevents.NewLogPollerClient(sourceChain.LogPoller(), execLggr, sourceChain.Client()), + sourceEvents: sourceChainEventClient, destEvents: ccipevents.NewLogPollerClient(destChain.LogPoller(), execLggr, destChain.Client()), onRamp: onRamp, offRamp: offRamp, @@ -123,7 +125,7 @@ func NewExecutionServices(lggr logger.Logger, jb job.Job, chainSet evm.LegacyCha sourceClient: sourceChain.Client(), destGasEstimator: destChain.GasEstimator(), leafHasher: hasher.NewLeafHasher(offRampConfig.SourceChainSelector, offRampConfig.ChainSelector, onRamp.Address(), hasher.NewKeccakCtx()), - usdcService: customtokens.NewUSDCService(pluginConfig.USDCAttestationApi, chainId), + usdcService: customtokens.NewUSDCService(sourceChainEventClient, offRampConfig.OnRamp, pluginConfig.USDCAttestationApi, chainId), }) err = wrappedPluginFactory.UpdateLogPollerFilters(zeroAddress) diff --git a/core/services/ocr2/plugins/ccip/execution_reporting_plugin.go b/core/services/ocr2/plugins/ccip/execution_reporting_plugin.go index 9309ae9373..cb37f222cd 100644 --- a/core/services/ocr2/plugins/ccip/execution_reporting_plugin.go +++ b/core/services/ocr2/plugins/ccip/execution_reporting_plugin.go @@ -38,7 +38,6 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/hasher" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/observability" "github.com/smartcontractkit/chainlink/v2/core/services/pg" - "github.com/smartcontractkit/chainlink/v2/core/utils" ) const ( @@ -362,8 +361,16 @@ func (r *ExecutionReportingPlugin) getExecutableObservations(ctx context.Context } buildBatchDuration := time.Now() - batch := r.buildBatch(rootLggr, rep, inflight, allowedTokenAmountValue.Tokens, - sourceTokensPricesValue, destTokensPricesValue, getDestGasPrice, sourceToDestTokens, destPoolRateLimits) + batch := r.buildBatch( + rootLggr, + rep, + inflight, + allowedTokenAmountValue.Tokens, + sourceTokensPricesValue, + destTokensPricesValue, + getDestGasPrice, + sourceToDestTokens, + destPoolRateLimits) measureBatchBuildDuration(timestamp, time.Since(buildBatchDuration)) if len(batch) != 0 { return batch, nil @@ -538,9 +545,8 @@ func (r *ExecutionReportingPlugin) buildBatch( for _, token := range msg.TokenAmounts { switch token.Token { case r.config.usdcService.SourceUSDCToken: - usdcMessageBody := "" - msgHash := utils.Keccak256Fixed([]byte(usdcMessageBody)) - success, attestation, err2 := r.config.usdcService.IsAttestationComplete(string(msgHash[:])) + // TODO non-nil context + success, attestation, err2 := r.config.usdcService.IsAttestationComplete(context.TODO(), msg.SequenceNumber) if err2 != nil { msgLggr.Errorw("Skipping message unable to check USDC attestation", "err", err2) continue 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 10f6605318..d02a0713ab 100644 --- a/core/services/ocr2/plugins/ccip/execution_reporting_plugin_test.go +++ b/core/services/ocr2/plugins/ccip/execution_reporting_plugin_test.go @@ -77,12 +77,13 @@ func setupExecTestHarness(t *testing.T) execTestHarness { InflightCacheExpiry: models.MustMakeDuration(3 * time.Minute), RelativeBoostPerWaitHour: 0.07, } + sourceChainEventClient := ccipevents.NewLogPollerClient(th.SourceLP, lggr, th.SourceClient) plugin := ExecutionReportingPlugin{ config: ExecutionPluginConfig{ lggr: th.Lggr, sourceLP: th.SourceLP, destLP: th.DestLP, - sourceEvents: ccipevents.NewLogPollerClient(th.SourceLP, lggr, th.SourceClient), + sourceEvents: sourceChainEventClient, destEvents: ccipevents.NewLogPollerClient(th.DestLP, lggr, th.DestClient), sourcePriceRegistry: th.Source.PriceRegistry, onRamp: th.Source.OnRamp, @@ -93,7 +94,7 @@ func setupExecTestHarness(t *testing.T) execTestHarness { sourceWrappedNativeToken: th.Source.WrappedNative.Address(), leafHasher: hasher.NewLeafHasher(th.Source.ChainSelector, th.Dest.ChainSelector, th.Source.OnRamp.Address(), hasher.NewKeccakCtx()), destGasEstimator: destFeeEstimator, - usdcService: customtokens.NewUSDCService("", th.Source.ChainSelector), + usdcService: customtokens.NewUSDCService(sourceChainEventClient, th.Source.OnRamp.Address(), "", th.Source.ChainSelector), }, onchainConfig: th.ExecOnchainConfig, offchainConfig: offchainConfig, @@ -1160,7 +1161,7 @@ func TestExecutionReportingPluginFactory_UpdateLogPollerFilters(t *testing.T) { commitStore: commitStore, offRamp: offRamp, sourcePriceRegistry: sourcePriceRegistry, - usdcService: customtokens.NewUSDCService("", sourceChainId), + usdcService: customtokens.NewUSDCService(nil, onRamp.Address(), "", sourceChainId), }, } From 2ac587959d73a28ded65a9d10adc4b61dc096c19 Mon Sep 17 00:00:00 2001 From: Rens Rooimans Date: Thu, 7 Sep 2023 14:50:17 +0200 Subject: [PATCH 06/20] make offchain token data providers generic --- .../ocr2/plugins/ccip/ccipevents/client.go | 2 + .../plugins/ccip/ccipevents/client_mock.go | 218 ++++++++++++++++++ .../plugins/ccip/customtokens/usdc_test.go | 101 -------- .../ocr2/plugins/ccip/execution_plugin.go | 36 ++- .../plugins/ccip/execution_plugin_test.go | 3 +- .../ccip/execution_reporting_plugin.go | 23 +- .../ccip/execution_reporting_plugin_test.go | 13 +- .../offchain_data_provider.go | 23 ++ .../usdc}/usdc.go | 103 +++++---- .../ccip/offchaintokendata/usdc/usdc_test.go | 141 +++++++++++ 10 files changed, 489 insertions(+), 174 deletions(-) create mode 100644 core/services/ocr2/plugins/ccip/ccipevents/client_mock.go delete mode 100644 core/services/ocr2/plugins/ccip/customtokens/usdc_test.go create mode 100644 core/services/ocr2/plugins/ccip/offchaintokendata/offchain_data_provider.go rename core/services/ocr2/plugins/ccip/{customtokens => offchaintokendata/usdc}/usdc.go (54%) create mode 100644 core/services/ocr2/plugins/ccip/offchaintokendata/usdc/usdc_test.go diff --git a/core/services/ocr2/plugins/ccip/ccipevents/client.go b/core/services/ocr2/plugins/ccip/ccipevents/client.go index 62ffd4fe0d..6209f3350d 100644 --- a/core/services/ocr2/plugins/ccip/ccipevents/client.go +++ b/core/services/ocr2/plugins/ccip/ccipevents/client.go @@ -22,6 +22,8 @@ type BlockMeta struct { } // Client can be used to fetch CCIP related parsed on-chain events. +// +//go:generate mockery --quiet --name Client --output . --filename client_mock.go --inpackage --case=underscore type Client 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. diff --git a/core/services/ocr2/plugins/ccip/ccipevents/client_mock.go b/core/services/ocr2/plugins/ccip/ccipevents/client_mock.go new file mode 100644 index 0000000000..a6e6bd3ece --- /dev/null +++ b/core/services/ocr2/plugins/ccip/ccipevents/client_mock.go @@ -0,0 +1,218 @@ +// Code generated by mockery v2.28.1. DO NOT EDIT. + +package ccipevents + +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" + 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" + + time "time" +) + +// MockClient is an autogenerated mock type for the Client type +type MockClient struct { + mock.Mock +} + +// GetExecutionStateChangesBetweenSeqNums provides a mock function with given fields: ctx, offRamp, seqNumMin, seqNumMax, confs +func (_m *MockClient) GetExecutionStateChangesBetweenSeqNums(ctx context.Context, offRamp common.Address, seqNumMin uint64, seqNumMax uint64, confs int) ([]Event[evm_2_evm_offramp.EVM2EVMOffRampExecutionStateChanged], error) { + ret := _m.Called(ctx, offRamp, seqNumMin, seqNumMax, confs) + + var r0 []Event[evm_2_evm_offramp.EVM2EVMOffRampExecutionStateChanged] + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, common.Address, uint64, uint64, int) ([]Event[evm_2_evm_offramp.EVM2EVMOffRampExecutionStateChanged], error)); ok { + return rf(ctx, offRamp, seqNumMin, seqNumMax, confs) + } + if rf, ok := ret.Get(0).(func(context.Context, common.Address, uint64, uint64, int) []Event[evm_2_evm_offramp.EVM2EVMOffRampExecutionStateChanged]); ok { + r0 = rf(ctx, offRamp, seqNumMin, seqNumMax, confs) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]Event[evm_2_evm_offramp.EVM2EVMOffRampExecutionStateChanged]) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, common.Address, uint64, uint64, int) error); ok { + r1 = rf(ctx, offRamp, seqNumMin, seqNumMax, confs) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetGasPriceUpdatesCreatedAfter provides a mock function with given fields: ctx, priceRegistry, chainSelector, ts, confs +func (_m *MockClient) GetGasPriceUpdatesCreatedAfter(ctx context.Context, priceRegistry common.Address, chainSelector uint64, ts time.Time, confs int) ([]Event[price_registry.PriceRegistryUsdPerUnitGasUpdated], error) { + ret := _m.Called(ctx, priceRegistry, chainSelector, ts, confs) + + var r0 []Event[price_registry.PriceRegistryUsdPerUnitGasUpdated] + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, common.Address, uint64, time.Time, int) ([]Event[price_registry.PriceRegistryUsdPerUnitGasUpdated], error)); ok { + return rf(ctx, priceRegistry, chainSelector, ts, confs) + } + if rf, ok := ret.Get(0).(func(context.Context, common.Address, uint64, time.Time, int) []Event[price_registry.PriceRegistryUsdPerUnitGasUpdated]); ok { + r0 = rf(ctx, priceRegistry, chainSelector, ts, confs) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]Event[price_registry.PriceRegistryUsdPerUnitGasUpdated]) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, common.Address, uint64, time.Time, int) error); ok { + r1 = rf(ctx, priceRegistry, chainSelector, ts, confs) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetLastUSDCMessagePriorToLogIndexInTx provides a mock function with given fields: ctx, logIndex, txHash +func (_m *MockClient) 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 *MockClient) 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 *MockClient) 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 *MockClient) GetTokenPriceUpdatesCreatedAfter(ctx context.Context, priceRegistry common.Address, ts time.Time, confs int) ([]Event[price_registry.PriceRegistryUsdPerTokenUpdated], error) { + ret := _m.Called(ctx, priceRegistry, ts, confs) + + var r0 []Event[price_registry.PriceRegistryUsdPerTokenUpdated] + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, common.Address, time.Time, int) ([]Event[price_registry.PriceRegistryUsdPerTokenUpdated], error)); ok { + return rf(ctx, priceRegistry, ts, confs) + } + if rf, ok := ret.Get(0).(func(context.Context, common.Address, time.Time, int) []Event[price_registry.PriceRegistryUsdPerTokenUpdated]); ok { + r0 = rf(ctx, priceRegistry, ts, confs) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]Event[price_registry.PriceRegistryUsdPerTokenUpdated]) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, common.Address, time.Time, int) error); ok { + r1 = rf(ctx, priceRegistry, ts, confs) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// LatestBlock provides a mock function with given fields: ctx +func (_m *MockClient) LatestBlock(ctx context.Context) (int64, error) { + ret := _m.Called(ctx) + + var r0 int64 + var r1 error + if rf, ok := ret.Get(0).(func(context.Context) (int64, error)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(context.Context) int64); ok { + r0 = rf(ctx) + } else { + r0 = ret.Get(0).(int64) + } + + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +type mockConstructorTestingTNewMockClient interface { + mock.TestingT + Cleanup(func()) +} + +// NewMockClient creates a new instance of MockClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewMockClient(t mockConstructorTestingTNewMockClient) *MockClient { + mock := &MockClient{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/core/services/ocr2/plugins/ccip/customtokens/usdc_test.go b/core/services/ocr2/plugins/ccip/customtokens/usdc_test.go deleted file mode 100644 index 7b9c8f2050..0000000000 --- a/core/services/ocr2/plugins/ccip/customtokens/usdc_test.go +++ /dev/null @@ -1,101 +0,0 @@ -package customtokens - -import ( - "context" - "encoding/json" - "net/http" - "net/http/httptest" - "testing" - - "github.com/ethereum/go-ethereum/common" - "github.com/stretchr/testify/require" - - "github.com/smartcontractkit/chainlink/v2/core/utils" -) - -var ( - mockOnRampAddress = utils.RandomAddress() -) - -func TestUSDCService_callAttestationApi(t *testing.T) { - t.Skipf("Skipping test because it uses the real USDC attestation API") - usdcMessageHash := "912f22a13e9ccb979b621500f6952b2afd6e75be7eadaed93fc2625fe11c52a2" - usdcService := NewUSDCService(nil, mockOnRampAddress, "https://iris-api-sandbox.circle.com", 420) - - attestation, err := usdcService.callAttestationApi(context.Background(), [32]byte(common.FromHex(usdcMessageHash))) - require.NoError(t, err) - - require.Equal(t, USDCAttestationStatusPending, attestation.Status) - require.Equal(t, "PENDING", attestation.Attestation) -} - -func TestUSDCService_callAttestationApiMock(t *testing.T) { - response := AttestationResponse{ - Status: USDCAttestationStatusSuccess, - Attestation: "720502893578a89a8a87982982ef781c18b193", - } - - ts := getMockUSDCEndpoint(t, response) - defer ts.Close() - - usdcService := NewUSDCService(nil, mockOnRampAddress, ts.URL, 420) - attestation, err := usdcService.callAttestationApi(context.Background(), utils.RandomBytes32()) - require.NoError(t, err) - - require.Equal(t, response.Status, attestation.Status) - require.Equal(t, response.Attestation, attestation.Attestation) -} - -func TestUSDCService_callAttestationApiMockError(t *testing.T) { - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusInternalServerError) - })) - defer ts.Close() - - usdcService := NewUSDCService(nil, mockOnRampAddress, ts.URL, 420) - _, err := usdcService.callAttestationApi(context.Background(), utils.RandomBytes32()) - require.Error(t, err) -} - -//func TestUSDCService_IsAttestationComplete(t *testing.T) { -// response := AttestationResponse{ -// Status: USDCAttestationStatusSuccess, -// Attestation: "720502893578a89a8a87982982ef781c18b193", -// } -// -// ts := getMockUSDCEndpoint(t, response) -// defer ts.Close() -// -// -// -// usdcService := NewUSDCService(nil, mockOnRampAddress, ts.URL, 420) -// isReady, attestation, err := usdcService.IsAttestationComplete(context.Background(), 245235) -// require.NoError(t, err) -// -// require.True(t, isReady) -// require.Equal(t, response.Attestation, attestation) -//} - -func getMockUSDCEndpoint(t *testing.T, response AttestationResponse) *httptest.Server { - responseBytes, err := json.Marshal(response) - require.NoError(t, err) - - return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - _, err := w.Write(responseBytes) - require.NoError(t, err) - })) -} - -//// Asserts the hard coded event signature matches Keccak256("MessageSent(bytes)") -//func TestGetUSDCServiceSourceLPFilters(t *testing.T) { -// usdcTokenAddress := common.HexToAddress("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48") -// filters := GetUSDCServiceSourceLPFilters(usdcTokenAddress) -// -// require.Equal(t, 1, len(filters)) -// filter := filters[0] -// require.Equal(t, logpoller.FilterName(USDC_MESSAGE_SENT_FILTER_NAME, usdcTokenAddress.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, usdcTokenAddress, filter.Addresses[0]) -//} diff --git a/core/services/ocr2/plugins/ccip/execution_plugin.go b/core/services/ocr2/plugins/ccip/execution_plugin.go index 0414dbf648..a893f16dd1 100644 --- a/core/services/ocr2/plugins/ccip/execution_plugin.go +++ b/core/services/ocr2/plugins/ccip/execution_plugin.go @@ -28,9 +28,10 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/abihelpers" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/ccipevents" ccipconfig "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/config" - "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/customtokens" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/hasher" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/observability" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/offchaintokendata" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/offchaintokendata/usdc" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/promwrapper" "github.com/smartcontractkit/chainlink/v2/core/services/pg" ) @@ -109,6 +110,18 @@ func NewExecutionServices(lggr logger.Logger, jb job.Job, chainSet evm.LegacyCha sourceChainEventClient := ccipevents.NewLogPollerClient(sourceChain.LogPoller(), execLggr, sourceChain.Client()) + tokenDataProviders := make(map[common.Address]offchaintokendata.Provider) + + // Subscribe all token data providers + if pluginConfig.USDCAttestationApi != "" { + tokenDataProviders[usdc.TokenMapping[chainId]] = usdc.NewUSDCOffchainTokenDataService( + sourceChainEventClient, + usdc.TokenMapping[chainId], + offRampConfig.OnRamp, + pluginConfig.USDCAttestationApi, + chainId) + } + wrappedPluginFactory := NewExecutionReportingPluginFactory( ExecutionPluginConfig{ lggr: execLggr, @@ -125,7 +138,7 @@ func NewExecutionServices(lggr logger.Logger, jb job.Job, chainSet evm.LegacyCha sourceClient: sourceChain.Client(), destGasEstimator: destChain.GasEstimator(), leafHasher: hasher.NewLeafHasher(offRampConfig.SourceChainSelector, offRampConfig.ChainSelector, onRamp.Address(), hasher.NewKeccakCtx()), - usdcService: customtokens.NewUSDCService(sourceChainEventClient, offRampConfig.OnRamp, pluginConfig.USDCAttestationApi, chainId), + tokenDataProviders: tokenDataProviders, }) err = wrappedPluginFactory.UpdateLogPollerFilters(zeroAddress) @@ -161,10 +174,10 @@ func NewExecutionServices(lggr logger.Logger, jb job.Job, chainSet evm.LegacyCha return []job.ServiceCtx{job.NewServiceAdapter(oracle)}, nil } -func getExecutionPluginSourceLpChainFilters(onRamp, priceRegistry, usdcToken common.Address) []logpoller.Filter { +func getExecutionPluginSourceLpChainFilters(onRamp, priceRegistry common.Address, tokenDataProviders map[common.Address]offchaintokendata.Provider) []logpoller.Filter { var filters []logpoller.Filter - if usdcToken != common.HexToAddress("") { - filters = customtokens.GetUSDCServiceSourceLPFilters(usdcToken) + for _, provider := range tokenDataProviders { + filters = append(filters, provider.GetSourceLogPollerFilters()...) } return append(filters, []logpoller.Filter{ @@ -296,10 +309,19 @@ func unregisterExecutionPluginLpFilters( return err } - if err := unregisterLpFilters( + tokenDataProviders := make(map[common.Address]offchaintokendata.Provider) + + // TODO the called function only uses the chainId to get the message transmitter address + tokenDataProviders[usdc.TokenMapping[chainId]] = usdc.NewUSDCOffchainTokenDataService(nil, + usdc.TokenMapping[chainId], + common.Address{}, + "", + chainId) + + if err = unregisterLpFilters( q, sourceLP, - getExecutionPluginSourceLpChainFilters(destOffRampConfig.OnRamp, onRampDynCfg.PriceRegistry, customtokens.USDCTokenMapping[chainId]), + getExecutionPluginSourceLpChainFilters(destOffRampConfig.OnRamp, onRampDynCfg.PriceRegistry, tokenDataProviders), ); err != nil { return err } diff --git a/core/services/ocr2/plugins/ccip/execution_plugin_test.go b/core/services/ocr2/plugins/ccip/execution_plugin_test.go index 9e1f49c640..11e6f261b3 100644 --- a/core/services/ocr2/plugins/ccip/execution_plugin_test.go +++ b/core/services/ocr2/plugins/ccip/execution_plugin_test.go @@ -118,8 +118,7 @@ func TestGetExecutionPluginFilterNames(t *testing.T) { evm_2_evm_offramp.EVM2EVMOffRampStaticConfig{ CommitStore: commitStoreAddr, OnRamp: onRampAddr, - ChainSelector: 5790810961207155433, - SourceChainSelector: 16015286601757825753, + SourceChainSelector: 5790810961207155433, }, mockOnRamp, nil, diff --git a/core/services/ocr2/plugins/ccip/execution_reporting_plugin.go b/core/services/ocr2/plugins/ccip/execution_reporting_plugin.go index cb37f222cd..9913e9bf55 100644 --- a/core/services/ocr2/plugins/ccip/execution_reporting_plugin.go +++ b/core/services/ocr2/plugins/ccip/execution_reporting_plugin.go @@ -34,9 +34,9 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/cache" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/ccipevents" ccipconfig "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/config" - "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/customtokens" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/hasher" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/observability" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/offchaintokendata" "github.com/smartcontractkit/chainlink/v2/core/services/pg" ) @@ -67,7 +67,7 @@ type ExecutionPluginConfig struct { sourceClient evmclient.Client destGasEstimator gas.EvmFeeEstimator leafHasher hasher.LeafHasherInterface[[32]byte] - usdcService *customtokens.USDCService + tokenDataProviders map[common.Address]offchaintokendata.Provider } type ExecutionReportingPlugin struct { @@ -205,7 +205,7 @@ func (rf *ExecutionReportingPluginFactory) UpdateLogPollerFilters(destPriceRegis sourceFiltersBefore, sourceFiltersNow := rf.sourceChainFilters, getExecutionPluginSourceLpChainFilters( rf.config.onRamp.Address(), rf.config.sourcePriceRegistry.Address(), - rf.config.usdcService.SourceUSDCToken, + rf.config.tokenDataProviders, ) created, deleted := filtersDiff(sourceFiltersBefore, sourceFiltersNow) if err := unregisterLpFilters(nilQueryer, rf.config.sourceLP, deleted); err != nil { @@ -540,23 +540,20 @@ func (r *ExecutionReportingPlugin) buildBatch( continue } - // Any tokens that require offchain token data should be added here. var tokenData [][]byte for _, token := range msg.TokenAmounts { - switch token.Token { - case r.config.usdcService.SourceUSDCToken: - // TODO non-nil context - success, attestation, err2 := r.config.usdcService.IsAttestationComplete(context.TODO(), msg.SequenceNumber) + if offchainTokenDataProvider, ok := r.config.tokenDataProviders[token.Token]; ok { + ready, attestation, err2 := offchainTokenDataProvider.IsAttestationComplete(context.TODO(), msg.SequenceNumber) if err2 != nil { - msgLggr.Errorw("Skipping message unable to check USDC attestation", "err", err2) + msgLggr.Errorw("Skipping message unable to check attestation", "err", err2) continue } - if !success { - msgLggr.Warnw("Skipping message USDC attestation not ready") + if !ready { + msgLggr.Warnw("Skipping message attestation not ready") continue } - tokenData = append(tokenData, []byte(attestation)) - default: + tokenData = append(tokenData, attestation) + } else { tokenData = append(tokenData, []byte{}) } } 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 d02a0713ab..e79f8e5969 100644 --- a/core/services/ocr2/plugins/ccip/execution_reporting_plugin_test.go +++ b/core/services/ocr2/plugins/ccip/execution_reporting_plugin_test.go @@ -30,7 +30,7 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/cache" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/ccipevents" ccipconfig "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/config" - "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/customtokens" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/offchaintokendata" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/testhelpers" plugintesthelpers "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/testhelpers/plugins" "github.com/smartcontractkit/chainlink/v2/core/utils" @@ -78,6 +78,8 @@ func setupExecTestHarness(t *testing.T) execTestHarness { RelativeBoostPerWaitHour: 0.07, } sourceChainEventClient := ccipevents.NewLogPollerClient(th.SourceLP, lggr, th.SourceClient) + tokenDataProviders := make(map[common.Address]offchaintokendata.Provider) + plugin := ExecutionReportingPlugin{ config: ExecutionPluginConfig{ lggr: th.Lggr, @@ -94,7 +96,7 @@ func setupExecTestHarness(t *testing.T) execTestHarness { sourceWrappedNativeToken: th.Source.WrappedNative.Address(), leafHasher: hasher.NewLeafHasher(th.Source.ChainSelector, th.Dest.ChainSelector, th.Source.OnRamp.Address(), hasher.NewKeccakCtx()), destGasEstimator: destFeeEstimator, - usdcService: customtokens.NewUSDCService(sourceChainEventClient, th.Source.OnRamp.Address(), "", th.Source.ChainSelector), + tokenDataProviders: tokenDataProviders, }, onchainConfig: th.ExecOnchainConfig, offchainConfig: offchainConfig, @@ -1145,9 +1147,6 @@ func TestExecutionReportingPluginFactory_UpdateLogPollerFilters(t *testing.T) { offRamp := mock_contracts.NewEVM2EVMOffRampInterface(t) offRamp.On("Address").Return(utils.RandomAddress(), nil) - sourceChainId := uint64(420) - sourceUSDCTokenAddress := customtokens.USDCTokenMapping[sourceChainId] - destPriceRegistryAddr := utils.RandomAddress() rf := &ExecutionReportingPluginFactory{ @@ -1161,11 +1160,11 @@ func TestExecutionReportingPluginFactory_UpdateLogPollerFilters(t *testing.T) { commitStore: commitStore, offRamp: offRamp, sourcePriceRegistry: sourcePriceRegistry, - usdcService: customtokens.NewUSDCService(nil, onRamp.Address(), "", sourceChainId), + tokenDataProviders: make(map[common.Address]offchaintokendata.Provider), }, } - for _, f := range getExecutionPluginSourceLpChainFilters(onRamp.Address(), sourcePriceRegistry.Address(), sourceUSDCTokenAddress) { + for _, f := range getExecutionPluginSourceLpChainFilters(onRamp.Address(), sourcePriceRegistry.Address(), rf.config.tokenDataProviders) { sourceLP.On("RegisterFilter", f).Return(nil) } for _, f := range getExecutionPluginDestLpChainFilters(commitStore.Address(), offRamp.Address(), destPriceRegistryAddr) { diff --git a/core/services/ocr2/plugins/ccip/offchaintokendata/offchain_data_provider.go b/core/services/ocr2/plugins/ccip/offchaintokendata/offchain_data_provider.go new file mode 100644 index 0000000000..81065f3774 --- /dev/null +++ b/core/services/ocr2/plugins/ccip/offchaintokendata/offchain_data_provider.go @@ -0,0 +1,23 @@ +package offchaintokendata + +import ( + "context" + + "github.com/ethereum/go-ethereum/common" + + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller" +) + +// Provider is an interface for fetching offchain token data +type Provider interface { + // IsAttestationComplete returns true if the attestation for the given sequence number is complete + // and returns the attestation bytes if it is complete. + // Note: this function can be called many times, the implementation should cache the result. + IsAttestationComplete(ctx context.Context, seqNum uint64) (ready bool, attestation []byte, err error) + + // GetSourceLogPollerFilters returns the filters that should be used for the source chain log poller + GetSourceLogPollerFilters() []logpoller.Filter + + // GetSourceToken returns the token address on the source chain + GetSourceToken() common.Address +} diff --git a/core/services/ocr2/plugins/ccip/customtokens/usdc.go b/core/services/ocr2/plugins/ccip/offchaintokendata/usdc/usdc.go similarity index 54% rename from core/services/ocr2/plugins/ccip/customtokens/usdc.go rename to core/services/ocr2/plugins/ccip/offchaintokendata/usdc/usdc.go index e968da15af..a95b9c939d 100644 --- a/core/services/ocr2/plugins/ccip/customtokens/usdc.go +++ b/core/services/ocr2/plugins/ccip/offchaintokendata/usdc/usdc.go @@ -1,4 +1,4 @@ -package customtokens +package usdc import ( "context" @@ -11,22 +11,24 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/abihelpers" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/ccipevents" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/offchaintokendata" "github.com/smartcontractkit/chainlink/v2/core/utils" ) -type USDCService struct { - sourceChainEvents ccipevents.Client - attestationApi string - sourceChainId uint64 - SourceUSDCToken common.Address - OnRampAddress common.Address +type OffchainTokenDataService struct { + sourceChainEvents ccipevents.Client + attestationApi string + messageTransmitter common.Address + sourceToken common.Address + onRampAddress common.Address // Cache of sequence number -> attestation attempt - attestationCache map[uint64]USDCAttestationAttempt + attestationCache map[uint64]AttestationAttempt } -type USDCAttestationAttempt struct { +type AttestationAttempt struct { USDCMessageBody []byte USDCMessageHash [32]byte CCIPSendTxHash common.Hash @@ -41,7 +43,7 @@ type AttestationResponse struct { // Hard coded mapping of chain id to USDC token addresses // Will be removed in favour of more flexible solution. -var USDCTokenMapping = map[uint64]common.Address{ +var TokenMapping = map[uint64]common.Address{ 420: common.HexToAddress("0xe05606174bac4A6364B31bd0eCA4bf4dD368f8C6"), 43113: common.HexToAddress("0x5425890298aed601595a70ab815c96711a31bc65"), 80001: common.HexToAddress("0x9999f7fea5938fd3b1e26a12c3f2fb024e194f97"), @@ -49,48 +51,57 @@ var USDCTokenMapping = map[uint64]common.Address{ 421613: common.HexToAddress("0xfd064A18f3BF249cf1f87FC203E90D8f650f2d63"), } +var messageTransmitterMapping = map[uint64]common.Address{ + 420: common.HexToAddress("0x8c5261668696ce22758910d05bab8f186d6eb247"), +} + const ( - version = "v1" - attestationPath = "attestations" - eventSignatureString = "8c5261668696ce22758910d05bab8f186d6eb247ceac2af2e82c7dc17669b036" - USDC_MESSAGE_SENT_FILTER_NAME = "USDC message sent" + version = "v1" + attestationPath = "attestations" + MESSAGE_SENT_FILTER_NAME = "USDC message sent" ) type AttestationStatus string const ( - USDCAttestationStatusSuccess AttestationStatus = "complete" - USDCAttestationStatusPending AttestationStatus = "pending_confirmations" - USDCAttestationStatusUnchecked AttestationStatus = "unchecked" + AttestationStatusSuccess AttestationStatus = "complete" + AttestationStatusPending AttestationStatus = "pending_confirmations" + AttestationStatusUnchecked AttestationStatus = "unchecked" ) -func NewUSDCService(sourceChainEvents ccipevents.Client, onRampAddress common.Address, usdcAttestationApi string, sourceChainId uint64) *USDCService { - return &USDCService{ - sourceChainEvents: sourceChainEvents, - attestationApi: usdcAttestationApi, - sourceChainId: sourceChainId, - SourceUSDCToken: USDCTokenMapping[sourceChainId], - OnRampAddress: onRampAddress, - attestationCache: make(map[uint64]USDCAttestationAttempt), +var _ offchaintokendata.Provider = &OffchainTokenDataService{} + +func NewUSDCOffchainTokenDataService(sourceChainEvents ccipevents.Client, usdcTokenAddress, onRampAddress common.Address, usdcAttestationApi string, sourceChainId uint64) *OffchainTokenDataService { + return &OffchainTokenDataService{ + sourceChainEvents: sourceChainEvents, + attestationApi: usdcAttestationApi, + messageTransmitter: messageTransmitterMapping[sourceChainId], + onRampAddress: onRampAddress, + sourceToken: usdcTokenAddress, + attestationCache: make(map[uint64]AttestationAttempt), } } -func (usdc *USDCService) IsAttestationComplete(ctx context.Context, seqNum uint64) (bool, string, error) { +func (usdc *OffchainTokenDataService) IsAttestationComplete(ctx context.Context, seqNum uint64) (success bool, attestation []byte, err error) { response, err := usdc.GetUpdatedAttestation(ctx, seqNum) if err != nil { - return false, "", err + return false, []byte{}, err } - if response.Status == USDCAttestationStatusSuccess { - return true, response.Attestation, nil + if response.Status == AttestationStatusSuccess { + attestationBytes, err := hex.DecodeString(response.Attestation) + if err != nil { + return false, nil, err + } + return true, attestationBytes, nil } - return false, "", nil + return false, []byte{}, nil } -func (usdc *USDCService) GetUpdatedAttestation(ctx context.Context, seqNum uint64) (AttestationResponse, error) { +func (usdc *OffchainTokenDataService) GetUpdatedAttestation(ctx context.Context, seqNum uint64) (AttestationResponse, error) { // Try to get information from cache to reduce the number of database and external calls attestationAttempt, ok := usdc.attestationCache[seqNum] - if ok && attestationAttempt.USDCAttestationResponse.Status == USDCAttestationStatusSuccess { + if ok && attestationAttempt.USDCAttestationResponse.Status == AttestationStatusSuccess { // If successful, return the cached response return attestationAttempt.USDCAttestationResponse, nil } @@ -118,15 +129,15 @@ func (usdc *USDCService) GetUpdatedAttestation(ctx context.Context, seqNum uint6 return response, nil } -func (usdc *USDCService) getAttemptInfoFromCCIPMessageId(ctx context.Context, seqNum uint64) (USDCAttestationAttempt, error) { +func (usdc *OffchainTokenDataService) getAttemptInfoFromCCIPMessageId(ctx context.Context, seqNum uint64) (AttestationAttempt, error) { // Get the CCIP message send event from the log poller - ccipSendRequests, err := usdc.sourceChainEvents.GetSendRequestsBetweenSeqNums(ctx, usdc.OnRampAddress, seqNum, seqNum, 0) + ccipSendRequests, err := usdc.sourceChainEvents.GetSendRequestsBetweenSeqNums(ctx, usdc.onRampAddress, seqNum, seqNum, 0) if err != nil { - return USDCAttestationAttempt{}, err + return AttestationAttempt{}, err } if len(ccipSendRequests) != 1 { - return USDCAttestationAttempt{}, fmt.Errorf("expected 1 CCIP send request, got %d", len(ccipSendRequests)) + return AttestationAttempt{}, fmt.Errorf("expected 1 CCIP send request, got %d", len(ccipSendRequests)) } ccipSendRequest := ccipSendRequests[0] @@ -137,22 +148,22 @@ func (usdc *USDCService) getAttemptInfoFromCCIPMessageId(ctx context.Context, se // Get the USDC message body usdcMessageBody, err := usdc.sourceChainEvents.GetLastUSDCMessagePriorToLogIndexInTx(ctx, ccipSendLogIndex, ccipSendTxHash) if err != nil { - return USDCAttestationAttempt{}, err + return AttestationAttempt{}, err } - return USDCAttestationAttempt{ + return AttestationAttempt{ USDCMessageBody: usdcMessageBody, USDCMessageHash: utils.Keccak256Fixed(usdcMessageBody), CCIPSendTxHash: ccipSendTxHash, CCIPSendLogIndex: ccipSendLogIndex, USDCAttestationResponse: AttestationResponse{ - Status: USDCAttestationStatusUnchecked, + Status: AttestationStatusUnchecked, Attestation: "", }, }, nil } -func (usdc *USDCService) callAttestationApi(ctx context.Context, usdcMessageHash [32]byte) (AttestationResponse, error) { +func (usdc *OffchainTokenDataService) callAttestationApi(ctx context.Context, usdcMessageHash [32]byte) (AttestationResponse, error) { fullAttestationUrl := fmt.Sprintf("%s/%s/%s/0x%s", usdc.attestationApi, version, attestationPath, hex.EncodeToString(usdcMessageHash[:])) req, err := http.NewRequestWithContext(ctx, "GET", fullAttestationUrl, nil) if err != nil { @@ -177,12 +188,16 @@ func (usdc *USDCService) callAttestationApi(ctx context.Context, usdcMessageHash return response, nil } -func GetUSDCServiceSourceLPFilters(usdcTokenAddress common.Address) []logpoller.Filter { +func (usdc *OffchainTokenDataService) GetSourceToken() common.Address { + return usdc.sourceToken +} + +func (usdc *OffchainTokenDataService) GetSourceLogPollerFilters() []logpoller.Filter { return []logpoller.Filter{ { - Name: logpoller.FilterName(USDC_MESSAGE_SENT_FILTER_NAME, usdcTokenAddress.Hex()), - EventSigs: []common.Hash{common.HexToHash(eventSignatureString)}, - Addresses: []common.Address{usdcTokenAddress}, + Name: logpoller.FilterName(MESSAGE_SENT_FILTER_NAME, usdc.messageTransmitter.Hex()), + EventSigs: []common.Hash{abihelpers.EventSignatures.USDCMessageSent}, + Addresses: []common.Address{usdc.messageTransmitter}, }, } } diff --git a/core/services/ocr2/plugins/ccip/offchaintokendata/usdc/usdc_test.go b/core/services/ocr2/plugins/ccip/offchaintokendata/usdc/usdc_test.go new file mode 100644 index 0000000000..88ffaf7502 --- /dev/null +++ b/core/services/ocr2/plugins/ccip/offchaintokendata/usdc/usdc_test.go @@ -0,0 +1,141 @@ +package usdc + +import ( + "context" + "encoding/hex" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "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/gethwrappers/ccip/generated/evm_2_evm_onramp" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/ccipevents" + "github.com/smartcontractkit/chainlink/v2/core/utils" +) + +var ( + mockOnRampAddress = utils.RandomAddress() + mockUSDCTokenAddress = utils.RandomAddress() +) + +func TestUSDCService_callAttestationApi(t *testing.T) { + t.Skipf("Skipping test because it uses the real USDC attestation API") + usdcMessageHash := "912f22a13e9ccb979b621500f6952b2afd6e75be7eadaed93fc2625fe11c52a2" + usdcService := NewUSDCOffchainTokenDataService(nil, mockUSDCTokenAddress, mockOnRampAddress, "https://iris-api-sandbox.circle.com", 420) + + attestation, err := usdcService.callAttestationApi(context.Background(), [32]byte(common.FromHex(usdcMessageHash))) + require.NoError(t, err) + + require.Equal(t, AttestationStatusPending, attestation.Status) + require.Equal(t, "PENDING", attestation.Attestation) +} + +func TestUSDCService_callAttestationApiMock(t *testing.T) { + response := AttestationResponse{ + Status: AttestationStatusSuccess, + Attestation: "720502893578a89a8a87982982ef781c18b193", + } + + ts := getMockUSDCEndpoint(t, response) + defer ts.Close() + + usdcService := NewUSDCOffchainTokenDataService(nil, mockUSDCTokenAddress, mockOnRampAddress, ts.URL, 420) + attestation, err := usdcService.callAttestationApi(context.Background(), utils.RandomBytes32()) + require.NoError(t, err) + + require.Equal(t, response.Status, attestation.Status) + require.Equal(t, response.Attestation, attestation.Attestation) +} + +func TestUSDCService_callAttestationApiMockError(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + })) + defer ts.Close() + + usdcService := NewUSDCOffchainTokenDataService(nil, mockUSDCTokenAddress, mockOnRampAddress, ts.URL, 420) + _, err := usdcService.callAttestationApi(context.Background(), utils.RandomBytes32()) + require.Error(t, err) +} + +func TestUSDCService_IsAttestationComplete(t *testing.T) { + response := AttestationResponse{ + Status: AttestationStatusSuccess, + Attestation: "720502893578a89a8a87982982ef781c18b193", + } + + attestationBytes, err := hex.DecodeString(response.Attestation) + require.NoError(t, err) + + ts := getMockUSDCEndpoint(t, response) + defer ts.Close() + + seqNum := uint64(23825) + txHash := utils.RandomBytes32() + logIndex := int64(4) + + eventsClient := ccipevents.MockClient{} + eventsClient.On("GetSendRequestsBetweenSeqNums", + mock.Anything, + mockOnRampAddress, + seqNum, + seqNum, + 0, + ).Return([]ccipevents.Event[evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested]{ + { + Data: evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested{ + Raw: types.Log{ + TxHash: txHash, + Index: uint(logIndex), + }, + }, + }, + }, nil) + + eventsClient.On("GetLastUSDCMessagePriorToLogIndexInTx", + mock.Anything, + logIndex, + common.Hash(txHash), + ).Return(attestationBytes, nil) + + usdcService := NewUSDCOffchainTokenDataService(&eventsClient, mockUSDCTokenAddress, mockOnRampAddress, ts.URL, 420) + isReady, attestation, err := usdcService.IsAttestationComplete(context.Background(), seqNum) + require.NoError(t, err) + + require.True(t, isReady) + require.Equal(t, attestationBytes, attestation) + require.Equal(t, isReady, response.Status == AttestationStatusSuccess) +} + +func getMockUSDCEndpoint(t *testing.T, response AttestationResponse) *httptest.Server { + responseBytes, err := json.Marshal(response) + require.NoError(t, err) + + return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, err := w.Write(responseBytes) + require.NoError(t, err) + })) +} + +// Asserts the hard coded event signature matches Keccak256("MessageSent(bytes)") +func TestGetUSDCServiceSourceLPFilters(t *testing.T) { + chainId := uint64(420) + usdcService := NewUSDCOffchainTokenDataService(nil, mockUSDCTokenAddress, mockOnRampAddress, "", chainId) + + filters := usdcService.GetSourceLogPollerFilters() + expectedTransmitterAddress := messageTransmitterMapping[chainId] + + require.Equal(t, 1, len(filters)) + filter := filters[0] + require.Equal(t, logpoller.FilterName(MESSAGE_SENT_FILTER_NAME, expectedTransmitterAddress.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, expectedTransmitterAddress, filter.Addresses[0]) +} From 8c2250f8135558709f7578605668f03566301b2f Mon Sep 17 00:00:00 2001 From: Rens Rooimans Date: Thu, 7 Sep 2023 15:07:59 +0200 Subject: [PATCH 07/20] add all usdc token and messageTransmitter addresses --- .../ccip/offchaintokendata/usdc/usdc.go | 28 +++++++++++++++---- 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/core/services/ocr2/plugins/ccip/offchaintokendata/usdc/usdc.go b/core/services/ocr2/plugins/ccip/offchaintokendata/usdc/usdc.go index a95b9c939d..d5da7171ec 100644 --- a/core/services/ocr2/plugins/ccip/offchaintokendata/usdc/usdc.go +++ b/core/services/ocr2/plugins/ccip/offchaintokendata/usdc/usdc.go @@ -44,15 +44,31 @@ type AttestationResponse struct { // Hard coded mapping of chain id to USDC token addresses // Will be removed in favour of more flexible solution. var TokenMapping = map[uint64]common.Address{ - 420: common.HexToAddress("0xe05606174bac4A6364B31bd0eCA4bf4dD368f8C6"), - 43113: common.HexToAddress("0x5425890298aed601595a70ab815c96711a31bc65"), - 80001: common.HexToAddress("0x9999f7fea5938fd3b1e26a12c3f2fb024e194f97"), - 84531: common.HexToAddress("0xf175520c52418dfe19c8098071a252da48cd1c19"), - 421613: common.HexToAddress("0xfd064A18f3BF249cf1f87FC203E90D8f650f2d63"), + // Mainnet + 1: common.HexToAddress("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"), // Ethereum + 10: common.HexToAddress("0x0b2c639c533813f4aa9d7837caf62653d097ff85"), // Optimism + 42161: common.HexToAddress("0xaf88d065e77c8cc2239327c5edb3a432268e5831"), // Arbitrum + 43114: common.HexToAddress("0xb97ef9ef8734c71904d8002f8b6bc66dd9c48a6e"), // Avalanche + + // Testnets + 5: common.HexToAddress("0xca6b4c00831ffb77afe22e734a6101b268b7fcbe"), // Goerli + 420: common.HexToAddress("0xe05606174bac4A6364B31bd0eCA4bf4dD368f8C6"), // Optimism Goerli + 43113: common.HexToAddress("0x5425890298aed601595a70ab815c96711a31bc65"), // Avalanche Fuji + 421613: common.HexToAddress("0xfd064A18f3BF249cf1f87FC203E90D8f650f2d63"), // Arbitrum Goerli } var messageTransmitterMapping = map[uint64]common.Address{ - 420: common.HexToAddress("0x8c5261668696ce22758910d05bab8f186d6eb247"), + // Mainnet + 1: common.HexToAddress("0x0a992d191deec32afe36203ad87d7d289a738f81"), // Ethereum + 10: common.HexToAddress("0x4d41f22c5a0e5c74090899e5a8fb597a8842b3e8"), // Optimism + 42161: common.HexToAddress("0xC30362313FBBA5cf9163F0bb16a0e01f01A896ca"), // Arbitrum + 43114: common.HexToAddress("0x8186359af5f57fbb40c6b14a588d2a59c0c29880"), // Avalanche + + // Testnets + 5: common.HexToAddress("0xca6b4c00831ffb77afe22e734a6101b268b7fcbe"), // Goerli + 420: common.HexToAddress("0x9ff9a4da6f2157a9c82ce756f8fd7e0d75be8895"), // Optimism Goerli + 43113: common.HexToAddress("0xa9fb1b3009dcb79e2fe346c16a604b8fa8ae0a79"), // Avalanche Fuji + 421613: common.HexToAddress("0x109bc137cb64eab7c0b1dddd1edf341467dc2d35"), // Arbitrum Goerli } const ( From 0f2e9eb11e7ee831d8484f1e51e51c8558d0eb7a Mon Sep 17 00:00:00 2001 From: Rens Rooimans Date: Wed, 13 Sep 2023 13:29:27 +0200 Subject: [PATCH 08/20] minor code improvements wrap error, improved url formatting, orm fix --- core/chains/evm/logpoller/orm.go | 4 ++-- .../ocr2/plugins/ccip/execution_reporting_plugin.go | 6 ++++-- .../ocr2/plugins/ccip/offchaintokendata/usdc/usdc.go | 4 ++-- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/core/chains/evm/logpoller/orm.go b/core/chains/evm/logpoller/orm.go index 94eba93833..2e6b4199fd 100644 --- a/core/chains/evm/logpoller/orm.go +++ b/core/chains/evm/logpoller/orm.go @@ -488,8 +488,8 @@ func (o *ORM) SelectIndexedLogsByTxHash(eventSig common.Hash, txHash common.Hash err := q.Select(&logs, ` SELECT * FROM evm_logs WHERE evm_logs.evm_chain_id = $1 - AND tx_hash = $3 - AND event_sig = $4 + AND tx_hash = $2 + AND event_sig = $3 ORDER BY (evm_logs.block_number, evm_logs.log_index)`, utils.NewBig(o.chainID), txHash.Bytes(), eventSig.Bytes()) if err != nil { diff --git a/core/services/ocr2/plugins/ccip/execution_reporting_plugin.go b/core/services/ocr2/plugins/ccip/execution_reporting_plugin.go index 9913e9bf55..bb1836345c 100644 --- a/core/services/ocr2/plugins/ccip/execution_reporting_plugin.go +++ b/core/services/ocr2/plugins/ccip/execution_reporting_plugin.go @@ -553,9 +553,11 @@ func (r *ExecutionReportingPlugin) buildBatch( continue } tokenData = append(tokenData, attestation) - } else { - tokenData = append(tokenData, []byte{}) + continue } + + // No token data required + tokenData = append(tokenData, []byte{}) } // Fee boosting diff --git a/core/services/ocr2/plugins/ccip/offchaintokendata/usdc/usdc.go b/core/services/ocr2/plugins/ccip/offchaintokendata/usdc/usdc.go index d5da7171ec..241582c40d 100644 --- a/core/services/ocr2/plugins/ccip/offchaintokendata/usdc/usdc.go +++ b/core/services/ocr2/plugins/ccip/offchaintokendata/usdc/usdc.go @@ -107,7 +107,7 @@ func (usdc *OffchainTokenDataService) IsAttestationComplete(ctx context.Context, if response.Status == AttestationStatusSuccess { attestationBytes, err := hex.DecodeString(response.Attestation) if err != nil { - return false, nil, err + return false, nil, fmt.Errorf("decode response attestation: %w", err) } return true, attestationBytes, nil } @@ -180,7 +180,7 @@ func (usdc *OffchainTokenDataService) getAttemptInfoFromCCIPMessageId(ctx contex } func (usdc *OffchainTokenDataService) callAttestationApi(ctx context.Context, usdcMessageHash [32]byte) (AttestationResponse, error) { - fullAttestationUrl := fmt.Sprintf("%s/%s/%s/0x%s", usdc.attestationApi, version, attestationPath, hex.EncodeToString(usdcMessageHash[:])) + fullAttestationUrl := fmt.Sprintf("%s/%s/%s/0x%x", usdc.attestationApi, version, attestationPath, usdcMessageHash) req, err := http.NewRequestWithContext(ctx, "GET", fullAttestationUrl, nil) if err != nil { return AttestationResponse{}, err From 256c29f8042cdfc12c8d082bd0951a87fa2623f2 Mon Sep 17 00:00:00 2001 From: Rens Rooimans Date: Thu, 14 Sep 2023 15:46:17 +0200 Subject: [PATCH 09/20] fix evm logs reference --- core/chains/evm/logpoller/orm.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/chains/evm/logpoller/orm.go b/core/chains/evm/logpoller/orm.go index 9269057271..b34b3a988d 100644 --- a/core/chains/evm/logpoller/orm.go +++ b/core/chains/evm/logpoller/orm.go @@ -493,11 +493,11 @@ func (o *ORM) SelectIndexedLogsByTxHash(eventSig common.Hash, txHash common.Hash q := o.q.WithOpts(qopts...) var logs []Log err := q.Select(&logs, ` - SELECT * FROM evm_logs - WHERE evm_logs.evm_chain_id = $1 + SELECT * FROM evm.logs + WHERE evm.logs.evm_chain_id = $1 AND tx_hash = $2 AND event_sig = $3 - ORDER BY (evm_logs.block_number, evm_logs.log_index)`, + ORDER BY (evm.logs.block_number, evm.logs.log_index)`, utils.NewBig(o.chainID), txHash.Bytes(), eventSig.Bytes()) if err != nil { return nil, err From 68a70d53f7640671461c7843ac2fe96e6be48d17 Mon Sep 17 00:00:00 2001 From: Rens Rooimans Date: Thu, 14 Sep 2023 16:51:19 +0200 Subject: [PATCH 10/20] review comments observed logpoller rename IsAttestationComplete mutex in usdc mapping --- core/chains/evm/logpoller/observability.go | 4 +- .../ccip/execution_reporting_plugin.go | 46 +++++++++++-------- .../offchain_data_provider.go | 4 +- .../ccip/offchaintokendata/usdc/usdc.go | 42 +++++++++-------- .../ccip/offchaintokendata/usdc/usdc_test.go | 2 +- 5 files changed, 57 insertions(+), 41 deletions(-) diff --git a/core/chains/evm/logpoller/observability.go b/core/chains/evm/logpoller/observability.go index 270b35e64b..b6f0dace90 100644 --- a/core/chains/evm/logpoller/observability.go +++ b/core/chains/evm/logpoller/observability.go @@ -93,7 +93,9 @@ func (o *ObservedLogPoller) LatestBlockByEventSigsAddrsWithConfs(fromBlock int64 } func (o *ObservedLogPoller) IndexedLogsByTxHash(eventSig common.Hash, txHash common.Hash, qopts ...pg.QOpt) ([]Log, error) { - return o.LogPoller.IndexedLogsByTxHash(eventSig, txHash, qopts...) + return withObservedQueryAndResults(o, "IndexedLogsByTxHash", func() ([]Log, error) { + return o.LogPoller.IndexedLogsByTxHash(eventSig, txHash, qopts...) + }) } func (o *ObservedLogPoller) IndexedLogs(eventSig common.Hash, address common.Address, topicIndex int, topicValues []common.Hash, confs int, qopts ...pg.QOpt) ([]Log, error) { diff --git a/core/services/ocr2/plugins/ccip/execution_reporting_plugin.go b/core/services/ocr2/plugins/ccip/execution_reporting_plugin.go index 7c86ea527d..8c1e09b4f7 100644 --- a/core/services/ocr2/plugins/ccip/execution_reporting_plugin.go +++ b/core/services/ocr2/plugins/ccip/execution_reporting_plugin.go @@ -540,24 +540,14 @@ func (r *ExecutionReportingPlugin) buildBatch( continue } - var tokenData [][]byte - for _, token := range msg.TokenAmounts { - if offchainTokenDataProvider, ok := r.config.tokenDataProviders[token.Token]; ok { - ready, attestation, err2 := offchainTokenDataProvider.IsAttestationComplete(context.TODO(), msg.SequenceNumber) - if err2 != nil { - msgLggr.Errorw("Skipping message unable to check attestation", "err", err2) - continue - } - if !ready { - msgLggr.Warnw("Skipping message attestation not ready") - continue - } - tokenData = append(tokenData, attestation) - continue - } - - // No token data required - tokenData = append(tokenData, []byte{}) + tokenData, ready, err2 := getTokenData(context.TODO(), msg, r.config.tokenDataProviders) + if err2 != nil { + msgLggr.Errorw("Skipping message unable to check attestation", "err", err2) + continue + } + if !ready { + msgLggr.Warnw("Skipping message attestation not ready") + continue } // Fee boosting @@ -641,6 +631,26 @@ func (r *ExecutionReportingPlugin) buildBatch( return executableMessages } +func getTokenData(ctx context.Context, msg evm2EVMOnRampCCIPSendRequestedWithMeta, tokenDataProviders map[common.Address]offchaintokendata.Provider) (tokenData [][]byte, allReady bool, err error) { + for _, token := range msg.TokenAmounts { + if offchainTokenDataProvider, ok := tokenDataProviders[token.Token]; ok { + ready, attestation, err2 := offchainTokenDataProvider.IsTokenDataReady(ctx, msg.SequenceNumber) + if err2 != nil { + return [][]byte{}, false, err2 + } + if !ready { + return [][]byte{}, false, nil + } + tokenData = append(tokenData, attestation) + continue + } + + // No token data required + tokenData = append(tokenData, []byte{}) + } + return tokenData, true, nil +} + func (r *ExecutionReportingPlugin) isRateLimitEnoughForTokenPool( destTokenPoolRateLimits map[common.Address]*big.Int, sourceTokenAmounts []evm_2_evm_offramp.ClientEVMTokenAmount, diff --git a/core/services/ocr2/plugins/ccip/offchaintokendata/offchain_data_provider.go b/core/services/ocr2/plugins/ccip/offchaintokendata/offchain_data_provider.go index 81065f3774..30d970aee1 100644 --- a/core/services/ocr2/plugins/ccip/offchaintokendata/offchain_data_provider.go +++ b/core/services/ocr2/plugins/ccip/offchaintokendata/offchain_data_provider.go @@ -10,10 +10,10 @@ import ( // Provider is an interface for fetching offchain token data type Provider interface { - // IsAttestationComplete returns true if the attestation for the given sequence number is complete + // IsTokenDataReady returns true if the attestation for the given sequence number is complete // and returns the attestation bytes if it is complete. // Note: this function can be called many times, the implementation should cache the result. - IsAttestationComplete(ctx context.Context, seqNum uint64) (ready bool, attestation []byte, err error) + IsTokenDataReady(ctx context.Context, seqNum uint64) (ready bool, tokenData []byte, err error) // GetSourceLogPollerFilters returns the filters that should be used for the source chain log poller GetSourceLogPollerFilters() []logpoller.Filter diff --git a/core/services/ocr2/plugins/ccip/offchaintokendata/usdc/usdc.go b/core/services/ocr2/plugins/ccip/offchaintokendata/usdc/usdc.go index 1c02902be1..b0488acbef 100644 --- a/core/services/ocr2/plugins/ccip/offchaintokendata/usdc/usdc.go +++ b/core/services/ocr2/plugins/ccip/offchaintokendata/usdc/usdc.go @@ -7,6 +7,7 @@ import ( "fmt" "io" "net/http" + "sync" "github.com/ethereum/go-ethereum/common" @@ -25,7 +26,8 @@ type OffchainTokenDataService struct { onRampAddress common.Address // Cache of sequence number -> attestation attempt - attestationCache map[uint64]AttestationAttempt + attestationCache map[uint64]AttestationAttempt + attestationCacheMutex sync.Mutex } type AttestationAttempt struct { @@ -98,8 +100,8 @@ func NewUSDCOffchainTokenDataService(sourceChainEvents ccipevents.Client, usdcTo } } -func (usdc *OffchainTokenDataService) IsAttestationComplete(ctx context.Context, seqNum uint64) (success bool, attestation []byte, err error) { - response, err := usdc.GetUpdatedAttestation(ctx, seqNum) +func (s *OffchainTokenDataService) IsTokenDataReady(ctx context.Context, seqNum uint64) (success bool, attestation []byte, err error) { + response, err := s.GetUpdatedAttestation(ctx, seqNum) if err != nil { return false, []byte{}, err } @@ -114,9 +116,11 @@ func (usdc *OffchainTokenDataService) IsAttestationComplete(ctx context.Context, return false, []byte{}, nil } -func (usdc *OffchainTokenDataService) GetUpdatedAttestation(ctx context.Context, seqNum uint64) (AttestationResponse, error) { +func (s *OffchainTokenDataService) GetUpdatedAttestation(ctx context.Context, seqNum uint64) (AttestationResponse, error) { // Try to get information from cache to reduce the number of database and external calls - attestationAttempt, ok := usdc.attestationCache[seqNum] + s.attestationCacheMutex.Lock() + defer s.attestationCacheMutex.Unlock() + attestationAttempt, ok := s.attestationCache[seqNum] if ok && attestationAttempt.USDCAttestationResponse.Status == AttestationStatusSuccess { // If successful, return the cached response return attestationAttempt.USDCAttestationResponse, nil @@ -125,29 +129,29 @@ func (usdc *OffchainTokenDataService) GetUpdatedAttestation(ctx context.Context, // If no attempt for this message id exists, get the required data to make one if !ok { var err error - attestationAttempt, err = usdc.getAttemptInfoFromCCIPMessageId(ctx, seqNum) + attestationAttempt, err = s.getAttemptInfoFromCCIPMessageId(ctx, seqNum) if err != nil { return AttestationResponse{}, err } // Save the attempt in the cache in case the external call fails - usdc.attestationCache[seqNum] = attestationAttempt + s.attestationCache[seqNum] = attestationAttempt } - response, err := usdc.callAttestationApi(ctx, attestationAttempt.USDCMessageHash) + response, err := s.callAttestationApi(ctx, attestationAttempt.USDCMessageHash) if err != nil { return AttestationResponse{}, err } // Save the response in the cache attestationAttempt.USDCAttestationResponse = response - usdc.attestationCache[seqNum] = attestationAttempt + s.attestationCache[seqNum] = attestationAttempt return response, nil } -func (usdc *OffchainTokenDataService) getAttemptInfoFromCCIPMessageId(ctx context.Context, seqNum uint64) (AttestationAttempt, error) { +func (s *OffchainTokenDataService) getAttemptInfoFromCCIPMessageId(ctx context.Context, seqNum uint64) (AttestationAttempt, error) { // Get the CCIP message send event from the log poller - ccipSendRequests, err := usdc.sourceChainEvents.GetSendRequestsBetweenSeqNums(ctx, usdc.onRampAddress, seqNum, seqNum, 0) + ccipSendRequests, err := s.sourceChainEvents.GetSendRequestsBetweenSeqNums(ctx, s.onRampAddress, seqNum, seqNum, 0) if err != nil { return AttestationAttempt{}, err } @@ -162,7 +166,7 @@ func (usdc *OffchainTokenDataService) getAttemptInfoFromCCIPMessageId(ctx contex ccipSendLogIndex := int64(ccipSendRequest.Data.Raw.Index) // Get the USDC message body - usdcMessageBody, err := usdc.sourceChainEvents.GetLastUSDCMessagePriorToLogIndexInTx(ctx, ccipSendLogIndex, ccipSendTxHash) + usdcMessageBody, err := s.sourceChainEvents.GetLastUSDCMessagePriorToLogIndexInTx(ctx, ccipSendLogIndex, ccipSendTxHash) if err != nil { return AttestationAttempt{}, err } @@ -179,8 +183,8 @@ func (usdc *OffchainTokenDataService) getAttemptInfoFromCCIPMessageId(ctx contex }, nil } -func (usdc *OffchainTokenDataService) callAttestationApi(ctx context.Context, usdcMessageHash [32]byte) (AttestationResponse, error) { - fullAttestationUrl := fmt.Sprintf("%s/%s/%s/0x%x", usdc.attestationApi, version, attestationPath, usdcMessageHash) +func (s *OffchainTokenDataService) callAttestationApi(ctx context.Context, usdcMessageHash [32]byte) (AttestationResponse, error) { + fullAttestationUrl := fmt.Sprintf("%s/%s/%s/0x%x", s.attestationApi, version, attestationPath, usdcMessageHash) req, err := http.NewRequestWithContext(ctx, "GET", fullAttestationUrl, nil) if err != nil { return AttestationResponse{}, err @@ -204,16 +208,16 @@ func (usdc *OffchainTokenDataService) callAttestationApi(ctx context.Context, us return response, nil } -func (usdc *OffchainTokenDataService) GetSourceToken() common.Address { - return usdc.sourceToken +func (s *OffchainTokenDataService) GetSourceToken() common.Address { + return s.sourceToken } -func (usdc *OffchainTokenDataService) GetSourceLogPollerFilters() []logpoller.Filter { +func (s *OffchainTokenDataService) GetSourceLogPollerFilters() []logpoller.Filter { return []logpoller.Filter{ { - Name: logpoller.FilterName(MESSAGE_SENT_FILTER_NAME, usdc.messageTransmitter.Hex()), + Name: logpoller.FilterName(MESSAGE_SENT_FILTER_NAME, s.messageTransmitter.Hex()), EventSigs: []common.Hash{abihelpers.EventSignatures.USDCMessageSent}, - Addresses: []common.Address{usdc.messageTransmitter}, + Addresses: []common.Address{s.messageTransmitter}, }, } } diff --git a/core/services/ocr2/plugins/ccip/offchaintokendata/usdc/usdc_test.go b/core/services/ocr2/plugins/ccip/offchaintokendata/usdc/usdc_test.go index f99ac60719..cacd3bfdf4 100644 --- a/core/services/ocr2/plugins/ccip/offchaintokendata/usdc/usdc_test.go +++ b/core/services/ocr2/plugins/ccip/offchaintokendata/usdc/usdc_test.go @@ -105,7 +105,7 @@ func TestUSDCService_IsAttestationComplete(t *testing.T) { ).Return(attestationBytes, nil) usdcService := NewUSDCOffchainTokenDataService(&eventsClient, mockUSDCTokenAddress, mockOnRampAddress, ts.URL, 420) - isReady, attestation, err := usdcService.IsAttestationComplete(context.Background(), seqNum) + isReady, attestation, err := usdcService.IsTokenDataReady(context.Background(), seqNum) require.NoError(t, err) require.True(t, isReady) From addbdbbab6ab2f5554684e6b484bcf9e6b99ad73 Mon Sep 17 00:00:00 2001 From: Rens Rooimans Date: Tue, 19 Sep 2023 12:38:20 +0200 Subject: [PATCH 11/20] renames and passing in uri --- .../ocr2/plugins/ccip/execution_plugin.go | 25 +++++--- .../ccip/execution_reporting_plugin.go | 6 +- .../ccip/execution_reporting_plugin_test.go | 6 +- .../offchain_data_provider.go | 6 +- .../usdc/usdc.go | 59 ++++++++++--------- .../usdc/usdc_test.go | 27 ++++++--- 6 files changed, 73 insertions(+), 56 deletions(-) rename core/services/ocr2/plugins/ccip/{offchaintokendata => tokendata}/offchain_data_provider.go (86%) rename core/services/ocr2/plugins/ccip/{offchaintokendata => tokendata}/usdc/usdc.go (78%) rename core/services/ocr2/plugins/ccip/{offchaintokendata => tokendata}/usdc/usdc_test.go (78%) diff --git a/core/services/ocr2/plugins/ccip/execution_plugin.go b/core/services/ocr2/plugins/ccip/execution_plugin.go index 6f3d57cd04..b9be42fd96 100644 --- a/core/services/ocr2/plugins/ccip/execution_plugin.go +++ b/core/services/ocr2/plugins/ccip/execution_plugin.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "net/url" "strconv" "github.com/ethereum/go-ethereum/accounts/abi/bind" @@ -15,6 +16,7 @@ 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/ccipevents" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/hashlib" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/oraclelib" @@ -31,8 +33,8 @@ import ( "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/observability" - "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/offchaintokendata" - "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/offchaintokendata/usdc" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/tokendata" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/tokendata/usdc" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/promwrapper" "github.com/smartcontractkit/chainlink/v2/core/services/pg" ) @@ -111,15 +113,19 @@ func NewExecutionServices(lggr logger.Logger, jb job.Job, chainSet evm.LegacyCha sourceChainEventClient := ccipevents.NewLogPollerClient(sourceChain.LogPoller(), execLggr, sourceChain.Client()) - tokenDataProviders := make(map[common.Address]offchaintokendata.Provider) + tokenDataProviders := make(map[common.Address]tokendata.Reader) // Subscribe all token data providers if pluginConfig.USDCAttestationApi != "" { - tokenDataProviders[usdc.TokenMapping[chainId]] = usdc.NewUSDCOffchainTokenDataService( + attestationURI, err := url.ParseRequestURI(pluginConfig.USDCAttestationApi) + if err != nil { + return nil, errors.Wrap(err, "failed to parse USDC attestation API") + } + tokenDataProviders[usdc.TokenMapping[chainId]] = usdc.NewUSDCTokenDataReader( sourceChainEventClient, usdc.TokenMapping[chainId], offRampConfig.OnRamp, - pluginConfig.USDCAttestationApi, + attestationURI, chainId) } @@ -175,7 +181,7 @@ func NewExecutionServices(lggr logger.Logger, jb job.Job, chainSet evm.LegacyCha return []job.ServiceCtx{job.NewServiceAdapter(oracle)}, nil } -func getExecutionPluginSourceLpChainFilters(onRamp, priceRegistry common.Address, tokenDataProviders map[common.Address]offchaintokendata.Provider) []logpoller.Filter { +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()...) @@ -310,13 +316,14 @@ func unregisterExecutionPluginLpFilters( return err } - tokenDataProviders := make(map[common.Address]offchaintokendata.Provider) + tokenDataProviders := make(map[common.Address]tokendata.Reader) // TODO the called function only uses the chainId to get the message transmitter address - tokenDataProviders[usdc.TokenMapping[chainId]] = usdc.NewUSDCOffchainTokenDataService(nil, + tokenDataProviders[usdc.TokenMapping[chainId]] = usdc.NewUSDCTokenDataReader( + nil, usdc.TokenMapping[chainId], common.Address{}, - "", + nil, chainId) if err = unregisterLpFilters( diff --git a/core/services/ocr2/plugins/ccip/execution_reporting_plugin.go b/core/services/ocr2/plugins/ccip/execution_reporting_plugin.go index 8c1e09b4f7..5db50a4151 100644 --- a/core/services/ocr2/plugins/ccip/execution_reporting_plugin.go +++ b/core/services/ocr2/plugins/ccip/execution_reporting_plugin.go @@ -36,7 +36,7 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipevents" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/hashlib" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/observability" - "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/offchaintokendata" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/tokendata" "github.com/smartcontractkit/chainlink/v2/core/services/pg" ) @@ -67,7 +67,7 @@ type ExecutionPluginConfig struct { sourceClient evmclient.Client destGasEstimator gas.EvmFeeEstimator leafHasher hashlib.LeafHasherInterface[[32]byte] - tokenDataProviders map[common.Address]offchaintokendata.Provider + tokenDataProviders map[common.Address]tokendata.Reader } type ExecutionReportingPlugin struct { @@ -631,7 +631,7 @@ func (r *ExecutionReportingPlugin) buildBatch( return executableMessages } -func getTokenData(ctx context.Context, msg evm2EVMOnRampCCIPSendRequestedWithMeta, tokenDataProviders map[common.Address]offchaintokendata.Provider) (tokenData [][]byte, allReady bool, err error) { +func getTokenData(ctx context.Context, msg evm2EVMOnRampCCIPSendRequestedWithMeta, tokenDataProviders map[common.Address]tokendata.Reader) (tokenData [][]byte, allReady bool, err error) { for _, token := range msg.TokenAmounts { if offchainTokenDataProvider, ok := tokenDataProviders[token.Token]; ok { ready, attestation, err2 := offchainTokenDataProvider.IsTokenDataReady(ctx, msg.SequenceNumber) 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 342a400ca4..f1dc135d2f 100644 --- a/core/services/ocr2/plugins/ccip/execution_reporting_plugin_test.go +++ b/core/services/ocr2/plugins/ccip/execution_reporting_plugin_test.go @@ -31,9 +31,9 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/cache" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipevents" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/hashlib" - "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/offchaintokendata" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/testhelpers" plugintesthelpers "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/testhelpers/plugins" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/tokendata" "github.com/smartcontractkit/chainlink/v2/core/utils" "github.com/smartcontractkit/chainlink/v2/core/assets" @@ -78,7 +78,7 @@ func setupExecTestHarness(t *testing.T) execTestHarness { RelativeBoostPerWaitHour: 0.07, } sourceChainEventClient := ccipevents.NewLogPollerClient(th.SourceLP, lggr, th.SourceClient) - tokenDataProviders := make(map[common.Address]offchaintokendata.Provider) + tokenDataProviders := make(map[common.Address]tokendata.Reader) plugin := ExecutionReportingPlugin{ config: ExecutionPluginConfig{ @@ -1160,7 +1160,7 @@ func TestExecutionReportingPluginFactory_UpdateLogPollerFilters(t *testing.T) { commitStore: commitStore, offRamp: offRamp, sourcePriceRegistry: sourcePriceRegistry, - tokenDataProviders: make(map[common.Address]offchaintokendata.Provider), + tokenDataProviders: make(map[common.Address]tokendata.Reader), }, } diff --git a/core/services/ocr2/plugins/ccip/offchaintokendata/offchain_data_provider.go b/core/services/ocr2/plugins/ccip/tokendata/offchain_data_provider.go similarity index 86% rename from core/services/ocr2/plugins/ccip/offchaintokendata/offchain_data_provider.go rename to core/services/ocr2/plugins/ccip/tokendata/offchain_data_provider.go index 30d970aee1..6e953d374d 100644 --- a/core/services/ocr2/plugins/ccip/offchaintokendata/offchain_data_provider.go +++ b/core/services/ocr2/plugins/ccip/tokendata/offchain_data_provider.go @@ -1,4 +1,4 @@ -package offchaintokendata +package tokendata import ( "context" @@ -8,8 +8,8 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller" ) -// Provider is an interface for fetching offchain token data -type Provider interface { +// Reader is an interface for fetching offchain token data +type Reader interface { // IsTokenDataReady returns true if the attestation for the given sequence number is complete // and returns the attestation bytes if it is complete. // Note: this function can be called many times, the implementation should cache the result. diff --git a/core/services/ocr2/plugins/ccip/offchaintokendata/usdc/usdc.go b/core/services/ocr2/plugins/ccip/tokendata/usdc/usdc.go similarity index 78% rename from core/services/ocr2/plugins/ccip/offchaintokendata/usdc/usdc.go rename to core/services/ocr2/plugins/ccip/tokendata/usdc/usdc.go index b0488acbef..c6507f9fbb 100644 --- a/core/services/ocr2/plugins/ccip/offchaintokendata/usdc/usdc.go +++ b/core/services/ocr2/plugins/ccip/tokendata/usdc/usdc.go @@ -7,6 +7,7 @@ import ( "fmt" "io" "net/http" + "net/url" "sync" "github.com/ethereum/go-ethereum/common" @@ -14,31 +15,31 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/abihelpers" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipevents" - "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/offchaintokendata" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/tokendata" "github.com/smartcontractkit/chainlink/v2/core/utils" ) -type OffchainTokenDataService struct { +type TokenDataReader struct { sourceChainEvents ccipevents.Client - attestationApi string + attestationApi *url.URL messageTransmitter common.Address sourceToken common.Address onRampAddress common.Address // Cache of sequence number -> attestation attempt - attestationCache map[uint64]AttestationAttempt + attestationCache map[uint64]attestationAttempt attestationCacheMutex sync.Mutex } -type AttestationAttempt struct { +type attestationAttempt struct { USDCMessageBody []byte USDCMessageHash [32]byte CCIPSendTxHash common.Hash CCIPSendLogIndex int64 - USDCAttestationResponse AttestationResponse + USDCAttestationResponse attestationResponse } -type AttestationResponse struct { +type attestationResponse struct { Status AttestationStatus `json:"status"` Attestation string `json:"attestation"` } @@ -87,20 +88,20 @@ const ( AttestationStatusUnchecked AttestationStatus = "unchecked" ) -var _ offchaintokendata.Provider = &OffchainTokenDataService{} +var _ tokendata.Reader = &TokenDataReader{} -func NewUSDCOffchainTokenDataService(sourceChainEvents ccipevents.Client, usdcTokenAddress, onRampAddress common.Address, usdcAttestationApi string, sourceChainId uint64) *OffchainTokenDataService { - return &OffchainTokenDataService{ +func NewUSDCTokenDataReader(sourceChainEvents ccipevents.Client, usdcTokenAddress, onRampAddress common.Address, usdcAttestationApi *url.URL, sourceChainId uint64) *TokenDataReader { + return &TokenDataReader{ sourceChainEvents: sourceChainEvents, attestationApi: usdcAttestationApi, messageTransmitter: messageTransmitterMapping[sourceChainId], onRampAddress: onRampAddress, sourceToken: usdcTokenAddress, - attestationCache: make(map[uint64]AttestationAttempt), + attestationCache: make(map[uint64]attestationAttempt), } } -func (s *OffchainTokenDataService) IsTokenDataReady(ctx context.Context, seqNum uint64) (success bool, attestation []byte, err error) { +func (s *TokenDataReader) IsTokenDataReady(ctx context.Context, seqNum uint64) (success bool, attestation []byte, err error) { response, err := s.GetUpdatedAttestation(ctx, seqNum) if err != nil { return false, []byte{}, err @@ -116,7 +117,7 @@ func (s *OffchainTokenDataService) IsTokenDataReady(ctx context.Context, seqNum return false, []byte{}, nil } -func (s *OffchainTokenDataService) GetUpdatedAttestation(ctx context.Context, seqNum uint64) (AttestationResponse, error) { +func (s *TokenDataReader) GetUpdatedAttestation(ctx context.Context, seqNum uint64) (attestationResponse, error) { // Try to get information from cache to reduce the number of database and external calls s.attestationCacheMutex.Lock() defer s.attestationCacheMutex.Unlock() @@ -131,7 +132,7 @@ func (s *OffchainTokenDataService) GetUpdatedAttestation(ctx context.Context, se var err error attestationAttempt, err = s.getAttemptInfoFromCCIPMessageId(ctx, seqNum) if err != nil { - return AttestationResponse{}, err + return attestationResponse{}, err } // Save the attempt in the cache in case the external call fails s.attestationCache[seqNum] = attestationAttempt @@ -139,7 +140,7 @@ func (s *OffchainTokenDataService) GetUpdatedAttestation(ctx context.Context, se response, err := s.callAttestationApi(ctx, attestationAttempt.USDCMessageHash) if err != nil { - return AttestationResponse{}, err + return attestationResponse{}, err } // Save the response in the cache @@ -149,15 +150,15 @@ func (s *OffchainTokenDataService) GetUpdatedAttestation(ctx context.Context, se return response, nil } -func (s *OffchainTokenDataService) getAttemptInfoFromCCIPMessageId(ctx context.Context, seqNum uint64) (AttestationAttempt, error) { +func (s *TokenDataReader) getAttemptInfoFromCCIPMessageId(ctx context.Context, seqNum uint64) (attestationAttempt, error) { // Get the CCIP message send event from the log poller ccipSendRequests, err := s.sourceChainEvents.GetSendRequestsBetweenSeqNums(ctx, s.onRampAddress, seqNum, seqNum, 0) if err != nil { - return AttestationAttempt{}, err + return attestationAttempt{}, err } if len(ccipSendRequests) != 1 { - return AttestationAttempt{}, fmt.Errorf("expected 1 CCIP send request, got %d", len(ccipSendRequests)) + return attestationAttempt{}, fmt.Errorf("expected 1 CCIP send request, got %d", len(ccipSendRequests)) } ccipSendRequest := ccipSendRequests[0] @@ -168,51 +169,51 @@ func (s *OffchainTokenDataService) getAttemptInfoFromCCIPMessageId(ctx context.C // Get the USDC message body usdcMessageBody, err := s.sourceChainEvents.GetLastUSDCMessagePriorToLogIndexInTx(ctx, ccipSendLogIndex, ccipSendTxHash) if err != nil { - return AttestationAttempt{}, err + return attestationAttempt{}, err } - return AttestationAttempt{ + return attestationAttempt{ USDCMessageBody: usdcMessageBody, USDCMessageHash: utils.Keccak256Fixed(usdcMessageBody), CCIPSendTxHash: ccipSendTxHash, CCIPSendLogIndex: ccipSendLogIndex, - USDCAttestationResponse: AttestationResponse{ + USDCAttestationResponse: attestationResponse{ Status: AttestationStatusUnchecked, Attestation: "", }, }, nil } -func (s *OffchainTokenDataService) callAttestationApi(ctx context.Context, usdcMessageHash [32]byte) (AttestationResponse, error) { +func (s *TokenDataReader) callAttestationApi(ctx context.Context, usdcMessageHash [32]byte) (attestationResponse, error) { fullAttestationUrl := fmt.Sprintf("%s/%s/%s/0x%x", s.attestationApi, version, attestationPath, usdcMessageHash) req, err := http.NewRequestWithContext(ctx, "GET", fullAttestationUrl, nil) if err != nil { - return AttestationResponse{}, err + return attestationResponse{}, err } req.Header.Add("accept", "application/json") res, err := http.DefaultClient.Do(req) if err != nil { - return AttestationResponse{}, err + return attestationResponse{}, err } defer res.Body.Close() body, err := io.ReadAll(res.Body) if err != nil { - return AttestationResponse{}, err + return attestationResponse{}, err } - var response AttestationResponse + var response attestationResponse err = json.Unmarshal(body, &response) if err != nil { - return AttestationResponse{}, err + return attestationResponse{}, err } return response, nil } -func (s *OffchainTokenDataService) GetSourceToken() common.Address { +func (s *TokenDataReader) GetSourceToken() common.Address { return s.sourceToken } -func (s *OffchainTokenDataService) GetSourceLogPollerFilters() []logpoller.Filter { +func (s *TokenDataReader) GetSourceLogPollerFilters() []logpoller.Filter { return []logpoller.Filter{ { Name: logpoller.FilterName(MESSAGE_SENT_FILTER_NAME, s.messageTransmitter.Hex()), diff --git a/core/services/ocr2/plugins/ccip/offchaintokendata/usdc/usdc_test.go b/core/services/ocr2/plugins/ccip/tokendata/usdc/usdc_test.go similarity index 78% rename from core/services/ocr2/plugins/ccip/offchaintokendata/usdc/usdc_test.go rename to core/services/ocr2/plugins/ccip/tokendata/usdc/usdc_test.go index cacd3bfdf4..8f8a346ab9 100644 --- a/core/services/ocr2/plugins/ccip/offchaintokendata/usdc/usdc_test.go +++ b/core/services/ocr2/plugins/ccip/tokendata/usdc/usdc_test.go @@ -6,6 +6,7 @@ import ( "encoding/json" "net/http" "net/http/httptest" + "net/url" "testing" "github.com/ethereum/go-ethereum/common" @@ -27,7 +28,9 @@ var ( func TestUSDCService_callAttestationApi(t *testing.T) { t.Skipf("Skipping test because it uses the real USDC attestation API") usdcMessageHash := "912f22a13e9ccb979b621500f6952b2afd6e75be7eadaed93fc2625fe11c52a2" - usdcService := NewUSDCOffchainTokenDataService(nil, mockUSDCTokenAddress, mockOnRampAddress, "https://iris-api-sandbox.circle.com", 420) + attestationURI, err := url.ParseRequestURI("https://iris-api-sandbox.circle.com") + require.NoError(t, err) + usdcService := NewUSDCTokenDataReader(nil, mockUSDCTokenAddress, mockOnRampAddress, attestationURI, 420) attestation, err := usdcService.callAttestationApi(context.Background(), [32]byte(common.FromHex(usdcMessageHash))) require.NoError(t, err) @@ -37,15 +40,17 @@ func TestUSDCService_callAttestationApi(t *testing.T) { } func TestUSDCService_callAttestationApiMock(t *testing.T) { - response := AttestationResponse{ + response := attestationResponse{ Status: AttestationStatusSuccess, Attestation: "720502893578a89a8a87982982ef781c18b193", } ts := getMockUSDCEndpoint(t, response) defer ts.Close() + attestationURI, err := url.ParseRequestURI(ts.URL) + require.NoError(t, err) - usdcService := NewUSDCOffchainTokenDataService(nil, mockUSDCTokenAddress, mockOnRampAddress, ts.URL, 420) + usdcService := NewUSDCTokenDataReader(nil, mockUSDCTokenAddress, mockOnRampAddress, attestationURI, 420) attestation, err := usdcService.callAttestationApi(context.Background(), utils.RandomBytes32()) require.NoError(t, err) @@ -58,14 +63,16 @@ func TestUSDCService_callAttestationApiMockError(t *testing.T) { w.WriteHeader(http.StatusInternalServerError) })) defer ts.Close() + attestationURI, err := url.ParseRequestURI(ts.URL) + require.NoError(t, err) - usdcService := NewUSDCOffchainTokenDataService(nil, mockUSDCTokenAddress, mockOnRampAddress, ts.URL, 420) - _, err := usdcService.callAttestationApi(context.Background(), utils.RandomBytes32()) + usdcService := NewUSDCTokenDataReader(nil, mockUSDCTokenAddress, mockOnRampAddress, attestationURI, 420) + _, err = usdcService.callAttestationApi(context.Background(), utils.RandomBytes32()) require.Error(t, err) } func TestUSDCService_IsAttestationComplete(t *testing.T) { - response := AttestationResponse{ + response := attestationResponse{ Status: AttestationStatusSuccess, Attestation: "720502893578a89a8a87982982ef781c18b193", } @@ -103,8 +110,10 @@ func TestUSDCService_IsAttestationComplete(t *testing.T) { logIndex, common.Hash(txHash), ).Return(attestationBytes, nil) + attestationURI, err := url.ParseRequestURI(ts.URL) + require.NoError(t, err) - usdcService := NewUSDCOffchainTokenDataService(&eventsClient, mockUSDCTokenAddress, mockOnRampAddress, ts.URL, 420) + usdcService := NewUSDCTokenDataReader(&eventsClient, mockUSDCTokenAddress, mockOnRampAddress, attestationURI, 420) isReady, attestation, err := usdcService.IsTokenDataReady(context.Background(), seqNum) require.NoError(t, err) @@ -113,7 +122,7 @@ func TestUSDCService_IsAttestationComplete(t *testing.T) { require.Equal(t, isReady, response.Status == AttestationStatusSuccess) } -func getMockUSDCEndpoint(t *testing.T, response AttestationResponse) *httptest.Server { +func getMockUSDCEndpoint(t *testing.T, response attestationResponse) *httptest.Server { responseBytes, err := json.Marshal(response) require.NoError(t, err) @@ -126,7 +135,7 @@ func getMockUSDCEndpoint(t *testing.T, response AttestationResponse) *httptest.S // Asserts the hard coded event signature matches Keccak256("MessageSent(bytes)") func TestGetUSDCServiceSourceLPFilters(t *testing.T) { chainId := uint64(420) - usdcService := NewUSDCOffchainTokenDataService(nil, mockUSDCTokenAddress, mockOnRampAddress, "", chainId) + usdcService := NewUSDCTokenDataReader(nil, mockUSDCTokenAddress, mockOnRampAddress, nil, chainId) filters := usdcService.GetSourceLogPollerFilters() expectedTransmitterAddress := messageTransmitterMapping[chainId] From 0ef66b382d8c848026b9c824a6ade4520d15ec63 Mon Sep 17 00:00:00 2001 From: Rens Rooimans Date: Tue, 19 Sep 2023 14:04:01 +0200 Subject: [PATCH 12/20] re-use logindex and tx hash from exec plugin --- .../ccip/execution_reporting_plugin.go | 6 +- .../ccip/tokendata/offchain_data_provider.go | 2 +- .../ocr2/plugins/ccip/tokendata/usdc/usdc.go | 95 ++++++++----------- .../plugins/ccip/tokendata/usdc/usdc_test.go | 2 +- 4 files changed, 45 insertions(+), 60 deletions(-) diff --git a/core/services/ocr2/plugins/ccip/execution_reporting_plugin.go b/core/services/ocr2/plugins/ccip/execution_reporting_plugin.go index ec517c19b0..57f6eba117 100644 --- a/core/services/ocr2/plugins/ccip/execution_reporting_plugin.go +++ b/core/services/ocr2/plugins/ccip/execution_reporting_plugin.go @@ -239,6 +239,8 @@ type evm2EVMOnRampCCIPSendRequestedWithMeta struct { blockTimestamp time.Time executed bool finalized bool + logIndex uint + txHash common.Hash } func (r *ExecutionReportingPlugin) getExecutableObservations(ctx context.Context, lggr logger.Logger, timestamp types.ReportTimestamp, inflight []InflightInternalExecutionReport) ([]ObservedMessage, error) { @@ -639,7 +641,7 @@ func (r *ExecutionReportingPlugin) buildBatch( func getTokenData(ctx context.Context, msg evm2EVMOnRampCCIPSendRequestedWithMeta, tokenDataProviders map[common.Address]tokendata.Reader) (tokenData [][]byte, allReady bool, err error) { for _, token := range msg.TokenAmounts { if offchainTokenDataProvider, ok := tokenDataProviders[token.Token]; ok { - ready, attestation, err2 := offchainTokenDataProvider.IsTokenDataReady(ctx, msg.SequenceNumber) + ready, attestation, err2 := offchainTokenDataProvider.IsTokenDataReady(ctx, msg.SequenceNumber, msg.logIndex, msg.txHash) if err2 != nil { return [][]byte{}, false, err2 } @@ -831,6 +833,8 @@ func (r *ExecutionReportingPlugin) getReportsWithSendRequests( blockTimestamp: sendReq.BlockTimestamp, executed: executed, finalized: finalized, + logIndex: sendReq.Data.Raw.Index, + txHash: sendReq.Data.Raw.TxHash, } // attach the msg to the appropriate reports diff --git a/core/services/ocr2/plugins/ccip/tokendata/offchain_data_provider.go b/core/services/ocr2/plugins/ccip/tokendata/offchain_data_provider.go index 6e953d374d..4f4f2c444f 100644 --- a/core/services/ocr2/plugins/ccip/tokendata/offchain_data_provider.go +++ b/core/services/ocr2/plugins/ccip/tokendata/offchain_data_provider.go @@ -13,7 +13,7 @@ type Reader interface { // IsTokenDataReady returns true if the attestation for the given sequence number is complete // and returns the attestation bytes if it is complete. // Note: this function can be called many times, the implementation should cache the result. - IsTokenDataReady(ctx context.Context, seqNum uint64) (ready bool, tokenData []byte, err error) + IsTokenDataReady(ctx context.Context, seqNum uint64, logIndex uint, txHash common.Hash) (ready bool, tokenData []byte, err error) // GetSourceLogPollerFilters returns the filters that should be used for the source chain log poller GetSourceLogPollerFilters() []logpoller.Filter diff --git a/core/services/ocr2/plugins/ccip/tokendata/usdc/usdc.go b/core/services/ocr2/plugins/ccip/tokendata/usdc/usdc.go index a41eace68f..6b7ea7f642 100644 --- a/core/services/ocr2/plugins/ccip/tokendata/usdc/usdc.go +++ b/core/services/ocr2/plugins/ccip/tokendata/usdc/usdc.go @@ -11,6 +11,7 @@ import ( "sync" "github.com/ethereum/go-ethereum/common" + "github.com/pkg/errors" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/abihelpers" @@ -26,16 +27,16 @@ type TokenDataReader struct { sourceToken common.Address onRampAddress common.Address + // Cache of sequence number -> usdc message body + usdcMessageHashCache map[uint64][32]byte + usdcMessageHashCacheMutex sync.Mutex + // Cache of sequence number -> attestation attempt attestationCache map[uint64]attestationAttempt attestationCacheMutex sync.Mutex } type attestationAttempt struct { - USDCMessageBody []byte - USDCMessageHash [32]byte - CCIPSendTxHash common.Hash - CCIPSendLogIndex int64 USDCAttestationResponse attestationResponse } @@ -83,26 +84,26 @@ const ( type AttestationStatus string const ( - AttestationStatusSuccess AttestationStatus = "complete" - AttestationStatusPending AttestationStatus = "pending_confirmations" - AttestationStatusUnchecked AttestationStatus = "unchecked" + AttestationStatusSuccess AttestationStatus = "complete" + AttestationStatusPending AttestationStatus = "pending_confirmations" ) var _ tokendata.Reader = &TokenDataReader{} func NewUSDCTokenDataReader(sourceChainEvents ccipdata.Reader, usdcTokenAddress, onRampAddress common.Address, usdcAttestationApi *url.URL, sourceChainId uint64) *TokenDataReader { return &TokenDataReader{ - sourceChainEvents: sourceChainEvents, - attestationApi: usdcAttestationApi, - messageTransmitter: messageTransmitterMapping[sourceChainId], - onRampAddress: onRampAddress, - sourceToken: usdcTokenAddress, - attestationCache: make(map[uint64]attestationAttempt), + sourceChainEvents: sourceChainEvents, + attestationApi: usdcAttestationApi, + messageTransmitter: messageTransmitterMapping[sourceChainId], + onRampAddress: onRampAddress, + sourceToken: usdcTokenAddress, + attestationCache: make(map[uint64]attestationAttempt), + usdcMessageHashCache: make(map[uint64][32]byte), } } -func (s *TokenDataReader) IsTokenDataReady(ctx context.Context, seqNum uint64) (success bool, attestation []byte, err error) { - response, err := s.GetUpdatedAttestation(ctx, seqNum) +func (s *TokenDataReader) IsTokenDataReady(ctx context.Context, seqNum uint64, logIndex uint, txHash common.Hash) (success bool, attestation []byte, err error) { + response, err := s.getUpdatedAttestation(ctx, seqNum, logIndex, txHash) if err != nil { return false, []byte{}, err } @@ -117,71 +118,51 @@ func (s *TokenDataReader) IsTokenDataReady(ctx context.Context, seqNum uint64) ( return false, []byte{}, nil } -func (s *TokenDataReader) GetUpdatedAttestation(ctx context.Context, seqNum uint64) (attestationResponse, error) { +func (s *TokenDataReader) getUpdatedAttestation(ctx context.Context, seqNum uint64, logIndex uint, txHash common.Hash) (attestationResponse, error) { // Try to get information from cache to reduce the number of database and external calls s.attestationCacheMutex.Lock() defer s.attestationCacheMutex.Unlock() - attestationAttempt, ok := s.attestationCache[seqNum] - if ok && attestationAttempt.USDCAttestationResponse.Status == AttestationStatusSuccess { + attempt, ok := s.attestationCache[seqNum] + if ok && attempt.USDCAttestationResponse.Status == AttestationStatusSuccess { // If successful, return the cached response - return attestationAttempt.USDCAttestationResponse, nil + return attempt.USDCAttestationResponse, nil } - // If no attempt for this message id exists, get the required data to make one - if !ok { - var err error - attestationAttempt, err = s.getAttemptInfoFromCCIPMessageId(ctx, seqNum) - if err != nil { - return attestationResponse{}, err - } - // Save the attempt in the cache in case the external call fails - s.attestationCache[seqNum] = attestationAttempt + messageBody, err := s.getUSDCMessageBody(ctx, seqNum, logIndex, txHash) + if err != nil { + return attestationResponse{}, errors.Wrap(err, "failed getting the USDC message body") } - response, err := s.callAttestationApi(ctx, attestationAttempt.USDCMessageHash) + response, err := s.callAttestationApi(ctx, messageBody) if err != nil { return attestationResponse{}, err } // Save the response in the cache - attestationAttempt.USDCAttestationResponse = response - s.attestationCache[seqNum] = attestationAttempt + attempt.USDCAttestationResponse = response + s.attestationCache[seqNum] = attempt return response, nil } -func (s *TokenDataReader) getAttemptInfoFromCCIPMessageId(ctx context.Context, seqNum uint64) (attestationAttempt, error) { - // Get the CCIP message send event from the log poller - ccipSendRequests, err := s.sourceChainEvents.GetSendRequestsBetweenSeqNums(ctx, s.onRampAddress, seqNum, seqNum, 0) - if err != nil { - return attestationAttempt{}, err - } +func (s *TokenDataReader) getUSDCMessageBody(ctx context.Context, seqNum uint64, logIndex uint, txHash common.Hash) ([32]byte, error) { + s.usdcMessageHashCacheMutex.Lock() + defer s.usdcMessageHashCacheMutex.Unlock() - if len(ccipSendRequests) != 1 { - return attestationAttempt{}, fmt.Errorf("expected 1 CCIP send request, got %d", len(ccipSendRequests)) + if body, ok := s.usdcMessageHashCache[seqNum]; ok { + return body, nil } - ccipSendRequest := ccipSendRequests[0] - - ccipSendTxHash := ccipSendRequest.Data.Raw.TxHash - ccipSendLogIndex := int64(ccipSendRequest.Data.Raw.Index) - - // Get the USDC message body - usdcMessageBody, err := s.sourceChainEvents.GetLastUSDCMessagePriorToLogIndexInTx(ctx, ccipSendLogIndex, ccipSendTxHash) + usdcMessageBody, err := s.sourceChainEvents.GetLastUSDCMessagePriorToLogIndexInTx(ctx, int64(logIndex), txHash) if err != nil { - return attestationAttempt{}, err + return [32]byte{}, err } - return attestationAttempt{ - USDCMessageBody: usdcMessageBody, - USDCMessageHash: utils.Keccak256Fixed(usdcMessageBody), - CCIPSendTxHash: ccipSendTxHash, - CCIPSendLogIndex: ccipSendLogIndex, - USDCAttestationResponse: attestationResponse{ - Status: AttestationStatusUnchecked, - Attestation: "", - }, - }, nil + msgBodyHash := utils.Keccak256Fixed(usdcMessageBody) + + // Save the attempt in the cache in case the external call fails + s.usdcMessageHashCache[seqNum] = msgBodyHash + return msgBodyHash, nil } func (s *TokenDataReader) callAttestationApi(ctx context.Context, usdcMessageHash [32]byte) (attestationResponse, error) { 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 aa4dc1e30c..cfc43bb22f 100644 --- a/core/services/ocr2/plugins/ccip/tokendata/usdc/usdc_test.go +++ b/core/services/ocr2/plugins/ccip/tokendata/usdc/usdc_test.go @@ -114,7 +114,7 @@ func TestUSDCService_IsAttestationComplete(t *testing.T) { require.NoError(t, err) usdcService := NewUSDCTokenDataReader(&eventsClient, mockUSDCTokenAddress, mockOnRampAddress, attestationURI, 420) - isReady, attestation, err := usdcService.IsTokenDataReady(context.Background(), seqNum) + isReady, attestation, err := usdcService.IsTokenDataReady(context.Background(), seqNum, uint(logIndex), txHash) require.NoError(t, err) require.True(t, isReady) From 239b7caf748ad8821380d0cb52ad6d4ce4762d6b Mon Sep 17 00:00:00 2001 From: Rens Rooimans Date: Tue, 19 Sep 2023 14:32:24 +0200 Subject: [PATCH 13/20] cached reader + ReadTokenData rename --- .../ocr2/plugins/ccip/execution_plugin.go | 4 +- .../ccip/execution_reporting_plugin.go | 9 +++-- .../plugins/ccip/tokendata/cached_reader.go | 40 +++++++++++++++++++ .../{offchain_data_provider.go => reader.go} | 11 +++-- .../ocr2/plugins/ccip/tokendata/usdc/usdc.go | 30 +++----------- .../plugins/ccip/tokendata/usdc/usdc_test.go | 5 +-- 6 files changed, 62 insertions(+), 37 deletions(-) create mode 100644 core/services/ocr2/plugins/ccip/tokendata/cached_reader.go rename core/services/ocr2/plugins/ccip/tokendata/{offchain_data_provider.go => reader.go} (56%) diff --git a/core/services/ocr2/plugins/ccip/execution_plugin.go b/core/services/ocr2/plugins/ccip/execution_plugin.go index 1942b8a261..9c50d6da44 100644 --- a/core/services/ocr2/plugins/ccip/execution_plugin.go +++ b/core/services/ocr2/plugins/ccip/execution_plugin.go @@ -121,12 +121,12 @@ func NewExecutionServices(lggr logger.Logger, jb job.Job, chainSet evm.LegacyCha if err != nil { return nil, errors.Wrap(err, "failed to parse USDC attestation API") } - tokenDataProviders[usdc.TokenMapping[chainId]] = usdc.NewUSDCTokenDataReader( + tokenDataProviders[usdc.TokenMapping[chainId]] = tokendata.NewCachedReader(usdc.NewUSDCTokenDataReader( sourceChainEventClient, usdc.TokenMapping[chainId], offRampConfig.OnRamp, attestationURI, - chainId) + chainId)) } wrappedPluginFactory := NewExecutionReportingPluginFactory( diff --git a/core/services/ocr2/plugins/ccip/execution_reporting_plugin.go b/core/services/ocr2/plugins/ccip/execution_reporting_plugin.go index 57f6eba117..a3b3a77608 100644 --- a/core/services/ocr2/plugins/ccip/execution_reporting_plugin.go +++ b/core/services/ocr2/plugins/ccip/execution_reporting_plugin.go @@ -641,13 +641,14 @@ func (r *ExecutionReportingPlugin) buildBatch( func getTokenData(ctx context.Context, msg evm2EVMOnRampCCIPSendRequestedWithMeta, tokenDataProviders map[common.Address]tokendata.Reader) (tokenData [][]byte, allReady bool, err error) { for _, token := range msg.TokenAmounts { if offchainTokenDataProvider, ok := tokenDataProviders[token.Token]; ok { - ready, attestation, err2 := offchainTokenDataProvider.IsTokenDataReady(ctx, msg.SequenceNumber, msg.logIndex, msg.txHash) + attestation, err2 := offchainTokenDataProvider.ReadTokenData(ctx, msg.SequenceNumber, msg.logIndex, msg.txHash) if err2 != nil { + if errors.Is(err2, tokendata.ErrNotReady) { + return [][]byte{}, false, nil + } return [][]byte{}, false, err2 } - if !ready { - return [][]byte{}, false, nil - } + tokenData = append(tokenData, attestation) continue } diff --git a/core/services/ocr2/plugins/ccip/tokendata/cached_reader.go b/core/services/ocr2/plugins/ccip/tokendata/cached_reader.go new file mode 100644 index 0000000000..563d25c0e7 --- /dev/null +++ b/core/services/ocr2/plugins/ccip/tokendata/cached_reader.go @@ -0,0 +1,40 @@ +package tokendata + +import ( + "context" + "sync" + + "github.com/ethereum/go-ethereum/common" +) + +type CachedReader struct { + Reader + + cache map[uint64][]byte + cacheMutex sync.Mutex +} + +func NewCachedReader(reader Reader) *CachedReader { + return &CachedReader{ + Reader: reader, + cache: make(map[uint64][]byte), + } +} + +func (r *CachedReader) ReadTokenData(ctx context.Context, seqNum uint64, logIndex uint, txHash common.Hash) ([]byte, error) { + r.cacheMutex.Lock() + defer r.cacheMutex.Unlock() + + if data, ok := r.cache[seqNum]; ok { + return data, nil + } + + tokenData, err := r.Reader.ReadTokenData(ctx, seqNum, logIndex, txHash) + if err != nil { + return []byte{}, err + } + + r.cache[seqNum] = tokenData + + return tokenData, nil +} diff --git a/core/services/ocr2/plugins/ccip/tokendata/offchain_data_provider.go b/core/services/ocr2/plugins/ccip/tokendata/reader.go similarity index 56% rename from core/services/ocr2/plugins/ccip/tokendata/offchain_data_provider.go rename to core/services/ocr2/plugins/ccip/tokendata/reader.go index 4f4f2c444f..9f6c99f32b 100644 --- a/core/services/ocr2/plugins/ccip/tokendata/offchain_data_provider.go +++ b/core/services/ocr2/plugins/ccip/tokendata/reader.go @@ -2,18 +2,21 @@ package tokendata import ( "context" + "errors" "github.com/ethereum/go-ethereum/common" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller" ) +var ( + ErrNotReady = errors.New("token data not ready") +) + // Reader is an interface for fetching offchain token data type Reader interface { - // IsTokenDataReady returns true if the attestation for the given sequence number is complete - // and returns the attestation bytes if it is complete. - // Note: this function can be called many times, the implementation should cache the result. - IsTokenDataReady(ctx context.Context, seqNum uint64, logIndex uint, txHash common.Hash) (ready bool, tokenData []byte, err error) + // ReadTokenData returns the attestation bytes if ready, and throws an error if not ready. + ReadTokenData(ctx context.Context, seqNum uint64, logIndex uint, txHash common.Hash) (tokenData []byte, err error) // GetSourceLogPollerFilters returns the filters that should be used for the source chain log poller GetSourceLogPollerFilters() []logpoller.Filter diff --git a/core/services/ocr2/plugins/ccip/tokendata/usdc/usdc.go b/core/services/ocr2/plugins/ccip/tokendata/usdc/usdc.go index 6b7ea7f642..29b9355b5f 100644 --- a/core/services/ocr2/plugins/ccip/tokendata/usdc/usdc.go +++ b/core/services/ocr2/plugins/ccip/tokendata/usdc/usdc.go @@ -30,10 +30,6 @@ type TokenDataReader struct { // Cache of sequence number -> usdc message body usdcMessageHashCache map[uint64][32]byte usdcMessageHashCacheMutex sync.Mutex - - // Cache of sequence number -> attestation attempt - attestationCache map[uint64]attestationAttempt - attestationCacheMutex sync.Mutex } type attestationAttempt struct { @@ -97,37 +93,27 @@ func NewUSDCTokenDataReader(sourceChainEvents ccipdata.Reader, usdcTokenAddress, messageTransmitter: messageTransmitterMapping[sourceChainId], onRampAddress: onRampAddress, sourceToken: usdcTokenAddress, - attestationCache: make(map[uint64]attestationAttempt), usdcMessageHashCache: make(map[uint64][32]byte), } } -func (s *TokenDataReader) IsTokenDataReady(ctx context.Context, seqNum uint64, logIndex uint, txHash common.Hash) (success bool, attestation []byte, err error) { +func (s *TokenDataReader) ReadTokenData(ctx context.Context, seqNum uint64, logIndex uint, txHash common.Hash) (attestation []byte, err error) { response, err := s.getUpdatedAttestation(ctx, seqNum, logIndex, txHash) if err != nil { - return false, []byte{}, err + return []byte{}, err } if response.Status == AttestationStatusSuccess { attestationBytes, err := hex.DecodeString(response.Attestation) if err != nil { - return false, nil, fmt.Errorf("decode response attestation: %w", err) + return nil, fmt.Errorf("decode response attestation: %w", err) } - return true, attestationBytes, nil + return attestationBytes, nil } - return false, []byte{}, nil + return []byte{}, tokendata.ErrNotReady } func (s *TokenDataReader) getUpdatedAttestation(ctx context.Context, seqNum uint64, logIndex uint, txHash common.Hash) (attestationResponse, error) { - // Try to get information from cache to reduce the number of database and external calls - s.attestationCacheMutex.Lock() - defer s.attestationCacheMutex.Unlock() - attempt, ok := s.attestationCache[seqNum] - if ok && attempt.USDCAttestationResponse.Status == AttestationStatusSuccess { - // If successful, return the cached response - return attempt.USDCAttestationResponse, nil - } - messageBody, err := s.getUSDCMessageBody(ctx, seqNum, logIndex, txHash) if err != nil { return attestationResponse{}, errors.Wrap(err, "failed getting the USDC message body") @@ -135,13 +121,9 @@ func (s *TokenDataReader) getUpdatedAttestation(ctx context.Context, seqNum uint response, err := s.callAttestationApi(ctx, messageBody) if err != nil { - return attestationResponse{}, err + return attestationResponse{}, errors.Wrap(err, "failed calling usdc attestation API ") } - // Save the response in the cache - attempt.USDCAttestationResponse = response - s.attestationCache[seqNum] = attempt - return response, nil } 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 cfc43bb22f..3b23d11fdd 100644 --- a/core/services/ocr2/plugins/ccip/tokendata/usdc/usdc_test.go +++ b/core/services/ocr2/plugins/ccip/tokendata/usdc/usdc_test.go @@ -114,12 +114,11 @@ func TestUSDCService_IsAttestationComplete(t *testing.T) { require.NoError(t, err) usdcService := NewUSDCTokenDataReader(&eventsClient, mockUSDCTokenAddress, mockOnRampAddress, attestationURI, 420) - isReady, attestation, err := usdcService.IsTokenDataReady(context.Background(), seqNum, uint(logIndex), txHash) + attestation, err := usdcService.ReadTokenData(context.Background(), seqNum, uint(logIndex), txHash) require.NoError(t, err) - require.True(t, isReady) require.Equal(t, attestationBytes, attestation) - require.Equal(t, isReady, response.Status == AttestationStatusSuccess) + require.Equal(t, response.Status, AttestationStatusSuccess) } func getMockUSDCEndpoint(t *testing.T, response attestationResponse) *httptest.Server { From 8029d4c5eeb7b83f1e93a3c891023a4944f90129 Mon Sep 17 00:00:00 2001 From: Rens Rooimans Date: Wed, 20 Sep 2023 10:13:50 +0200 Subject: [PATCH 14/20] fix test, rm unused code --- .../ocr2/plugins/ccip/execution_plugin.go | 23 +++++++++++++------ .../plugins/ccip/execution_plugin_test.go | 10 ++++++-- .../ocr2/plugins/ccip/tokendata/usdc/usdc.go | 20 ++++++++++++---- 3 files changed, 39 insertions(+), 14 deletions(-) diff --git a/core/services/ocr2/plugins/ccip/execution_plugin.go b/core/services/ocr2/plugins/ccip/execution_plugin.go index 9c50d6da44..ace1c15fad 100644 --- a/core/services/ocr2/plugins/ccip/execution_plugin.go +++ b/core/services/ocr2/plugins/ccip/execution_plugin.go @@ -117,13 +117,18 @@ func NewExecutionServices(lggr logger.Logger, jb job.Job, chainSet evm.LegacyCha // Subscribe all token data providers if pluginConfig.USDCAttestationApi != "" { - attestationURI, err := url.ParseRequestURI(pluginConfig.USDCAttestationApi) - if err != nil { - return nil, errors.Wrap(err, "failed to parse USDC attestation API") + attestationURI, err2 := url.ParseRequestURI(pluginConfig.USDCAttestationApi) + if err2 != nil { + return nil, errors.Wrap(err2, "failed to parse USDC attestation API") } - tokenDataProviders[usdc.TokenMapping[chainId]] = tokendata.NewCachedReader(usdc.NewUSDCTokenDataReader( + usdcTokenAddress, err2 := usdc.GetUSDCTokenAddress(chainId) + if err2 != nil { + return nil, errors.Wrap(err2, "failed to get USDC token address") + } + + tokenDataProviders[usdcTokenAddress] = tokendata.NewCachedReader(usdc.NewUSDCTokenDataReader( sourceChainEventClient, - usdc.TokenMapping[chainId], + usdcTokenAddress, offRampConfig.OnRamp, attestationURI, chainId)) @@ -318,10 +323,14 @@ func unregisterExecutionPluginLpFilters( tokenDataProviders := make(map[common.Address]tokendata.Reader) + usdcTokenAddress, err := usdc.GetUSDCTokenAddress(chainId) + if err != nil { + return errors.Wrap(err, "failed to get USDC token address") + } // TODO the called function only uses the chainId to get the message transmitter address - tokenDataProviders[usdc.TokenMapping[chainId]] = usdc.NewUSDCTokenDataReader( + tokenDataProviders[usdcTokenAddress] = usdc.NewUSDCTokenDataReader( nil, - usdc.TokenMapping[chainId], + usdcTokenAddress, common.Address{}, nil, chainId) diff --git a/core/services/ocr2/plugins/ccip/execution_plugin_test.go b/core/services/ocr2/plugins/ccip/execution_plugin_test.go index 54e0a13964..db9a496fdf 100644 --- a/core/services/ocr2/plugins/ccip/execution_plugin_test.go +++ b/core/services/ocr2/plugins/ccip/execution_plugin_test.go @@ -7,6 +7,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" mocklp "github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller/mocks" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/mocks" @@ -14,6 +15,7 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/evm_2_evm_onramp" "github.com/smartcontractkit/chainlink/v2/core/services/job" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/testhelpers" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/tokendata/usdc" ) func TestGetExecutionPluginFilterNamesFromSpec(t *testing.T) { @@ -74,11 +76,15 @@ func TestGetExecutionPluginFilterNames(t *testing.T) { mockOnRamp, onRampAddr := testhelpers.NewFakeOnRamp(t) mockOnRamp.SetDynamicCfg(evm_2_evm_onramp.EVM2EVMOnRampDynamicConfig{PriceRegistry: srcPriceRegAddr}) + usdcMessageTransmitter, err := usdc.GetUSDCMessageTransmitterAddress(1) + require.NoError(t, err) + srcLP := mocklp.NewLogPoller(t) srcFilters := []string{ "Exec ccip sends - " + onRampAddr.String(), "Fee token added - 0xdAFea492D9c6733aE3d56B7ed1ADb60692c98bC9", "Fee token removed - 0xdAFea492D9c6733aE3d56B7ed1ADb60692c98bC9", + usdc.MESSAGE_SENT_FILTER_NAME + " - " + usdcMessageTransmitter.Hex(), } for _, f := range srcFilters { srcLP.On("UnregisterFilter", f, mock.Anything).Return(nil) @@ -97,7 +103,7 @@ func TestGetExecutionPluginFilterNames(t *testing.T) { dstLP.On("UnregisterFilter", f, mock.Anything).Return(nil) } - err := unregisterExecutionPluginLpFilters( + err = unregisterExecutionPluginLpFilters( context.Background(), srcLP, dstLP, @@ -105,7 +111,7 @@ func TestGetExecutionPluginFilterNames(t *testing.T) { evm_2_evm_offramp.EVM2EVMOffRampStaticConfig{ CommitStore: commitStoreAddr, OnRamp: onRampAddr, - SourceChainSelector: 5790810961207155433, + SourceChainSelector: 5009297550715157269, }, mockOnRamp, nil, diff --git a/core/services/ocr2/plugins/ccip/tokendata/usdc/usdc.go b/core/services/ocr2/plugins/ccip/tokendata/usdc/usdc.go index 29b9355b5f..6b41c7582a 100644 --- a/core/services/ocr2/plugins/ccip/tokendata/usdc/usdc.go +++ b/core/services/ocr2/plugins/ccip/tokendata/usdc/usdc.go @@ -32,10 +32,6 @@ type TokenDataReader struct { usdcMessageHashCacheMutex sync.Mutex } -type attestationAttempt struct { - USDCAttestationResponse attestationResponse -} - type attestationResponse struct { Status AttestationStatus `json:"status"` Attestation string `json:"attestation"` @@ -43,7 +39,7 @@ type attestationResponse struct { // Hard coded mapping of chain id to USDC token addresses // Will be removed in favour of more flexible solution. -var TokenMapping = map[uint64]common.Address{ +var tokenMapping = map[uint64]common.Address{ // Mainnet 1: common.HexToAddress("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"), // Ethereum 10: common.HexToAddress("0x0b2c639c533813f4aa9d7837caf62653d097ff85"), // Optimism @@ -57,6 +53,13 @@ var TokenMapping = map[uint64]common.Address{ 421613: common.HexToAddress("0xfd064A18f3BF249cf1f87FC203E90D8f650f2d63"), // Arbitrum Goerli } +func GetUSDCTokenAddress(chain uint64) (common.Address, error) { + if tokenAddress, ok := tokenMapping[chain]; ok { + return tokenAddress, nil + } + return common.Address{}, errors.New("token not found") +} + var messageTransmitterMapping = map[uint64]common.Address{ // Mainnet 1: common.HexToAddress("0x0a992d191deec32afe36203ad87d7d289a738f81"), // Ethereum @@ -71,6 +74,13 @@ var messageTransmitterMapping = map[uint64]common.Address{ 421613: common.HexToAddress("0x109bc137cb64eab7c0b1dddd1edf341467dc2d35"), // Arbitrum Goerli } +func GetUSDCMessageTransmitterAddress(chain uint64) (common.Address, error) { + if transmitterAddress, ok := messageTransmitterMapping[chain]; ok { + return transmitterAddress, nil + } + return common.Address{}, errors.New("usdc transmitter not found") +} + const ( version = "v1" attestationPath = "attestations" From 6e311248ab507697ea7cd806009c6c1e6d76d910 Mon Sep 17 00:00:00 2001 From: Rens Rooimans Date: Wed, 20 Sep 2023 10:54:29 +0200 Subject: [PATCH 15/20] pass in internal.EVM2EVMOnRampCCIPSendRequestedWithMeta --- .../ccip/execution_reporting_plugin.go | 41 +++----- .../ccip/execution_reporting_plugin_test.go | 99 ++++++++++--------- .../ocr2/plugins/ccip/internal/models.go | 19 ++++ .../plugins/ccip/tokendata/cached_reader.go | 10 +- .../ocr2/plugins/ccip/tokendata/reader.go | 8 +- .../ocr2/plugins/ccip/tokendata/usdc/usdc.go | 21 ++-- .../ccip/tokendata/usdc/usdc_blackbox_test.go | 96 ++++++++++++++++++ .../plugins/ccip/tokendata/usdc/usdc_test.go | 63 +----------- 8 files changed, 201 insertions(+), 156 deletions(-) create mode 100644 core/services/ocr2/plugins/ccip/internal/models.go create mode 100644 core/services/ocr2/plugins/ccip/tokendata/usdc/usdc_blackbox_test.go diff --git a/core/services/ocr2/plugins/ccip/execution_reporting_plugin.go b/core/services/ocr2/plugins/ccip/execution_reporting_plugin.go index a3b3a77608..0f33d85c63 100644 --- a/core/services/ocr2/plugins/ccip/execution_reporting_plugin.go +++ b/core/services/ocr2/plugins/ccip/execution_reporting_plugin.go @@ -31,6 +31,7 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/logger" "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" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/cache" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipdata" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/hashlib" @@ -233,16 +234,6 @@ func (rf *ExecutionReportingPluginFactory) UpdateLogPollerFilters(destPriceRegis return nil } -// helper struct to hold the send request and some metadata -type evm2EVMOnRampCCIPSendRequestedWithMeta struct { - evm_2_evm_offramp.InternalEVM2EVMMessage - blockTimestamp time.Time - executed bool - finalized bool - logIndex uint - txHash common.Hash -} - func (r *ExecutionReportingPlugin) getExecutableObservations(ctx context.Context, lggr logger.Logger, timestamp types.ReportTimestamp, inflight []InflightInternalExecutionReport) ([]ObservedMessage, error) { unexpiredReports, err := getUnexpiredCommitReports( ctx, @@ -500,7 +491,7 @@ func (r *ExecutionReportingPlugin) buildBatch( for _, msg := range report.sendRequestsWithMeta { msgLggr := lggr.With("messageID", hexutil.Encode(msg.MessageId[:])) - if msg.executed { + if msg.Executed { msgLggr.Infow("Skipping message already executed", "seqNr", msg.SequenceNumber) continue } @@ -590,10 +581,10 @@ func (r *ExecutionReportingPlugin) buildBatch( availableFee := big.NewInt(0).Mul(msg.FeeTokenAmount, sourceFeeTokenPrice) availableFee = availableFee.Div(availableFee, big.NewInt(1e18)) - availableFeeUsd := waitBoostedFee(time.Since(msg.blockTimestamp), availableFee, r.offchainConfig.RelativeBoostPerWaitHour) + availableFeeUsd := waitBoostedFee(time.Since(msg.BlockTimestamp), availableFee, r.offchainConfig.RelativeBoostPerWaitHour) if availableFeeUsd.Cmp(execCostUsd) < 0 { msgLggr.Infow("Insufficient remaining fee", "availableFeeUsd", availableFeeUsd, "execCostUsd", execCostUsd, - "sourceBlockTimestamp", msg.blockTimestamp, "waitTime", time.Since(msg.blockTimestamp), "boost", r.offchainConfig.RelativeBoostPerWaitHour) + "sourceBlockTimestamp", msg.BlockTimestamp, "waitTime", time.Since(msg.BlockTimestamp), "boost", r.offchainConfig.RelativeBoostPerWaitHour) continue } @@ -638,10 +629,10 @@ func (r *ExecutionReportingPlugin) buildBatch( return executableMessages } -func getTokenData(ctx context.Context, msg evm2EVMOnRampCCIPSendRequestedWithMeta, tokenDataProviders map[common.Address]tokendata.Reader) (tokenData [][]byte, allReady bool, err error) { +func getTokenData(ctx context.Context, msg internal.EVM2EVMOnRampCCIPSendRequestedWithMeta, tokenDataProviders map[common.Address]tokendata.Reader) (tokenData [][]byte, allReady bool, err error) { for _, token := range msg.TokenAmounts { if offchainTokenDataProvider, ok := tokenDataProviders[token.Token]; ok { - attestation, err2 := offchainTokenDataProvider.ReadTokenData(ctx, msg.SequenceNumber, msg.logIndex, msg.txHash) + attestation, err2 := offchainTokenDataProvider.ReadTokenData(ctx, msg) if err2 != nil { if errors.Is(err2, tokendata.ErrNotReady) { return [][]byte{}, false, nil @@ -726,7 +717,7 @@ func calculateMessageMaxGas(gasLimit *big.Int, numRequests, dataLen, numTokens i // helper struct to hold the commitReport and the related send requests type commitReportWithSendRequests struct { commitReport commit_store.CommitStoreCommitReport - sendRequestsWithMeta []evm2EVMOnRampCCIPSendRequestedWithMeta + sendRequestsWithMeta []internal.EVM2EVMOnRampCCIPSendRequestedWithMeta } func (r *commitReportWithSendRequests) validate() error { @@ -741,7 +732,7 @@ func (r *commitReportWithSendRequests) validate() error { func (r *commitReportWithSendRequests) allRequestsAreExecutedAndFinalized() bool { for _, req := range r.sendRequestsWithMeta { - if !req.executed || !req.finalized { + if !req.Executed || !req.Finalized { return false } } @@ -749,7 +740,7 @@ func (r *commitReportWithSendRequests) allRequestsAreExecutedAndFinalized() bool } // checks if the send request fits the commit report interval -func (r *commitReportWithSendRequests) sendReqFits(sendReq evm2EVMOnRampCCIPSendRequestedWithMeta) bool { +func (r *commitReportWithSendRequests) sendReqFits(sendReq internal.EVM2EVMOnRampCCIPSendRequestedWithMeta) bool { return sendReq.SequenceNumber >= r.commitReport.Interval.Min && sendReq.SequenceNumber <= r.commitReport.Interval.Max } @@ -818,7 +809,7 @@ func (r *ExecutionReportingPlugin) getReportsWithSendRequests( for i, report := range reports { reportsWithSendReqs[i] = commitReportWithSendRequests{ commitReport: report, - sendRequestsWithMeta: make([]evm2EVMOnRampCCIPSendRequestedWithMeta, 0, report.Interval.Max-report.Interval.Min+1), + sendRequestsWithMeta: make([]internal.EVM2EVMOnRampCCIPSendRequestedWithMeta, 0, report.Interval.Max-report.Interval.Min+1), } } @@ -829,13 +820,13 @@ func (r *ExecutionReportingPlugin) getReportsWithSendRequests( // if value exists, and it's true then it's considered finalized finalized, executed := executedSeqNums[msg.SequenceNumber] - reqWithMeta := evm2EVMOnRampCCIPSendRequestedWithMeta{ + reqWithMeta := internal.EVM2EVMOnRampCCIPSendRequestedWithMeta{ InternalEVM2EVMMessage: msg, - blockTimestamp: sendReq.BlockTimestamp, - executed: executed, - finalized: finalized, - logIndex: sendReq.Data.Raw.Index, - txHash: sendReq.Data.Raw.TxHash, + BlockTimestamp: sendReq.BlockTimestamp, + Executed: executed, + Finalized: finalized, + LogIndex: sendReq.Data.Raw.Index, + TxHash: sendReq.Data.Raw.TxHash, } // attach the msg to the appropriate reports 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 61354a89f3..f6ef4ff6de 100644 --- a/core/services/ocr2/plugins/ccip/execution_reporting_plugin_test.go +++ b/core/services/ocr2/plugins/ccip/execution_reporting_plugin_test.go @@ -36,6 +36,7 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/logger" "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" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/cache" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipdata" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/testhelpers" @@ -434,7 +435,7 @@ func TestExecutionReportingPlugin_buildBatch(t *testing.T) { lggr: logger.TestLogger(t), } - msg1 := evm2EVMOnRampCCIPSendRequestedWithMeta{ + msg1 := internal.EVM2EVMOnRampCCIPSendRequestedWithMeta{ InternalEVM2EVMMessage: evm_2_evm_offramp.InternalEVM2EVMMessage{ SequenceNumber: 1, FeeTokenAmount: big.NewInt(1e9), @@ -448,15 +449,15 @@ func TestExecutionReportingPlugin_buildBatch(t *testing.T) { FeeToken: srcNative, MessageId: [32]byte{}, }, - blockTimestamp: time.Date(2010, 1, 1, 12, 12, 12, 0, time.UTC), + BlockTimestamp: time.Date(2010, 1, 1, 12, 12, 12, 0, time.UTC), } msg2 := msg1 - msg2.executed = true + msg2.Executed = true msg3 := msg1 - msg3.executed = true - msg3.finalized = true + msg3.Executed = true + msg3.Finalized = true msg4 := msg1 msg4.TokenAmounts = []evm_2_evm_offramp.ClientEVMTokenAmount{ @@ -469,7 +470,7 @@ func TestExecutionReportingPlugin_buildBatch(t *testing.T) { var tt = []struct { name string - reqs []evm2EVMOnRampCCIPSendRequestedWithMeta + reqs []internal.EVM2EVMOnRampCCIPSendRequestedWithMeta inflight []InflightInternalExecutionReport tokenLimit, destGasPrice *big.Int srcPrices, dstPrices map[common.Address]*big.Int @@ -480,7 +481,7 @@ func TestExecutionReportingPlugin_buildBatch(t *testing.T) { }{ { name: "single message no tokens", - reqs: []evm2EVMOnRampCCIPSendRequestedWithMeta{msg1}, + reqs: []internal.EVM2EVMOnRampCCIPSendRequestedWithMeta{msg1}, inflight: []InflightInternalExecutionReport{}, tokenLimit: big.NewInt(0), destGasPrice: big.NewInt(10), @@ -491,7 +492,7 @@ func TestExecutionReportingPlugin_buildBatch(t *testing.T) { }, { name: "executed non finalized messages should be skipped", - reqs: []evm2EVMOnRampCCIPSendRequestedWithMeta{msg2}, + reqs: []internal.EVM2EVMOnRampCCIPSendRequestedWithMeta{msg2}, inflight: []InflightInternalExecutionReport{}, tokenLimit: big.NewInt(0), destGasPrice: big.NewInt(10), @@ -502,7 +503,7 @@ func TestExecutionReportingPlugin_buildBatch(t *testing.T) { }, { name: "finalized executed log", - reqs: []evm2EVMOnRampCCIPSendRequestedWithMeta{msg3}, + reqs: []internal.EVM2EVMOnRampCCIPSendRequestedWithMeta{msg3}, inflight: []InflightInternalExecutionReport{}, tokenLimit: big.NewInt(0), destGasPrice: big.NewInt(10), @@ -513,7 +514,7 @@ func TestExecutionReportingPlugin_buildBatch(t *testing.T) { }, { name: "dst token price does not exist", - reqs: []evm2EVMOnRampCCIPSendRequestedWithMeta{msg2}, + reqs: []internal.EVM2EVMOnRampCCIPSendRequestedWithMeta{msg2}, inflight: []InflightInternalExecutionReport{}, tokenLimit: big.NewInt(0), destGasPrice: big.NewInt(10), @@ -524,7 +525,7 @@ func TestExecutionReportingPlugin_buildBatch(t *testing.T) { }, { name: "src token price does not exist", - reqs: []evm2EVMOnRampCCIPSendRequestedWithMeta{msg2}, + reqs: []internal.EVM2EVMOnRampCCIPSendRequestedWithMeta{msg2}, inflight: []InflightInternalExecutionReport{}, tokenLimit: big.NewInt(0), destGasPrice: big.NewInt(10), @@ -535,7 +536,7 @@ func TestExecutionReportingPlugin_buildBatch(t *testing.T) { }, { name: "rate limit hit", - reqs: []evm2EVMOnRampCCIPSendRequestedWithMeta{msg4}, + reqs: []internal.EVM2EVMOnRampCCIPSendRequestedWithMeta{msg4}, tokenLimit: big.NewInt(0), destGasPrice: big.NewInt(10), srcPrices: map[common.Address]*big.Int{srcNative: big.NewInt(1)}, @@ -551,7 +552,7 @@ func TestExecutionReportingPlugin_buildBatch(t *testing.T) { }, { name: "message with tokens is not executed if limit is reached", - reqs: []evm2EVMOnRampCCIPSendRequestedWithMeta{msg4}, + reqs: []internal.EVM2EVMOnRampCCIPSendRequestedWithMeta{msg4}, inflight: []InflightInternalExecutionReport{}, tokenLimit: big.NewInt(2), destGasPrice: big.NewInt(10), @@ -565,7 +566,7 @@ func TestExecutionReportingPlugin_buildBatch(t *testing.T) { }, { name: "message with tokens is not executed if limit is reached when inflight is full", - reqs: []evm2EVMOnRampCCIPSendRequestedWithMeta{msg5}, + reqs: []internal.EVM2EVMOnRampCCIPSendRequestedWithMeta{msg5}, inflight: []InflightInternalExecutionReport{ { createdAt: time.Now(), @@ -584,7 +585,7 @@ func TestExecutionReportingPlugin_buildBatch(t *testing.T) { }, { name: "some messages skipped after hitting max batch data len", - reqs: []evm2EVMOnRampCCIPSendRequestedWithMeta{ + reqs: []internal.EVM2EVMOnRampCCIPSendRequestedWithMeta{ { InternalEVM2EVMMessage: evm_2_evm_offramp.InternalEVM2EVMMessage{ SequenceNumber: 10, @@ -596,7 +597,7 @@ func TestExecutionReportingPlugin_buildBatch(t *testing.T) { FeeToken: srcNative, MessageId: [32]byte{}, }, - blockTimestamp: time.Date(2010, 1, 1, 12, 12, 12, 0, time.UTC), + BlockTimestamp: time.Date(2010, 1, 1, 12, 12, 12, 0, time.UTC), }, { InternalEVM2EVMMessage: evm_2_evm_offramp.InternalEVM2EVMMessage{ @@ -609,7 +610,7 @@ func TestExecutionReportingPlugin_buildBatch(t *testing.T) { FeeToken: srcNative, MessageId: [32]byte{}, }, - blockTimestamp: time.Date(2010, 1, 1, 12, 12, 12, 0, time.UTC), + BlockTimestamp: time.Date(2010, 1, 1, 12, 12, 12, 0, time.UTC), }, { InternalEVM2EVMMessage: evm_2_evm_offramp.InternalEVM2EVMMessage{ @@ -622,7 +623,7 @@ func TestExecutionReportingPlugin_buildBatch(t *testing.T) { FeeToken: srcNative, MessageId: [32]byte{}, }, - blockTimestamp: time.Date(2010, 1, 1, 12, 12, 12, 0, time.UTC), + BlockTimestamp: time.Date(2010, 1, 1, 12, 12, 12, 0, time.UTC), }, }, inflight: []InflightInternalExecutionReport{}, @@ -856,7 +857,7 @@ func TestExecutionReportingPlugin_destPoolRateLimits(t *testing.T) { rateLimits, err := p.destPoolRateLimits(ctx, []commitReportWithSendRequests{ { - sendRequestsWithMeta: []evm2EVMOnRampCCIPSendRequestedWithMeta{ + sendRequestsWithMeta: []internal.EVM2EVMOnRampCCIPSendRequestedWithMeta{ { InternalEVM2EVMMessage: evm_2_evm_offramp.InternalEVM2EVMMessage{ TokenAmounts: tc.tokenAmounts, @@ -976,16 +977,16 @@ func TestExecutionReportingPlugin_getReportsWithSendRequests(t *testing.T) { Interval: commit_store.CommitStoreInterval{Min: 1, Max: 2}, MerkleRoot: [32]byte{100}, }, - sendRequestsWithMeta: []evm2EVMOnRampCCIPSendRequestedWithMeta{ + sendRequestsWithMeta: []internal.EVM2EVMOnRampCCIPSendRequestedWithMeta{ { InternalEVM2EVMMessage: evm_2_evm_offramp.InternalEVM2EVMMessage{SequenceNumber: 1}, - executed: true, - finalized: true, + Executed: true, + Finalized: true, }, { InternalEVM2EVMMessage: evm_2_evm_offramp.InternalEVM2EVMMessage{SequenceNumber: 2}, - executed: false, - finalized: false, + Executed: false, + Finalized: false, }, }, }, @@ -994,11 +995,11 @@ func TestExecutionReportingPlugin_getReportsWithSendRequests(t *testing.T) { Interval: commit_store.CommitStoreInterval{Min: 3, Max: 3}, MerkleRoot: [32]byte{200}, }, - sendRequestsWithMeta: []evm2EVMOnRampCCIPSendRequestedWithMeta{ + sendRequestsWithMeta: []internal.EVM2EVMOnRampCCIPSendRequestedWithMeta{ { InternalEVM2EVMMessage: evm_2_evm_offramp.InternalEVM2EVMMessage{SequenceNumber: 3}, - executed: false, - finalized: false, + Executed: false, + Finalized: false, }, }, }, @@ -1047,8 +1048,8 @@ func TestExecutionReportingPlugin_getReportsWithSendRequests(t *testing.T) { for i, expReport := range tc.expReports { assert.Equal(t, len(expReport.sendRequestsWithMeta), len(populatedReports[i].sendRequestsWithMeta)) for j, expReq := range expReport.sendRequestsWithMeta { - assert.Equal(t, expReq.executed, populatedReports[i].sendRequestsWithMeta[j].executed) - assert.Equal(t, expReq.finalized, populatedReports[i].sendRequestsWithMeta[j].finalized) + assert.Equal(t, expReq.Executed, populatedReports[i].sendRequestsWithMeta[j].Executed) + assert.Equal(t, expReq.Finalized, populatedReports[i].sendRequestsWithMeta[j].Finalized) assert.Equal(t, expReq.SequenceNumber, populatedReports[i].sendRequestsWithMeta[j].SequenceNumber) } } @@ -1577,7 +1578,7 @@ func Test_commitReportWithSendRequests_validate(t *testing.T) { commitReport: commit_store.CommitStoreCommitReport{ Interval: tc.reportInterval, }, - sendRequestsWithMeta: make([]evm2EVMOnRampCCIPSendRequestedWithMeta, tc.numReqs), + sendRequestsWithMeta: make([]internal.EVM2EVMOnRampCCIPSendRequestedWithMeta, tc.numReqs), } err := rep.validate() isValid := err == nil @@ -1589,38 +1590,38 @@ func Test_commitReportWithSendRequests_validate(t *testing.T) { func Test_commitReportWithSendRequests_allRequestsAreExecutedAndFinalized(t *testing.T) { testCases := []struct { name string - reqs []evm2EVMOnRampCCIPSendRequestedWithMeta + reqs []internal.EVM2EVMOnRampCCIPSendRequestedWithMeta expRes bool }{ { name: "all requests executed and finalized", - reqs: []evm2EVMOnRampCCIPSendRequestedWithMeta{ - {executed: true, finalized: true}, - {executed: true, finalized: true}, - {executed: true, finalized: true}, + reqs: []internal.EVM2EVMOnRampCCIPSendRequestedWithMeta{ + {Executed: true, Finalized: true}, + {Executed: true, Finalized: true}, + {Executed: true, Finalized: true}, }, expRes: true, }, { name: "true when there are zero requests", - reqs: []evm2EVMOnRampCCIPSendRequestedWithMeta{}, + reqs: []internal.EVM2EVMOnRampCCIPSendRequestedWithMeta{}, expRes: true, }, { name: "some request not executed", - reqs: []evm2EVMOnRampCCIPSendRequestedWithMeta{ - {executed: true, finalized: true}, - {executed: true, finalized: true}, - {executed: false, finalized: true}, + reqs: []internal.EVM2EVMOnRampCCIPSendRequestedWithMeta{ + {Executed: true, Finalized: true}, + {Executed: true, Finalized: true}, + {Executed: false, Finalized: true}, }, expRes: false, }, { name: "some request not finalized", - reqs: []evm2EVMOnRampCCIPSendRequestedWithMeta{ - {executed: true, finalized: true}, - {executed: true, finalized: true}, - {executed: true, finalized: false}, + reqs: []internal.EVM2EVMOnRampCCIPSendRequestedWithMeta{ + {Executed: true, Finalized: true}, + {Executed: true, Finalized: true}, + {Executed: true, Finalized: false}, }, expRes: false, }, @@ -1638,13 +1639,13 @@ func Test_commitReportWithSendRequests_allRequestsAreExecutedAndFinalized(t *tes func Test_commitReportWithSendRequests_sendReqFits(t *testing.T) { testCases := []struct { name string - req evm2EVMOnRampCCIPSendRequestedWithMeta + req internal.EVM2EVMOnRampCCIPSendRequestedWithMeta report commit_store.CommitStoreCommitReport expRes bool }{ { name: "all requests executed and finalized", - req: evm2EVMOnRampCCIPSendRequestedWithMeta{ + req: internal.EVM2EVMOnRampCCIPSendRequestedWithMeta{ InternalEVM2EVMMessage: evm_2_evm_offramp.InternalEVM2EVMMessage{SequenceNumber: 1}, }, report: commit_store.CommitStoreCommitReport{ @@ -1654,7 +1655,7 @@ func Test_commitReportWithSendRequests_sendReqFits(t *testing.T) { }, { name: "all requests executed and finalized", - req: evm2EVMOnRampCCIPSendRequestedWithMeta{ + req: internal.EVM2EVMOnRampCCIPSendRequestedWithMeta{ InternalEVM2EVMMessage: evm_2_evm_offramp.InternalEVM2EVMMessage{SequenceNumber: 10}, }, report: commit_store.CommitStoreCommitReport{ @@ -1664,7 +1665,7 @@ func Test_commitReportWithSendRequests_sendReqFits(t *testing.T) { }, { name: "all requests executed and finalized", - req: evm2EVMOnRampCCIPSendRequestedWithMeta{ + req: internal.EVM2EVMOnRampCCIPSendRequestedWithMeta{ InternalEVM2EVMMessage: evm_2_evm_offramp.InternalEVM2EVMMessage{SequenceNumber: 11}, }, report: commit_store.CommitStoreCommitReport{ @@ -1674,7 +1675,7 @@ func Test_commitReportWithSendRequests_sendReqFits(t *testing.T) { }, { name: "all requests executed and finalized", - req: evm2EVMOnRampCCIPSendRequestedWithMeta{ + req: internal.EVM2EVMOnRampCCIPSendRequestedWithMeta{ InternalEVM2EVMMessage: evm_2_evm_offramp.InternalEVM2EVMMessage{SequenceNumber: 10}, }, report: commit_store.CommitStoreCommitReport{ diff --git a/core/services/ocr2/plugins/ccip/internal/models.go b/core/services/ocr2/plugins/ccip/internal/models.go new file mode 100644 index 0000000000..b9eee7bea4 --- /dev/null +++ b/core/services/ocr2/plugins/ccip/internal/models.go @@ -0,0 +1,19 @@ +package internal + +import ( + "time" + + "github.com/ethereum/go-ethereum/common" + + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/evm_2_evm_offramp" +) + +// EVM2EVMOnRampCCIPSendRequestedWithMeta helper struct to hold the send request and some metadata +type EVM2EVMOnRampCCIPSendRequestedWithMeta struct { + evm_2_evm_offramp.InternalEVM2EVMMessage + BlockTimestamp time.Time + Executed bool + Finalized bool + LogIndex uint + TxHash common.Hash +} diff --git a/core/services/ocr2/plugins/ccip/tokendata/cached_reader.go b/core/services/ocr2/plugins/ccip/tokendata/cached_reader.go index 563d25c0e7..f0f9e5c3ad 100644 --- a/core/services/ocr2/plugins/ccip/tokendata/cached_reader.go +++ b/core/services/ocr2/plugins/ccip/tokendata/cached_reader.go @@ -4,7 +4,7 @@ import ( "context" "sync" - "github.com/ethereum/go-ethereum/common" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal" ) type CachedReader struct { @@ -21,20 +21,20 @@ func NewCachedReader(reader Reader) *CachedReader { } } -func (r *CachedReader) ReadTokenData(ctx context.Context, seqNum uint64, logIndex uint, txHash common.Hash) ([]byte, error) { +func (r *CachedReader) ReadTokenData(ctx context.Context, msg internal.EVM2EVMOnRampCCIPSendRequestedWithMeta) ([]byte, error) { r.cacheMutex.Lock() defer r.cacheMutex.Unlock() - if data, ok := r.cache[seqNum]; ok { + if data, ok := r.cache[msg.SequenceNumber]; ok { return data, nil } - tokenData, err := r.Reader.ReadTokenData(ctx, seqNum, logIndex, txHash) + tokenData, err := r.Reader.ReadTokenData(ctx, msg) if err != nil { return []byte{}, err } - r.cache[seqNum] = tokenData + r.cache[msg.SequenceNumber] = tokenData return tokenData, nil } diff --git a/core/services/ocr2/plugins/ccip/tokendata/reader.go b/core/services/ocr2/plugins/ccip/tokendata/reader.go index 9f6c99f32b..7cf83eca6e 100644 --- a/core/services/ocr2/plugins/ccip/tokendata/reader.go +++ b/core/services/ocr2/plugins/ccip/tokendata/reader.go @@ -4,9 +4,8 @@ import ( "context" "errors" - "github.com/ethereum/go-ethereum/common" - "github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal" ) var ( @@ -16,11 +15,8 @@ var ( // Reader is an interface for fetching offchain token data type Reader interface { // ReadTokenData returns the attestation bytes if ready, and throws an error if not ready. - ReadTokenData(ctx context.Context, seqNum uint64, logIndex uint, txHash common.Hash) (tokenData []byte, err error) + 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 - - // GetSourceToken returns the token address on the source chain - GetSourceToken() common.Address } diff --git a/core/services/ocr2/plugins/ccip/tokendata/usdc/usdc.go b/core/services/ocr2/plugins/ccip/tokendata/usdc/usdc.go index 6b41c7582a..42479790c9 100644 --- a/core/services/ocr2/plugins/ccip/tokendata/usdc/usdc.go +++ b/core/services/ocr2/plugins/ccip/tokendata/usdc/usdc.go @@ -15,6 +15,7 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller" "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" "github.com/smartcontractkit/chainlink/v2/core/utils" @@ -107,8 +108,8 @@ func NewUSDCTokenDataReader(sourceChainEvents ccipdata.Reader, usdcTokenAddress, } } -func (s *TokenDataReader) ReadTokenData(ctx context.Context, seqNum uint64, logIndex uint, txHash common.Hash) (attestation []byte, err error) { - response, err := s.getUpdatedAttestation(ctx, seqNum, logIndex, txHash) +func (s *TokenDataReader) ReadTokenData(ctx context.Context, msg internal.EVM2EVMOnRampCCIPSendRequestedWithMeta) (attestation []byte, err error) { + response, err := s.getUpdatedAttestation(ctx, msg) if err != nil { return []byte{}, err } @@ -123,8 +124,8 @@ func (s *TokenDataReader) ReadTokenData(ctx context.Context, seqNum uint64, logI return []byte{}, tokendata.ErrNotReady } -func (s *TokenDataReader) getUpdatedAttestation(ctx context.Context, seqNum uint64, logIndex uint, txHash common.Hash) (attestationResponse, error) { - messageBody, err := s.getUSDCMessageBody(ctx, seqNum, logIndex, txHash) +func (s *TokenDataReader) getUpdatedAttestation(ctx context.Context, msg internal.EVM2EVMOnRampCCIPSendRequestedWithMeta) (attestationResponse, error) { + messageBody, err := s.getUSDCMessageBody(ctx, msg) if err != nil { return attestationResponse{}, errors.Wrap(err, "failed getting the USDC message body") } @@ -137,15 +138,15 @@ func (s *TokenDataReader) getUpdatedAttestation(ctx context.Context, seqNum uint return response, nil } -func (s *TokenDataReader) getUSDCMessageBody(ctx context.Context, seqNum uint64, logIndex uint, txHash common.Hash) ([32]byte, error) { +func (s *TokenDataReader) getUSDCMessageBody(ctx context.Context, msg internal.EVM2EVMOnRampCCIPSendRequestedWithMeta) ([32]byte, error) { s.usdcMessageHashCacheMutex.Lock() defer s.usdcMessageHashCacheMutex.Unlock() - if body, ok := s.usdcMessageHashCache[seqNum]; ok { + if body, ok := s.usdcMessageHashCache[msg.SequenceNumber]; ok { return body, nil } - usdcMessageBody, err := s.sourceChainEvents.GetLastUSDCMessagePriorToLogIndexInTx(ctx, int64(logIndex), txHash) + usdcMessageBody, err := s.sourceChainEvents.GetLastUSDCMessagePriorToLogIndexInTx(ctx, int64(msg.LogIndex), msg.TxHash) if err != nil { return [32]byte{}, err } @@ -153,7 +154,7 @@ func (s *TokenDataReader) getUSDCMessageBody(ctx context.Context, seqNum uint64, msgBodyHash := utils.Keccak256Fixed(usdcMessageBody) // Save the attempt in the cache in case the external call fails - s.usdcMessageHashCache[seqNum] = msgBodyHash + s.usdcMessageHashCache[msg.SequenceNumber] = msgBodyHash return msgBodyHash, nil } @@ -182,10 +183,6 @@ func (s *TokenDataReader) callAttestationApi(ctx context.Context, usdcMessageHas return response, nil } -func (s *TokenDataReader) GetSourceToken() common.Address { - return s.sourceToken -} - func (s *TokenDataReader) GetSourceLogPollerFilters() []logpoller.Filter { return []logpoller.Filter{ { 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 new file mode 100644 index 0000000000..8142bf4e47 --- /dev/null +++ b/core/services/ocr2/plugins/ccip/tokendata/usdc/usdc_blackbox_test.go @@ -0,0 +1,96 @@ +package usdc_test + +import ( + "context" + "encoding/hex" + "encoding/json" + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/ethereum/go-ethereum/common" + "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/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() +) + +type attestationResponse struct { + Status usdc.AttestationStatus `json:"status"` + Attestation string `json:"attestation"` +} + +func TestUSDCReader_ReadTokenData(t *testing.T) { + response := attestationResponse{ + Status: usdc.AttestationStatusSuccess, + Attestation: "720502893578a89a8a87982982ef781c18b193", + } + + attestationBytes, err := hex.DecodeString(response.Attestation) + require.NoError(t, err) + + responseBytes, err := json.Marshal(response) + require.NoError(t, err) + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, err := w.Write(responseBytes) + require.NoError(t, err) + })) + + defer ts.Close() + + seqNum := uint64(23825) + 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", + mock.Anything, + logIndex, + common.Hash(txHash), + ).Return(attestationBytes, nil) + attestationURI, err := url.ParseRequestURI(ts.URL) + require.NoError(t, err) + + usdcService := usdc.NewUSDCTokenDataReader(&eventsClient, mockUSDCTokenAddress, mockOnRampAddress, attestationURI, 420) + attestation, err := usdcService.ReadTokenData(context.Background(), internal.EVM2EVMOnRampCCIPSendRequestedWithMeta{ + InternalEVM2EVMMessage: evm_2_evm_offramp.InternalEVM2EVMMessage{ + SequenceNumber: seqNum, + }, + TxHash: txHash, + LogIndex: uint(logIndex), + }) + require.NoError(t, err) + + require.Equal(t, attestationBytes, attestation) + require.Equal(t, response.Status, usdc.AttestationStatusSuccess) +} 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 3b23d11fdd..eb4b4df838 100644 --- a/core/services/ocr2/plugins/ccip/tokendata/usdc/usdc_test.go +++ b/core/services/ocr2/plugins/ccip/tokendata/usdc/usdc_test.go @@ -2,7 +2,6 @@ package usdc import ( "context" - "encoding/hex" "encoding/json" "net/http" "net/http/httptest" @@ -10,13 +9,9 @@ import ( "testing" "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core/types" - "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/gethwrappers/ccip/generated/evm_2_evm_onramp" - "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipdata" "github.com/smartcontractkit/chainlink/v2/core/utils" ) @@ -25,7 +20,7 @@ var ( mockUSDCTokenAddress = utils.RandomAddress() ) -func TestUSDCService_callAttestationApi(t *testing.T) { +func TestUSDCReader_callAttestationApi(t *testing.T) { t.Skipf("Skipping test because it uses the real USDC attestation API") usdcMessageHash := "912f22a13e9ccb979b621500f6952b2afd6e75be7eadaed93fc2625fe11c52a2" attestationURI, err := url.ParseRequestURI("https://iris-api-sandbox.circle.com") @@ -39,7 +34,7 @@ func TestUSDCService_callAttestationApi(t *testing.T) { require.Equal(t, "PENDING", attestation.Attestation) } -func TestUSDCService_callAttestationApiMock(t *testing.T) { +func TestUSDCReader_callAttestationApiMock(t *testing.T) { response := attestationResponse{ Status: AttestationStatusSuccess, Attestation: "720502893578a89a8a87982982ef781c18b193", @@ -58,7 +53,7 @@ func TestUSDCService_callAttestationApiMock(t *testing.T) { require.Equal(t, response.Attestation, attestation.Attestation) } -func TestUSDCService_callAttestationApiMockError(t *testing.T) { +func TestUSDCReader_callAttestationApiMockError(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusInternalServerError) })) @@ -71,56 +66,6 @@ func TestUSDCService_callAttestationApiMockError(t *testing.T) { require.Error(t, err) } -func TestUSDCService_IsAttestationComplete(t *testing.T) { - response := attestationResponse{ - Status: AttestationStatusSuccess, - Attestation: "720502893578a89a8a87982982ef781c18b193", - } - - attestationBytes, err := hex.DecodeString(response.Attestation) - require.NoError(t, err) - - ts := getMockUSDCEndpoint(t, response) - defer ts.Close() - - seqNum := uint64(23825) - 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", - mock.Anything, - logIndex, - common.Hash(txHash), - ).Return(attestationBytes, nil) - attestationURI, err := url.ParseRequestURI(ts.URL) - require.NoError(t, err) - - usdcService := NewUSDCTokenDataReader(&eventsClient, mockUSDCTokenAddress, mockOnRampAddress, attestationURI, 420) - attestation, err := usdcService.ReadTokenData(context.Background(), seqNum, uint(logIndex), txHash) - require.NoError(t, err) - - require.Equal(t, attestationBytes, attestation) - require.Equal(t, response.Status, AttestationStatusSuccess) -} - func getMockUSDCEndpoint(t *testing.T, response attestationResponse) *httptest.Server { responseBytes, err := json.Marshal(response) require.NoError(t, err) @@ -132,7 +77,7 @@ func getMockUSDCEndpoint(t *testing.T, response attestationResponse) *httptest.S } // Asserts the hard coded event signature matches Keccak256("MessageSent(bytes)") -func TestGetUSDCServiceSourceLPFilters(t *testing.T) { +func TestGetUSDCReaderSourceLPFilters(t *testing.T) { chainId := uint64(420) usdcService := NewUSDCTokenDataReader(nil, mockUSDCTokenAddress, mockOnRampAddress, nil, chainId) From 0e80c67e5b70593859e5220218fef748c788e6e3 Mon Sep 17 00:00:00 2001 From: Rens Rooimans Date: Wed, 20 Sep 2023 11:07:59 +0200 Subject: [PATCH 16/20] extract chains logic and fix linting --- .../plugins/ccip/internal/chains/chains.go | 15 +++++++ .../ocr2/plugins/ccip/tokendata/usdc/usdc.go | 43 ++++++++++--------- .../ccip/tokendata/usdc/usdc_blackbox_test.go | 2 +- .../plugins/ccip/tokendata/usdc/usdc_test.go | 3 +- 4 files changed, 40 insertions(+), 23 deletions(-) create mode 100644 core/services/ocr2/plugins/ccip/internal/chains/chains.go diff --git a/core/services/ocr2/plugins/ccip/internal/chains/chains.go b/core/services/ocr2/plugins/ccip/internal/chains/chains.go new file mode 100644 index 0000000000..33cf377bf9 --- /dev/null +++ b/core/services/ocr2/plugins/ccip/internal/chains/chains.go @@ -0,0 +1,15 @@ +package chains + +type EVMChain uint64 + +const ( + Ethereum EVMChain = 1 + Optimism EVMChain = 10 + Arbitrum EVMChain = 42161 + Avalanche EVMChain = 43114 + + GoerliTestnet EVMChain = 5 + OptimismGoerliTestnet EVMChain = 420 + AvalancheFujiTestnet EVMChain = 43113 + ArbitrumGoerliTestnet EVMChain = 421613 +) diff --git a/core/services/ocr2/plugins/ccip/tokendata/usdc/usdc.go b/core/services/ocr2/plugins/ccip/tokendata/usdc/usdc.go index 42479790c9..a896c7434e 100644 --- a/core/services/ocr2/plugins/ccip/tokendata/usdc/usdc.go +++ b/core/services/ocr2/plugins/ccip/tokendata/usdc/usdc.go @@ -17,6 +17,7 @@ import ( "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/internal/chains" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/tokendata" "github.com/smartcontractkit/chainlink/v2/core/utils" ) @@ -40,43 +41,43 @@ type attestationResponse struct { // Hard coded mapping of chain id to USDC token addresses // Will be removed in favour of more flexible solution. -var tokenMapping = map[uint64]common.Address{ +var tokenMapping = map[chains.EVMChain]common.Address{ // Mainnet - 1: common.HexToAddress("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"), // Ethereum - 10: common.HexToAddress("0x0b2c639c533813f4aa9d7837caf62653d097ff85"), // Optimism - 42161: common.HexToAddress("0xaf88d065e77c8cc2239327c5edb3a432268e5831"), // Arbitrum - 43114: common.HexToAddress("0xb97ef9ef8734c71904d8002f8b6bc66dd9c48a6e"), // Avalanche + chains.Ethereum: common.HexToAddress("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"), + chains.Optimism: common.HexToAddress("0x0b2c639c533813f4aa9d7837caf62653d097ff85"), + chains.Arbitrum: common.HexToAddress("0xaf88d065e77c8cc2239327c5edb3a432268e5831"), + chains.Avalanche: common.HexToAddress("0xb97ef9ef8734c71904d8002f8b6bc66dd9c48a6e"), // Testnets - 5: common.HexToAddress("0xca6b4c00831ffb77afe22e734a6101b268b7fcbe"), // Goerli - 420: common.HexToAddress("0xe05606174bac4A6364B31bd0eCA4bf4dD368f8C6"), // Optimism Goerli - 43113: common.HexToAddress("0x5425890298aed601595a70ab815c96711a31bc65"), // Avalanche Fuji - 421613: common.HexToAddress("0xfd064A18f3BF249cf1f87FC203E90D8f650f2d63"), // Arbitrum Goerli + chains.GoerliTestnet: common.HexToAddress("0xca6b4c00831ffb77afe22e734a6101b268b7fcbe"), + chains.OptimismGoerliTestnet: common.HexToAddress("0xe05606174bac4A6364B31bd0eCA4bf4dD368f8C6"), + chains.AvalancheFujiTestnet: common.HexToAddress("0x5425890298aed601595a70ab815c96711a31bc65"), + chains.ArbitrumGoerliTestnet: common.HexToAddress("0xfd064A18f3BF249cf1f87FC203E90D8f650f2d63"), } func GetUSDCTokenAddress(chain uint64) (common.Address, error) { - if tokenAddress, ok := tokenMapping[chain]; ok { + if tokenAddress, ok := tokenMapping[chains.EVMChain(chain)]; ok { return tokenAddress, nil } return common.Address{}, errors.New("token not found") } -var messageTransmitterMapping = map[uint64]common.Address{ +var messageTransmitterMapping = map[chains.EVMChain]common.Address{ // Mainnet - 1: common.HexToAddress("0x0a992d191deec32afe36203ad87d7d289a738f81"), // Ethereum - 10: common.HexToAddress("0x4d41f22c5a0e5c74090899e5a8fb597a8842b3e8"), // Optimism - 42161: common.HexToAddress("0xC30362313FBBA5cf9163F0bb16a0e01f01A896ca"), // Arbitrum - 43114: common.HexToAddress("0x8186359af5f57fbb40c6b14a588d2a59c0c29880"), // Avalanche + chains.Ethereum: common.HexToAddress("0x0a992d191deec32afe36203ad87d7d289a738f81"), + chains.Optimism: common.HexToAddress("0x4d41f22c5a0e5c74090899e5a8fb597a8842b3e8"), + chains.Arbitrum: common.HexToAddress("0xC30362313FBBA5cf9163F0bb16a0e01f01A896ca"), + chains.Avalanche: common.HexToAddress("0x8186359af5f57fbb40c6b14a588d2a59c0c29880"), // Testnets - 5: common.HexToAddress("0xca6b4c00831ffb77afe22e734a6101b268b7fcbe"), // Goerli - 420: common.HexToAddress("0x9ff9a4da6f2157a9c82ce756f8fd7e0d75be8895"), // Optimism Goerli - 43113: common.HexToAddress("0xa9fb1b3009dcb79e2fe346c16a604b8fa8ae0a79"), // Avalanche Fuji - 421613: common.HexToAddress("0x109bc137cb64eab7c0b1dddd1edf341467dc2d35"), // Arbitrum Goerli + chains.GoerliTestnet: common.HexToAddress("0xca6b4c00831ffb77afe22e734a6101b268b7fcbe"), + chains.OptimismGoerliTestnet: common.HexToAddress("0x9ff9a4da6f2157a9c82ce756f8fd7e0d75be8895"), + chains.AvalancheFujiTestnet: common.HexToAddress("0xa9fb1b3009dcb79e2fe346c16a604b8fa8ae0a79"), + chains.ArbitrumGoerliTestnet: common.HexToAddress("0x109bc137cb64eab7c0b1dddd1edf341467dc2d35"), } func GetUSDCMessageTransmitterAddress(chain uint64) (common.Address, error) { - if transmitterAddress, ok := messageTransmitterMapping[chain]; ok { + if transmitterAddress, ok := messageTransmitterMapping[chains.EVMChain(chain)]; ok { return transmitterAddress, nil } return common.Address{}, errors.New("usdc transmitter not found") @@ -101,7 +102,7 @@ func NewUSDCTokenDataReader(sourceChainEvents ccipdata.Reader, usdcTokenAddress, return &TokenDataReader{ sourceChainEvents: sourceChainEvents, attestationApi: usdcAttestationApi, - messageTransmitter: messageTransmitterMapping[sourceChainId], + messageTransmitter: messageTransmitterMapping[chains.EVMChain(sourceChainId)], onRampAddress: onRampAddress, sourceToken: usdcTokenAddress, usdcMessageHashCache: make(map[uint64][32]byte), 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 8142bf4e47..2dbc97035e 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 @@ -45,7 +45,7 @@ func TestUSDCReader_ReadTokenData(t *testing.T) { require.NoError(t, err) ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - _, err := w.Write(responseBytes) + _, err = w.Write(responseBytes) require.NoError(t, err) })) 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 eb4b4df838..56b2c14bc7 100644 --- a/core/services/ocr2/plugins/ccip/tokendata/usdc/usdc_test.go +++ b/core/services/ocr2/plugins/ccip/tokendata/usdc/usdc_test.go @@ -82,7 +82,8 @@ func TestGetUSDCReaderSourceLPFilters(t *testing.T) { usdcService := NewUSDCTokenDataReader(nil, mockUSDCTokenAddress, mockOnRampAddress, nil, chainId) filters := usdcService.GetSourceLogPollerFilters() - expectedTransmitterAddress := messageTransmitterMapping[chainId] + expectedTransmitterAddress, err := GetUSDCMessageTransmitterAddress(chainId) + require.NoError(t, err) require.Equal(t, 1, len(filters)) filter := filters[0] From 7be78f64c435a6b52d5348648bf35e8e8ad73ab2 Mon Sep 17 00:00:00 2001 From: Rens Rooimans Date: Wed, 20 Sep 2023 15:53:34 +0200 Subject: [PATCH 17/20] add cached reader test & reader mock --- .../ccip/tokendata/chached_reader_test.go | 35 +++++++++ .../ocr2/plugins/ccip/tokendata/reader.go | 2 + .../plugins/ccip/tokendata/reader_mock.go | 74 +++++++++++++++++++ 3 files changed, 111 insertions(+) create mode 100644 core/services/ocr2/plugins/ccip/tokendata/chached_reader_test.go create mode 100644 core/services/ocr2/plugins/ccip/tokendata/reader_mock.go diff --git a/core/services/ocr2/plugins/ccip/tokendata/chached_reader_test.go b/core/services/ocr2/plugins/ccip/tokendata/chached_reader_test.go new file mode 100644 index 0000000000..c63027efe6 --- /dev/null +++ b/core/services/ocr2/plugins/ccip/tokendata/chached_reader_test.go @@ -0,0 +1,35 @@ +package tokendata + +import ( + "testing" + + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal" +) + +func TestCachedReader_ReadTokenData(t *testing.T) { + mockReader := MockReader{} + cachedReader := NewCachedReader(&mockReader) + + msgData := []byte("msgData") + mockReader.On("ReadTokenData", mock.Anything, mock.Anything).Return(msgData, nil) + + msg := internal.EVM2EVMOnRampCCIPSendRequestedWithMeta{} + + // Call ReadTokenData twice, expect only one call to underlying reader + data, err := cachedReader.ReadTokenData(nil, msg) + require.NoError(t, err) + require.Equal(t, msgData, data) + + // First time, calls the underlying reader + mockReader.AssertNumberOfCalls(t, "ReadTokenData", 1) + + data, err = cachedReader.ReadTokenData(nil, msg) + require.NoError(t, err) + require.Equal(t, msgData, data) + + // Second time, get data from cache + mockReader.AssertNumberOfCalls(t, "ReadTokenData", 1) +} diff --git a/core/services/ocr2/plugins/ccip/tokendata/reader.go b/core/services/ocr2/plugins/ccip/tokendata/reader.go index 7cf83eca6e..ec3a2d7497 100644 --- a/core/services/ocr2/plugins/ccip/tokendata/reader.go +++ b/core/services/ocr2/plugins/ccip/tokendata/reader.go @@ -13,6 +13,8 @@ var ( ) // Reader is an interface for fetching offchain token data +// +//go:generate mockery --quiet --name Reader --output . --filename reader_mock.go --inpackage --case=underscore 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) diff --git a/core/services/ocr2/plugins/ccip/tokendata/reader_mock.go b/core/services/ocr2/plugins/ccip/tokendata/reader_mock.go new file mode 100644 index 0000000000..9a87fafb34 --- /dev/null +++ b/core/services/ocr2/plugins/ccip/tokendata/reader_mock.go @@ -0,0 +1,74 @@ +// Code generated by mockery v2.28.1. DO NOT EDIT. + +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" +) + +// MockReader is an autogenerated mock type for the Reader type +type MockReader struct { + mock.Mock +} + +// GetSourceLogPollerFilters provides a mock function with given fields: +func (_m *MockReader) GetSourceLogPollerFilters() []logpoller.Filter { + ret := _m.Called() + + var r0 []logpoller.Filter + if rf, ok := ret.Get(0).(func() []logpoller.Filter); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]logpoller.Filter) + } + } + + return r0 +} + +// ReadTokenData provides a mock function with given fields: ctx, msg +func (_m *MockReader) ReadTokenData(ctx context.Context, msg internal.EVM2EVMOnRampCCIPSendRequestedWithMeta) ([]byte, error) { + ret := _m.Called(ctx, msg) + + var r0 []byte + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, internal.EVM2EVMOnRampCCIPSendRequestedWithMeta) ([]byte, error)); ok { + return rf(ctx, msg) + } + if rf, ok := ret.Get(0).(func(context.Context, internal.EVM2EVMOnRampCCIPSendRequestedWithMeta) []byte); ok { + r0 = rf(ctx, msg) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]byte) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, internal.EVM2EVMOnRampCCIPSendRequestedWithMeta) error); ok { + r1 = rf(ctx, msg) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +type mockConstructorTestingTNewMockReader interface { + mock.TestingT + Cleanup(func()) +} + +// NewMockReader creates a new instance of MockReader. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewMockReader(t mockConstructorTestingTNewMockReader) *MockReader { + mock := &MockReader{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} From 142a6d1ddb9fbc17d27f3fab67e3edc3713bbb07 Mon Sep 17 00:00:00 2001 From: Rens Rooimans Date: Wed, 20 Sep 2023 16:21:58 +0200 Subject: [PATCH 18/20] more tests, real context and extract into method --- .../ocr2/plugins/ccip/execution_plugin.go | 46 +++++++++++-------- .../ccip/execution_reporting_plugin.go | 4 +- .../ccip/execution_reporting_plugin_test.go | 1 + .../plugins/ccip/tokendata/cached_reader.go | 15 ++++-- .../ccip/tokendata/chached_reader_test.go | 14 ++++-- .../plugins/ccip/tokendata/usdc/usdc_test.go | 27 +++++++++++ 6 files changed, 78 insertions(+), 29 deletions(-) diff --git a/core/services/ocr2/plugins/ccip/execution_plugin.go b/core/services/ocr2/plugins/ccip/execution_plugin.go index ace1c15fad..cb49846d9b 100644 --- a/core/services/ocr2/plugins/ccip/execution_plugin.go +++ b/core/services/ocr2/plugins/ccip/execution_plugin.go @@ -113,25 +113,9 @@ func NewExecutionServices(lggr logger.Logger, jb job.Job, chainSet evm.LegacyCha sourceChainEventClient := ccipdata.NewLogPollerReader(sourceChain.LogPoller(), execLggr, sourceChain.Client()) - tokenDataProviders := make(map[common.Address]tokendata.Reader) - - // Subscribe all token data providers - if pluginConfig.USDCAttestationApi != "" { - attestationURI, err2 := url.ParseRequestURI(pluginConfig.USDCAttestationApi) - if err2 != nil { - return nil, errors.Wrap(err2, "failed to parse USDC attestation API") - } - usdcTokenAddress, err2 := usdc.GetUSDCTokenAddress(chainId) - if err2 != nil { - return nil, errors.Wrap(err2, "failed to get USDC token address") - } - - tokenDataProviders[usdcTokenAddress] = tokendata.NewCachedReader(usdc.NewUSDCTokenDataReader( - sourceChainEventClient, - usdcTokenAddress, - offRampConfig.OnRamp, - attestationURI, - chainId)) + tokenDataProviders, err := getTokenDataProviders(pluginConfig, chainId, offRampConfig.OnRamp, sourceChainEventClient) + if err != nil { + return nil, errors.Wrap(err, "could not get token data providers") } wrappedPluginFactory := NewExecutionReportingPluginFactory( @@ -186,6 +170,30 @@ func NewExecutionServices(lggr logger.Logger, jb job.Job, chainSet evm.LegacyCha return []job.ServiceCtx{job.NewServiceAdapter(oracle)}, nil } +func getTokenDataProviders(pluginConfig ccipconfig.ExecutionPluginJobSpecConfig, sourceChainId uint64, onRampAddress common.Address, sourceChainEventClient *ccipdata.LogPollerReader) (map[common.Address]tokendata.Reader, error) { + tokenDataProviders := make(map[common.Address]tokendata.Reader) + + if pluginConfig.USDCAttestationApi != "" { + attestationURI, err2 := url.ParseRequestURI(pluginConfig.USDCAttestationApi) + if err2 != nil { + return nil, errors.Wrap(err2, "failed to parse USDC attestation API") + } + usdcTokenAddress, err2 := usdc.GetUSDCTokenAddress(sourceChainId) + if err2 != nil { + return nil, errors.Wrap(err2, "failed to get USDC token address") + } + + tokenDataProviders[usdcTokenAddress] = tokendata.NewCachedReader(usdc.NewUSDCTokenDataReader( + sourceChainEventClient, + usdcTokenAddress, + onRampAddress, + attestationURI, + sourceChainId)) + } + + 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 { diff --git a/core/services/ocr2/plugins/ccip/execution_reporting_plugin.go b/core/services/ocr2/plugins/ccip/execution_reporting_plugin.go index 0f33d85c63..f6f3aef9d0 100644 --- a/core/services/ocr2/plugins/ccip/execution_reporting_plugin.go +++ b/core/services/ocr2/plugins/ccip/execution_reporting_plugin.go @@ -358,6 +358,7 @@ func (r *ExecutionReportingPlugin) getExecutableObservations(ctx context.Context buildBatchDuration := time.Now() batch := r.buildBatch( + ctx, rootLggr, rep, inflight, @@ -470,6 +471,7 @@ func (r *ExecutionReportingPlugin) getExecutedSeqNrsInRange(ctx context.Context, // the available gas, rate limiting, execution state, nonce state, and // profitability of execution. func (r *ExecutionReportingPlugin) buildBatch( + ctx context.Context, lggr logger.Logger, report commitReportWithSendRequests, inflight []InflightInternalExecutionReport, @@ -538,7 +540,7 @@ func (r *ExecutionReportingPlugin) buildBatch( continue } - tokenData, ready, err2 := getTokenData(context.TODO(), msg, r.config.tokenDataProviders) + tokenData, ready, err2 := getTokenData(ctx, msg, r.config.tokenDataProviders) if err2 != nil { msgLggr.Errorw("Skipping message unable to check attestation", "err", err2) continue 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 f6ef4ff6de..18e1db63fc 100644 --- a/core/services/ocr2/plugins/ccip/execution_reporting_plugin_test.go +++ b/core/services/ocr2/plugins/ccip/execution_reporting_plugin_test.go @@ -641,6 +641,7 @@ func TestExecutionReportingPlugin_buildBatch(t *testing.T) { t.Run(tc.name, func(t *testing.T) { offRamp.SetSenderNonces(tc.offRampNoncesBySender) seqNrs := plugin.buildBatch( + context.Background(), lggr, commitReportWithSendRequests{sendRequestsWithMeta: tc.reqs}, tc.inflight, diff --git a/core/services/ocr2/plugins/ccip/tokendata/cached_reader.go b/core/services/ocr2/plugins/ccip/tokendata/cached_reader.go index f0f9e5c3ad..06781c52f6 100644 --- a/core/services/ocr2/plugins/ccip/tokendata/cached_reader.go +++ b/core/services/ocr2/plugins/ccip/tokendata/cached_reader.go @@ -11,7 +11,7 @@ type CachedReader struct { Reader cache map[uint64][]byte - cacheMutex sync.Mutex + cacheMutex sync.RWMutex } func NewCachedReader(reader Reader) *CachedReader { @@ -21,11 +21,14 @@ func NewCachedReader(reader Reader) *CachedReader { } } +// ReadTokenData tries to get the tokenData from cache, if not found then calls the underlying reader +// and updates the cache. func (r *CachedReader) ReadTokenData(ctx context.Context, msg internal.EVM2EVMOnRampCCIPSendRequestedWithMeta) ([]byte, error) { - r.cacheMutex.Lock() - defer r.cacheMutex.Unlock() + r.cacheMutex.RLock() + data, ok := r.cache[msg.SequenceNumber] + r.cacheMutex.RUnlock() - if data, ok := r.cache[msg.SequenceNumber]; ok { + if ok { return data, nil } @@ -34,6 +37,10 @@ func (r *CachedReader) ReadTokenData(ctx context.Context, msg internal.EVM2EVMOn return []byte{}, err } + r.cacheMutex.Lock() + defer r.cacheMutex.Unlock() + + // Update the cache r.cache[msg.SequenceNumber] = tokenData return tokenData, nil diff --git a/core/services/ocr2/plugins/ccip/tokendata/chached_reader_test.go b/core/services/ocr2/plugins/ccip/tokendata/chached_reader_test.go index c63027efe6..ae78688fb4 100644 --- a/core/services/ocr2/plugins/ccip/tokendata/chached_reader_test.go +++ b/core/services/ocr2/plugins/ccip/tokendata/chached_reader_test.go @@ -1,32 +1,36 @@ -package tokendata +package tokendata_test import ( + "context" "testing" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/tokendata" ) +// Black box test func TestCachedReader_ReadTokenData(t *testing.T) { - mockReader := MockReader{} - cachedReader := NewCachedReader(&mockReader) + mockReader := tokendata.MockReader{} + cachedReader := tokendata.NewCachedReader(&mockReader) msgData := []byte("msgData") mockReader.On("ReadTokenData", mock.Anything, mock.Anything).Return(msgData, nil) + ctx := context.Background() msg := internal.EVM2EVMOnRampCCIPSendRequestedWithMeta{} // Call ReadTokenData twice, expect only one call to underlying reader - data, err := cachedReader.ReadTokenData(nil, msg) + data, err := cachedReader.ReadTokenData(ctx, msg) require.NoError(t, err) require.Equal(t, msgData, data) // First time, calls the underlying reader mockReader.AssertNumberOfCalls(t, "ReadTokenData", 1) - data, err = cachedReader.ReadTokenData(nil, msg) + data, err = cachedReader.ReadTokenData(ctx, msg) require.NoError(t, err) require.Equal(t, msgData, data) 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 56b2c14bc7..59b874c93a 100644 --- a/core/services/ocr2/plugins/ccip/tokendata/usdc/usdc_test.go +++ b/core/services/ocr2/plugins/ccip/tokendata/usdc/usdc_test.go @@ -9,9 +9,12 @@ import ( "testing" "github.com/ethereum/go-ethereum/common" + "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/services/ocr2/plugins/ccip/internal" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipdata" "github.com/smartcontractkit/chainlink/v2/core/utils" ) @@ -93,3 +96,27 @@ func TestGetUSDCReaderSourceLPFilters(t *testing.T) { require.Equal(t, hash, filter.EventSigs[0].Bytes()) require.Equal(t, expectedTransmitterAddress, filter.Addresses[0]) } + +func TestGetUSDCMessageBody(t *testing.T) { + chainId := uint64(420) + expectedBody := []byte("TestGetUSDCMessageBody") + expectedBodyHash := utils.Keccak256Fixed(expectedBody) + + sourceChainEventsMock := ccipdata.MockReader{} + sourceChainEventsMock.On("GetLastUSDCMessagePriorToLogIndexInTx", mock.Anything, mock.Anything, mock.Anything).Return(expectedBody, nil) + + usdcService := NewUSDCTokenDataReader(&sourceChainEventsMock, mockUSDCTokenAddress, mockOnRampAddress, nil, chainId) + + // 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, expectedBodyHash) + + sourceChainEventsMock.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, expectedBodyHash) + sourceChainEventsMock.AssertNumberOfCalls(t, "GetLastUSDCMessagePriorToLogIndexInTx", 1) +} From acfeef5f773f41985c28feccf5b49752ef5c562c Mon Sep 17 00:00:00 2001 From: Rens Rooimans Date: Wed, 20 Sep 2023 20:07:40 +0200 Subject: [PATCH 19/20] read usdc config from jobspec --- .../ocr2/plugins/ccip/config/config.go | 33 +++++++++++- .../ocr2/plugins/ccip/execution_plugin.go | 52 ++++++++----------- .../plugins/ccip/execution_plugin_test.go | 17 ++++-- .../ocr2/plugins/ccip/tokendata/usdc/usdc.go | 49 +---------------- .../ccip/tokendata/usdc/usdc_blackbox_test.go | 3 +- .../plugins/ccip/tokendata/usdc/usdc_test.go | 19 +++---- 6 files changed, 78 insertions(+), 95 deletions(-) diff --git a/core/services/ocr2/plugins/ccip/config/config.go b/core/services/ocr2/plugins/ccip/config/config.go index d46b50045d..441ecf21e4 100644 --- a/core/services/ocr2/plugins/ccip/config/config.go +++ b/core/services/ocr2/plugins/ccip/config/config.go @@ -1,5 +1,12 @@ package config +import ( + "github.com/ethereum/go-ethereum/common" + "github.com/pkg/errors" + + "github.com/smartcontractkit/chainlink/v2/core/utils" +) + // CommitPluginJobSpecConfig contains the plugin specific variables for the ccip.CCIPCommit plugin. // We use ID here to keep it as general as possible, e.g. abstracting for chains which don't have an address concept. type CommitPluginJobSpecConfig struct { @@ -14,5 +21,29 @@ type CommitPluginJobSpecConfig struct { // ExecutionPluginJobSpecConfig contains the plugin specific variables for the ccip.CCIPExecution plugin. type ExecutionPluginJobSpecConfig struct { SourceStartBlock, DestStartBlock int64 // Only for first time job add. - USDCAttestationApi string + USDCConfig USDCConfig +} + +type USDCConfig struct { + SourceTokenAddress common.Address + SourceMessageTransmitterAddress common.Address + AttestationAPI string +} + +func (uc *USDCConfig) ValidateUSDCConfig() error { + if uc.AttestationAPI == "" && uc.SourceTokenAddress == utils.ZeroAddress && uc.SourceMessageTransmitterAddress == utils.ZeroAddress { + return nil + } + + if uc.AttestationAPI == "" { + return errors.New("AttestationAPI is required") + } + if uc.SourceTokenAddress == utils.ZeroAddress { + return errors.New("SourceTokenAddress is required") + } + if uc.SourceMessageTransmitterAddress == utils.ZeroAddress { + return errors.New("SourceMessageTransmitterAddress is required") + } + + return nil } diff --git a/core/services/ocr2/plugins/ccip/execution_plugin.go b/core/services/ocr2/plugins/ccip/execution_plugin.go index d8e667f515..3c9855b7b8 100644 --- a/core/services/ocr2/plugins/ccip/execution_plugin.go +++ b/core/services/ocr2/plugins/ccip/execution_plugin.go @@ -116,7 +116,7 @@ func NewExecutionServices(lggr logger.Logger, jb job.Job, chainSet evm.LegacyCha sourceChainEventClient := ccipdata.NewLogPollerReader(sourceChain.LogPoller(), execLggr, sourceChain.Client()) - tokenDataProviders, err := getTokenDataProviders(pluginConfig, chainId, offRampConfig.OnRamp, sourceChainEventClient) + tokenDataProviders, err := getTokenDataProviders(pluginConfig, offRampConfig.OnRamp, sourceChainEventClient) if err != nil { return nil, errors.Wrap(err, "could not get token data providers") } @@ -173,25 +173,29 @@ func NewExecutionServices(lggr logger.Logger, jb job.Job, chainSet evm.LegacyCha return []job.ServiceCtx{job.NewServiceAdapter(oracle)}, nil } -func getTokenDataProviders(pluginConfig ccipconfig.ExecutionPluginJobSpecConfig, sourceChainId uint64, onRampAddress common.Address, sourceChainEventClient *ccipdata.LogPollerReader) (map[common.Address]tokendata.Reader, error) { +func getTokenDataProviders(pluginConfig ccipconfig.ExecutionPluginJobSpecConfig, onRampAddress common.Address, sourceChainEventClient *ccipdata.LogPollerReader) (map[common.Address]tokendata.Reader, error) { tokenDataProviders := make(map[common.Address]tokendata.Reader) - if pluginConfig.USDCAttestationApi != "" { - attestationURI, err2 := url.ParseRequestURI(pluginConfig.USDCAttestationApi) - if err2 != nil { - return nil, errors.Wrap(err2, "failed to parse USDC attestation API") + if pluginConfig.USDCConfig.AttestationAPI != "" { + err := pluginConfig.USDCConfig.ValidateUSDCConfig() + if err != nil { + return nil, err } - usdcTokenAddress, err2 := usdc.GetUSDCTokenAddress(sourceChainId) + + attestationURI, err2 := url.ParseRequestURI(pluginConfig.USDCConfig.AttestationAPI) if err2 != nil { - return nil, errors.Wrap(err2, "failed to get USDC token address") + return nil, errors.Wrap(err2, "failed to parse USDC attestation API") } - tokenDataProviders[usdcTokenAddress] = tokendata.NewCachedReader(usdc.NewUSDCTokenDataReader( - sourceChainEventClient, - usdcTokenAddress, - onRampAddress, - attestationURI, - sourceChainId)) + tokenDataProviders[pluginConfig.USDCConfig.SourceTokenAddress] = tokendata.NewCachedReader( + usdc.NewUSDCTokenDataReader( + sourceChainEventClient, + pluginConfig.USDCConfig.SourceTokenAddress, + pluginConfig.USDCConfig.SourceMessageTransmitterAddress, + onRampAddress, + attestationURI, + ), + ) } return tokenDataProviders, nil @@ -305,7 +309,7 @@ func UnregisterExecPluginLpFilters(ctx context.Context, spec *job.OCR2OracleSpec return errors.Wrap(err, "failed loading onRamp") } - return unregisterExecutionPluginLpFilters(ctx, sourceChain.LogPoller(), destChain.LogPoller(), offRamp, offRampConfig, sourceOnRamp, sourceChain.Client(), qopts...) + return unregisterExecutionPluginLpFilters(ctx, sourceChain.LogPoller(), destChain.LogPoller(), offRamp, offRampConfig, sourceOnRamp, sourceChain.Client(), pluginConfig, qopts...) } func unregisterExecutionPluginLpFilters( @@ -316,6 +320,7 @@ func unregisterExecutionPluginLpFilters( destOffRampConfig evm_2_evm_offramp.EVM2EVMOffRampStaticConfig, 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 { @@ -327,25 +332,12 @@ func unregisterExecutionPluginLpFilters( return err } - chainId, err := chainselectors.ChainIdFromSelector(destOffRampConfig.SourceChainSelector) + // SourceChainEventClient can be nil because it is not used in unregisterExecutionPluginLpFilters + tokenDataProviders, err := getTokenDataProviders(pluginConfig, destOffRampConfig.OnRamp, nil) if err != nil { return err } - tokenDataProviders := make(map[common.Address]tokendata.Reader) - - usdcTokenAddress, err := usdc.GetUSDCTokenAddress(chainId) - if err != nil { - return errors.Wrap(err, "failed to get USDC token address") - } - // TODO the called function only uses the chainId to get the message transmitter address - tokenDataProviders[usdcTokenAddress] = usdc.NewUSDCTokenDataReader( - nil, - usdcTokenAddress, - common.Address{}, - nil, - chainId) - if err = logpollerutil.UnregisterLpFilters( sourceLP, getExecutionPluginSourceLpChainFilters(destOffRampConfig.OnRamp, onRampDynCfg.PriceRegistry, tokenDataProviders), diff --git a/core/services/ocr2/plugins/ccip/execution_plugin_test.go b/core/services/ocr2/plugins/ccip/execution_plugin_test.go index db9a496fdf..db43d888ce 100644 --- a/core/services/ocr2/plugins/ccip/execution_plugin_test.go +++ b/core/services/ocr2/plugins/ccip/execution_plugin_test.go @@ -7,15 +7,16 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" mocklp "github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller/mocks" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/mocks" "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/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) { @@ -76,15 +77,20 @@ func TestGetExecutionPluginFilterNames(t *testing.T) { mockOnRamp, onRampAddr := testhelpers.NewFakeOnRamp(t) mockOnRamp.SetDynamicCfg(evm_2_evm_onramp.EVM2EVMOnRampDynamicConfig{PriceRegistry: srcPriceRegAddr}) - usdcMessageTransmitter, err := usdc.GetUSDCMessageTransmitterAddress(1) - require.NoError(t, err) + 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 + " - " + usdcMessageTransmitter.Hex(), + usdc.MESSAGE_SENT_FILTER_NAME + " - " + pluginConfig.USDCConfig.SourceMessageTransmitterAddress.Hex(), } for _, f := range srcFilters { srcLP.On("UnregisterFilter", f, mock.Anything).Return(nil) @@ -103,7 +109,7 @@ func TestGetExecutionPluginFilterNames(t *testing.T) { dstLP.On("UnregisterFilter", f, mock.Anything).Return(nil) } - err = unregisterExecutionPluginLpFilters( + err := unregisterExecutionPluginLpFilters( context.Background(), srcLP, dstLP, @@ -115,6 +121,7 @@ func TestGetExecutionPluginFilterNames(t *testing.T) { }, mockOnRamp, nil, + pluginConfig, ) assert.NoError(t, err) diff --git a/core/services/ocr2/plugins/ccip/tokendata/usdc/usdc.go b/core/services/ocr2/plugins/ccip/tokendata/usdc/usdc.go index a896c7434e..8da8023897 100644 --- a/core/services/ocr2/plugins/ccip/tokendata/usdc/usdc.go +++ b/core/services/ocr2/plugins/ccip/tokendata/usdc/usdc.go @@ -17,7 +17,6 @@ import ( "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/internal/chains" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/tokendata" "github.com/smartcontractkit/chainlink/v2/core/utils" ) @@ -39,50 +38,6 @@ type attestationResponse struct { Attestation string `json:"attestation"` } -// Hard coded mapping of chain id to USDC token addresses -// Will be removed in favour of more flexible solution. -var tokenMapping = map[chains.EVMChain]common.Address{ - // Mainnet - chains.Ethereum: common.HexToAddress("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"), - chains.Optimism: common.HexToAddress("0x0b2c639c533813f4aa9d7837caf62653d097ff85"), - chains.Arbitrum: common.HexToAddress("0xaf88d065e77c8cc2239327c5edb3a432268e5831"), - chains.Avalanche: common.HexToAddress("0xb97ef9ef8734c71904d8002f8b6bc66dd9c48a6e"), - - // Testnets - chains.GoerliTestnet: common.HexToAddress("0xca6b4c00831ffb77afe22e734a6101b268b7fcbe"), - chains.OptimismGoerliTestnet: common.HexToAddress("0xe05606174bac4A6364B31bd0eCA4bf4dD368f8C6"), - chains.AvalancheFujiTestnet: common.HexToAddress("0x5425890298aed601595a70ab815c96711a31bc65"), - chains.ArbitrumGoerliTestnet: common.HexToAddress("0xfd064A18f3BF249cf1f87FC203E90D8f650f2d63"), -} - -func GetUSDCTokenAddress(chain uint64) (common.Address, error) { - if tokenAddress, ok := tokenMapping[chains.EVMChain(chain)]; ok { - return tokenAddress, nil - } - return common.Address{}, errors.New("token not found") -} - -var messageTransmitterMapping = map[chains.EVMChain]common.Address{ - // Mainnet - chains.Ethereum: common.HexToAddress("0x0a992d191deec32afe36203ad87d7d289a738f81"), - chains.Optimism: common.HexToAddress("0x4d41f22c5a0e5c74090899e5a8fb597a8842b3e8"), - chains.Arbitrum: common.HexToAddress("0xC30362313FBBA5cf9163F0bb16a0e01f01A896ca"), - chains.Avalanche: common.HexToAddress("0x8186359af5f57fbb40c6b14a588d2a59c0c29880"), - - // Testnets - chains.GoerliTestnet: common.HexToAddress("0xca6b4c00831ffb77afe22e734a6101b268b7fcbe"), - chains.OptimismGoerliTestnet: common.HexToAddress("0x9ff9a4da6f2157a9c82ce756f8fd7e0d75be8895"), - chains.AvalancheFujiTestnet: common.HexToAddress("0xa9fb1b3009dcb79e2fe346c16a604b8fa8ae0a79"), - chains.ArbitrumGoerliTestnet: common.HexToAddress("0x109bc137cb64eab7c0b1dddd1edf341467dc2d35"), -} - -func GetUSDCMessageTransmitterAddress(chain uint64) (common.Address, error) { - if transmitterAddress, ok := messageTransmitterMapping[chains.EVMChain(chain)]; ok { - return transmitterAddress, nil - } - return common.Address{}, errors.New("usdc transmitter not found") -} - const ( version = "v1" attestationPath = "attestations" @@ -98,11 +53,11 @@ const ( var _ tokendata.Reader = &TokenDataReader{} -func NewUSDCTokenDataReader(sourceChainEvents ccipdata.Reader, usdcTokenAddress, onRampAddress common.Address, usdcAttestationApi *url.URL, sourceChainId uint64) *TokenDataReader { +func NewUSDCTokenDataReader(sourceChainEvents ccipdata.Reader, usdcTokenAddress, usdcMessageTransmitterAddress, onRampAddress common.Address, usdcAttestationApi *url.URL) *TokenDataReader { return &TokenDataReader{ sourceChainEvents: sourceChainEvents, attestationApi: usdcAttestationApi, - messageTransmitter: messageTransmitterMapping[chains.EVMChain(sourceChainId)], + messageTransmitter: usdcMessageTransmitterAddress, onRampAddress: onRampAddress, sourceToken: usdcTokenAddress, usdcMessageHashCache: make(map[uint64][32]byte), 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 2dbc97035e..19860d8221 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 @@ -25,6 +25,7 @@ import ( var ( mockOnRampAddress = utils.RandomAddress() mockUSDCTokenAddress = utils.RandomAddress() + mockMsgTransmitter = utils.RandomAddress() ) type attestationResponse struct { @@ -81,7 +82,7 @@ func TestUSDCReader_ReadTokenData(t *testing.T) { attestationURI, err := url.ParseRequestURI(ts.URL) require.NoError(t, err) - usdcService := usdc.NewUSDCTokenDataReader(&eventsClient, mockUSDCTokenAddress, mockOnRampAddress, attestationURI, 420) + usdcService := usdc.NewUSDCTokenDataReader(&eventsClient, mockUSDCTokenAddress, mockMsgTransmitter, mockOnRampAddress, attestationURI) attestation, err := usdcService.ReadTokenData(context.Background(), internal.EVM2EVMOnRampCCIPSendRequestedWithMeta{ InternalEVM2EVMMessage: evm_2_evm_offramp.InternalEVM2EVMMessage{ SequenceNumber: seqNum, 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 59b874c93a..76049a2da7 100644 --- a/core/services/ocr2/plugins/ccip/tokendata/usdc/usdc_test.go +++ b/core/services/ocr2/plugins/ccip/tokendata/usdc/usdc_test.go @@ -21,6 +21,7 @@ import ( var ( mockOnRampAddress = utils.RandomAddress() mockUSDCTokenAddress = utils.RandomAddress() + mockMsgTransmitter = utils.RandomAddress() ) func TestUSDCReader_callAttestationApi(t *testing.T) { @@ -28,7 +29,7 @@ func TestUSDCReader_callAttestationApi(t *testing.T) { usdcMessageHash := "912f22a13e9ccb979b621500f6952b2afd6e75be7eadaed93fc2625fe11c52a2" attestationURI, err := url.ParseRequestURI("https://iris-api-sandbox.circle.com") require.NoError(t, err) - usdcService := NewUSDCTokenDataReader(nil, mockUSDCTokenAddress, mockOnRampAddress, attestationURI, 420) + usdcService := NewUSDCTokenDataReader(nil, mockUSDCTokenAddress, mockMsgTransmitter, mockOnRampAddress, attestationURI) attestation, err := usdcService.callAttestationApi(context.Background(), [32]byte(common.FromHex(usdcMessageHash))) require.NoError(t, err) @@ -48,7 +49,7 @@ func TestUSDCReader_callAttestationApiMock(t *testing.T) { attestationURI, err := url.ParseRequestURI(ts.URL) require.NoError(t, err) - usdcService := NewUSDCTokenDataReader(nil, mockUSDCTokenAddress, mockOnRampAddress, attestationURI, 420) + usdcService := NewUSDCTokenDataReader(nil, mockUSDCTokenAddress, mockMsgTransmitter, mockOnRampAddress, attestationURI) attestation, err := usdcService.callAttestationApi(context.Background(), utils.RandomBytes32()) require.NoError(t, err) @@ -64,7 +65,7 @@ func TestUSDCReader_callAttestationApiMockError(t *testing.T) { attestationURI, err := url.ParseRequestURI(ts.URL) require.NoError(t, err) - usdcService := NewUSDCTokenDataReader(nil, mockUSDCTokenAddress, mockOnRampAddress, attestationURI, 420) + usdcService := NewUSDCTokenDataReader(nil, mockUSDCTokenAddress, mockMsgTransmitter, mockOnRampAddress, attestationURI) _, err = usdcService.callAttestationApi(context.Background(), utils.RandomBytes32()) require.Error(t, err) } @@ -81,31 +82,27 @@ func getMockUSDCEndpoint(t *testing.T, response attestationResponse) *httptest.S // Asserts the hard coded event signature matches Keccak256("MessageSent(bytes)") func TestGetUSDCReaderSourceLPFilters(t *testing.T) { - chainId := uint64(420) - usdcService := NewUSDCTokenDataReader(nil, mockUSDCTokenAddress, mockOnRampAddress, nil, chainId) + usdcService := NewUSDCTokenDataReader(nil, mockUSDCTokenAddress, mockMsgTransmitter, mockOnRampAddress, nil) filters := usdcService.GetSourceLogPollerFilters() - expectedTransmitterAddress, err := GetUSDCMessageTransmitterAddress(chainId) - require.NoError(t, err) require.Equal(t, 1, len(filters)) filter := filters[0] - require.Equal(t, logpoller.FilterName(MESSAGE_SENT_FILTER_NAME, expectedTransmitterAddress.Hex()), filter.Name) + 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, expectedTransmitterAddress, filter.Addresses[0]) + require.Equal(t, mockMsgTransmitter, filter.Addresses[0]) } func TestGetUSDCMessageBody(t *testing.T) { - chainId := uint64(420) expectedBody := []byte("TestGetUSDCMessageBody") expectedBodyHash := utils.Keccak256Fixed(expectedBody) sourceChainEventsMock := ccipdata.MockReader{} sourceChainEventsMock.On("GetLastUSDCMessagePriorToLogIndexInTx", mock.Anything, mock.Anything, mock.Anything).Return(expectedBody, nil) - usdcService := NewUSDCTokenDataReader(&sourceChainEventsMock, mockUSDCTokenAddress, mockOnRampAddress, nil, chainId) + usdcService := NewUSDCTokenDataReader(&sourceChainEventsMock, mockUSDCTokenAddress, mockMsgTransmitter, mockOnRampAddress, nil) // Make the first call and assert the underlying function is called body, err := usdcService.getUSDCMessageBody(context.Background(), internal.EVM2EVMOnRampCCIPSendRequestedWithMeta{}) From d00da8cbb26e786d8ccf6b3f6482cef22b914d8b Mon Sep 17 00:00:00 2001 From: Rens Rooimans Date: Wed, 20 Sep 2023 21:39:52 +0200 Subject: [PATCH 20/20] improve logging, reduce exported usdc types --- core/services/ocr2/delegate.go | 2 +- .../ocr2/plugins/ccip/execution_plugin.go | 12 ++++--- .../plugins/ccip/execution_plugin_test.go | 4 ++- .../ccip/execution_reporting_plugin.go | 34 ++++++++++--------- .../ocr2/plugins/ccip/tokendata/usdc/usdc.go | 10 +++--- .../ccip/tokendata/usdc/usdc_blackbox_test.go | 7 ++-- .../plugins/ccip/tokendata/usdc/usdc_test.go | 4 +-- 7 files changed, 39 insertions(+), 34 deletions(-) diff --git a/core/services/ocr2/delegate.go b/core/services/ocr2/delegate.go index 615bcbc67b..966014b239 100644 --- a/core/services/ocr2/delegate.go +++ b/core/services/ocr2/delegate.go @@ -293,7 +293,7 @@ func (d *Delegate) cleanupEVM(jb job.Job, q pg.Queryer, relayID relay.ID) error } return nil case types.CCIPExecution: - err = ccip.UnregisterExecPluginLpFilters(context.Background(), spec, d.legacyChains, pg.WithQueryer(q)) + err = ccip.UnregisterExecPluginLpFilters(context.Background(), d.lggr, spec, d.legacyChains, pg.WithQueryer(q)) if err != nil { d.lggr.Errorw("failed to unregister ccip exec plugin filters", "err", err, "spec", spec) } diff --git a/core/services/ocr2/plugins/ccip/execution_plugin.go b/core/services/ocr2/plugins/ccip/execution_plugin.go index 3c9855b7b8..2d00333d93 100644 --- a/core/services/ocr2/plugins/ccip/execution_plugin.go +++ b/core/services/ocr2/plugins/ccip/execution_plugin.go @@ -116,7 +116,7 @@ func NewExecutionServices(lggr logger.Logger, jb job.Job, chainSet evm.LegacyCha sourceChainEventClient := ccipdata.NewLogPollerReader(sourceChain.LogPoller(), execLggr, sourceChain.Client()) - tokenDataProviders, err := getTokenDataProviders(pluginConfig, offRampConfig.OnRamp, sourceChainEventClient) + tokenDataProviders, err := getTokenDataProviders(lggr, pluginConfig, offRampConfig.OnRamp, sourceChainEventClient) if err != nil { return nil, errors.Wrap(err, "could not get token data providers") } @@ -173,10 +173,11 @@ func NewExecutionServices(lggr logger.Logger, jb job.Job, chainSet evm.LegacyCha return []job.ServiceCtx{job.NewServiceAdapter(oracle)}, nil } -func getTokenDataProviders(pluginConfig ccipconfig.ExecutionPluginJobSpecConfig, onRampAddress common.Address, sourceChainEventClient *ccipdata.LogPollerReader) (map[common.Address]tokendata.Reader, error) { +func getTokenDataProviders(lggr logger.Logger, pluginConfig ccipconfig.ExecutionPluginJobSpecConfig, onRampAddress common.Address, sourceChainEventClient *ccipdata.LogPollerReader) (map[common.Address]tokendata.Reader, error) { tokenDataProviders := make(map[common.Address]tokendata.Reader) if pluginConfig.USDCConfig.AttestationAPI != "" { + lggr.Infof("USDC token data provider enabled") err := pluginConfig.USDCConfig.ValidateUSDCConfig() if err != nil { return nil, err @@ -262,7 +263,7 @@ func getExecutionPluginDestLpChainFilters(commitStore, offRamp, priceRegistry co } // UnregisterExecPluginLpFilters unregisters all the registered filters for both source and dest chains. -func UnregisterExecPluginLpFilters(ctx context.Context, spec *job.OCR2OracleSpec, chainSet evm.LegacyChainContainer, qopts ...pg.QOpt) error { +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") } @@ -309,11 +310,12 @@ func UnregisterExecPluginLpFilters(ctx context.Context, spec *job.OCR2OracleSpec return errors.Wrap(err, "failed loading onRamp") } - return unregisterExecutionPluginLpFilters(ctx, sourceChain.LogPoller(), destChain.LogPoller(), offRamp, offRampConfig, sourceOnRamp, sourceChain.Client(), pluginConfig, qopts...) + return unregisterExecutionPluginLpFilters(ctx, lggr, sourceChain.LogPoller(), destChain.LogPoller(), offRamp, offRampConfig, sourceOnRamp, sourceChain.Client(), pluginConfig, qopts...) } func unregisterExecutionPluginLpFilters( ctx context.Context, + lggr logger.Logger, sourceLP logpoller.LogPoller, destLP logpoller.LogPoller, destOffRamp evm_2_evm_offramp.EVM2EVMOffRampInterface, @@ -333,7 +335,7 @@ func unregisterExecutionPluginLpFilters( } // SourceChainEventClient can be nil because it is not used in unregisterExecutionPluginLpFilters - tokenDataProviders, err := getTokenDataProviders(pluginConfig, destOffRampConfig.OnRamp, nil) + tokenDataProviders, err := getTokenDataProviders(lggr, pluginConfig, destOffRampConfig.OnRamp, nil) if err != nil { return err } diff --git a/core/services/ocr2/plugins/ccip/execution_plugin_test.go b/core/services/ocr2/plugins/ccip/execution_plugin_test.go index db43d888ce..dec6e2f5ad 100644 --- a/core/services/ocr2/plugins/ccip/execution_plugin_test.go +++ b/core/services/ocr2/plugins/ccip/execution_plugin_test.go @@ -12,6 +12,7 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/chains/evm/mocks" "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/job" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/config" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/testhelpers" @@ -56,7 +57,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(), tc.spec, chainSet) + err := UnregisterExecPluginLpFilters(context.Background(), logger.TestLogger(t), tc.spec, chainSet) if tc.expectingErr { assert.Error(t, err) } else { @@ -111,6 +112,7 @@ func TestGetExecutionPluginFilterNames(t *testing.T) { err := unregisterExecutionPluginLpFilters( context.Background(), + logger.TestLogger(t), srcLP, dstLP, mockOffRamp, diff --git a/core/services/ocr2/plugins/ccip/execution_reporting_plugin.go b/core/services/ocr2/plugins/ccip/execution_reporting_plugin.go index 0feb38c865..805366217e 100644 --- a/core/services/ocr2/plugins/ccip/execution_reporting_plugin.go +++ b/core/services/ocr2/plugins/ccip/execution_reporting_plugin.go @@ -513,7 +513,7 @@ func (r *ExecutionReportingPlugin) buildBatch( // Chain holds existing nonce. nonce, err := r.config.offRamp.GetSenderNonce(nil, msg.Sender) if err != nil { - lggr.Errorw("unable to get sender nonce", "err", err) + lggr.Errorw("unable to get sender nonce", "err", err, "seqNr", msg.SequenceNumber) continue } expectedNonces[msg.Sender] = nonce + 1 @@ -542,9 +542,9 @@ func (r *ExecutionReportingPlugin) buildBatch( continue } - tokenData, ready, err2 := getTokenData(ctx, msg, r.config.tokenDataProviders) + tokenData, ready, err2 := getTokenData(ctx, msgLggr, msg, r.config.tokenDataProviders) if err2 != nil { - msgLggr.Errorw("Skipping message unable to check attestation", "err", err2) + msgLggr.Errorw("Skipping message unable to check token data", "err", err2) continue } if !ready { @@ -633,23 +633,25 @@ func (r *ExecutionReportingPlugin) buildBatch( return executableMessages } -func getTokenData(ctx context.Context, msg internal.EVM2EVMOnRampCCIPSendRequestedWithMeta, tokenDataProviders map[common.Address]tokendata.Reader) (tokenData [][]byte, allReady bool, err error) { +func getTokenData(ctx context.Context, lggr logger.Logger, msg internal.EVM2EVMOnRampCCIPSendRequestedWithMeta, tokenDataProviders map[common.Address]tokendata.Reader) (tokenData [][]byte, allReady bool, err error) { for _, token := range msg.TokenAmounts { - if offchainTokenDataProvider, ok := tokenDataProviders[token.Token]; ok { - attestation, err2 := offchainTokenDataProvider.ReadTokenData(ctx, msg) - if err2 != nil { - if errors.Is(err2, tokendata.ErrNotReady) { - return [][]byte{}, false, nil - } - return [][]byte{}, false, err2 - } - - tokenData = append(tokenData, attestation) + offchainTokenDataProvider, ok := tokenDataProviders[token.Token] + if !ok { + // No token data required + tokenData = append(tokenData, []byte{}) continue } + lggr.Infow("Fetching token data", "token", token.Token.Hex()) + tknData, err2 := offchainTokenDataProvider.ReadTokenData(ctx, msg) + if err2 != nil { + if errors.Is(err2, tokendata.ErrNotReady) { + lggr.Infof("Token data not ready yet for token %s", token.Token.Hex()) + return [][]byte{}, false, nil + } + return [][]byte{}, false, err2 + } - // No token data required - tokenData = append(tokenData, []byte{}) + tokenData = append(tokenData, tknData) } return tokenData, true, nil } diff --git a/core/services/ocr2/plugins/ccip/tokendata/usdc/usdc.go b/core/services/ocr2/plugins/ccip/tokendata/usdc/usdc.go index 8da8023897..3b1f1b6177 100644 --- a/core/services/ocr2/plugins/ccip/tokendata/usdc/usdc.go +++ b/core/services/ocr2/plugins/ccip/tokendata/usdc/usdc.go @@ -34,7 +34,7 @@ type TokenDataReader struct { } type attestationResponse struct { - Status AttestationStatus `json:"status"` + Status attestationStatus `json:"status"` Attestation string `json:"attestation"` } @@ -44,11 +44,11 @@ const ( MESSAGE_SENT_FILTER_NAME = "USDC message sent" ) -type AttestationStatus string +type attestationStatus string const ( - AttestationStatusSuccess AttestationStatus = "complete" - AttestationStatusPending AttestationStatus = "pending_confirmations" + attestationStatusSuccess attestationStatus = "complete" + attestationStatusPending attestationStatus = "pending_confirmations" ) var _ tokendata.Reader = &TokenDataReader{} @@ -70,7 +70,7 @@ func (s *TokenDataReader) ReadTokenData(ctx context.Context, msg internal.EVM2EV return []byte{}, err } - if response.Status == AttestationStatusSuccess { + if response.Status == attestationStatusSuccess { attestationBytes, err := hex.DecodeString(response.Attestation) if err != nil { return nil, fmt.Errorf("decode response attestation: %w", err) 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 19860d8221..6e61be5ed3 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 @@ -29,13 +29,13 @@ var ( ) type attestationResponse struct { - Status usdc.AttestationStatus `json:"status"` - Attestation string `json:"attestation"` + Status string `json:"status"` + Attestation string `json:"attestation"` } func TestUSDCReader_ReadTokenData(t *testing.T) { response := attestationResponse{ - Status: usdc.AttestationStatusSuccess, + Status: "complete", Attestation: "720502893578a89a8a87982982ef781c18b193", } @@ -93,5 +93,4 @@ func TestUSDCReader_ReadTokenData(t *testing.T) { require.NoError(t, err) require.Equal(t, attestationBytes, attestation) - require.Equal(t, response.Status, usdc.AttestationStatusSuccess) } 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 76049a2da7..d61ef530d5 100644 --- a/core/services/ocr2/plugins/ccip/tokendata/usdc/usdc_test.go +++ b/core/services/ocr2/plugins/ccip/tokendata/usdc/usdc_test.go @@ -34,13 +34,13 @@ func TestUSDCReader_callAttestationApi(t *testing.T) { attestation, err := usdcService.callAttestationApi(context.Background(), [32]byte(common.FromHex(usdcMessageHash))) require.NoError(t, err) - require.Equal(t, AttestationStatusPending, attestation.Status) + require.Equal(t, attestationStatusPending, attestation.Status) require.Equal(t, "PENDING", attestation.Attestation) } func TestUSDCReader_callAttestationApiMock(t *testing.T) { response := attestationResponse{ - Status: AttestationStatusSuccess, + Status: attestationStatusSuccess, Attestation: "720502893578a89a8a87982982ef781c18b193", }