diff --git a/core/services/ocr2/plugins/ccip/tokendata/usdc/usdc.go b/core/services/ocr2/plugins/ccip/tokendata/usdc/usdc.go index 81a22ad2c7..18e2479ca0 100644 --- a/core/services/ocr2/plugins/ccip/tokendata/usdc/usdc.go +++ b/core/services/ocr2/plugins/ccip/tokendata/usdc/usdc.go @@ -24,26 +24,8 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/utils" ) -type TokenDataReader struct { - lggr logger.Logger - sourceChainEvents ccipdata.Reader - attestationApi *url.URL - messageTransmitter common.Address - sourceToken common.Address - onRampAddress common.Address - - // Cache of sequence number -> usdc message body - usdcMessageHashCache map[uint64][32]byte - usdcMessageHashCacheMutex sync.Mutex -} - -type attestationResponse struct { - Status attestationStatus `json:"status"` - Attestation string `json:"attestation"` -} - const ( - version = "v1" + apiVersion = "v1" attestationPath = "attestations" MESSAGE_SENT_FILTER_NAME = "USDC message sent" ) @@ -55,6 +37,7 @@ const ( attestationStatusPending attestationStatus = "pending_confirmations" ) +// usdcPayload has to match the onchain event emitted by the USDC message transmitter type usdcPayload []byte func (d usdcPayload) AbiString() string { @@ -68,6 +51,52 @@ func (d usdcPayload) Validate() error { return nil } +// messageAndAttestation has to match the onchain struct `MessageAndAttestation` in the +// USDC token pool. +type messageAndAttestation struct { + Message []byte + Attestation []byte +} + +func (m messageAndAttestation) AbiString() string { + return ` + [{ + "components": [ + {"name": "message", "type": "bytes"}, + {"name": "attestation", "type": "bytes"} + ], + "type": "tuple" + }]` +} + +func (m messageAndAttestation) Validate() error { + if len(m.Message) == 0 { + return errors.New("message must be non-empty") + } + if len(m.Attestation) == 0 { + return errors.New("attestation must be non-empty") + } + return nil +} + +type TokenDataReader struct { + lggr logger.Logger + sourceChainEvents ccipdata.Reader + attestationApi *url.URL + messageTransmitter common.Address + sourceToken common.Address + onRampAddress common.Address + + // Cache of sequence number -> usdc message body + usdcMessageHashCache map[uint64][]byte + usdcMessageHashCacheMutex sync.Mutex +} + +type attestationResponse struct { + Status attestationStatus `json:"status"` + Attestation string `json:"attestation"` +} + var _ tokendata.Reader = &TokenDataReader{} func NewUSDCTokenDataReader(lggr logger.Logger, sourceChainEvents ccipdata.Reader, usdcTokenAddress, usdcMessageTransmitterAddress, onRampAddress common.Address, usdcAttestationApi *url.URL) *TokenDataReader { @@ -78,43 +107,52 @@ func NewUSDCTokenDataReader(lggr logger.Logger, sourceChainEvents ccipdata.Reade messageTransmitter: usdcMessageTransmitterAddress, onRampAddress: onRampAddress, sourceToken: usdcTokenAddress, - usdcMessageHashCache: make(map[uint64][32]byte), + usdcMessageHashCache: make(map[uint64][]byte), } } -func (s *TokenDataReader) ReadTokenData(ctx context.Context, msg internal.EVM2EVMOnRampCCIPSendRequestedWithMeta) (attestation []byte, err error) { - response, err := s.getUpdatedAttestation(ctx, msg) +func (s *TokenDataReader) ReadTokenData(ctx context.Context, msg internal.EVM2EVMOnRampCCIPSendRequestedWithMeta) (messageAndAttestation []byte, err error) { + messageBody, err := s.getUSDCMessageBody(ctx, msg) if err != nil { - return []byte{}, err + return []byte{}, errors.Wrap(err, "failed getting the USDC message body") } - if response.Status == attestationStatusSuccess { - attestationBytes, err := hex.DecodeString(strings.TrimPrefix(response.Attestation, "0x")) - if err != nil { - return nil, fmt.Errorf("decode response attestation: %w", err) - } - return attestationBytes, nil + s.lggr.Infow("Calling attestation API", "messageBodyHash", hexutil.Encode(messageBody[:]), "messageID", hexutil.Encode(msg.MessageId[:])) + + // The attestation API expects the hash of the message body + attestationResp, err := s.callAttestationApi(ctx, utils.Keccak256Fixed(messageBody)) + if err != nil { + return []byte{}, errors.Wrap(err, "failed calling usdc attestation API ") } - return []byte{}, tokendata.ErrNotReady -} -func (s *TokenDataReader) getUpdatedAttestation(ctx context.Context, msg internal.EVM2EVMOnRampCCIPSendRequestedWithMeta) (attestationResponse, error) { - messageBodyHash, err := s.getUSDCMessageBodyHash(ctx, msg) + if attestationResp.Status != attestationStatusSuccess { + return []byte{}, tokendata.ErrNotReady + } + + // The USDC pool needs a combination of the message body and the attestation + messageAndAttestation, err = encodeMessageAndAttestation(messageBody, attestationResp.Attestation) if err != nil { - return attestationResponse{}, errors.Wrap(err, "failed getting the USDC message body") + return nil, fmt.Errorf("failed to encode messageAndAttestation : %w", err) } - s.lggr.Infow("Calling attestation API", "messageBodyHash", hexutil.Encode(messageBodyHash[:]), "messageID", hexutil.Encode(msg.MessageId[:])) + return messageAndAttestation, nil +} - response, err := s.callAttestationApi(ctx, messageBodyHash) +// encodeMessageAndAttestation encodes the message body and attestation into a single byte array +// that is readable onchain. +func encodeMessageAndAttestation(messageBody []byte, attestation string) ([]byte, error) { + attestationBytes, err := hex.DecodeString(strings.TrimPrefix(attestation, "0x")) if err != nil { - return attestationResponse{}, errors.Wrap(err, "failed calling usdc attestation API ") + return nil, fmt.Errorf("failed to decode response attestation: %w", err) } - return response, nil + return abihelpers.EncodeAbiStruct[messageAndAttestation](messageAndAttestation{ + Message: messageBody, + Attestation: attestationBytes, + }) } -func (s *TokenDataReader) getUSDCMessageBodyHash(ctx context.Context, msg internal.EVM2EVMOnRampCCIPSendRequestedWithMeta) ([32]byte, error) { +func (s *TokenDataReader) getUSDCMessageBody(ctx context.Context, msg internal.EVM2EVMOnRampCCIPSendRequestedWithMeta) ([]byte, error) { s.usdcMessageHashCacheMutex.Lock() defer s.usdcMessageHashCacheMutex.Unlock() @@ -124,20 +162,19 @@ func (s *TokenDataReader) getUSDCMessageBodyHash(ctx context.Context, msg intern usdcMessageBody, err := s.sourceChainEvents.GetLastUSDCMessagePriorToLogIndexInTx(ctx, int64(msg.LogIndex), msg.TxHash) if err != nil { - return [32]byte{}, err + return []byte{}, err } - s.lggr.Infow("Got USDC message body", "messageBody", hexutil.Encode(usdcMessageBody), "messageID", hexutil.Encode(msg.MessageId[:])) - parsedMsgBody, err := decodeUSDCMessageSent(usdcMessageBody) if err != nil { - return [32]byte{}, errors.Wrap(err, "failed parsing solidity struct") + return []byte{}, errors.Wrap(err, "failed parsing solidity struct") } - msgBodyHash := utils.Keccak256Fixed(parsedMsgBody) + + s.lggr.Infow("Got USDC message body", "messageBody", hexutil.Encode(parsedMsgBody), "messageID", hexutil.Encode(msg.MessageId[:])) // Save the attempt in the cache in case the external call fails - s.usdcMessageHashCache[msg.SequenceNumber] = msgBodyHash - return msgBodyHash, nil + s.usdcMessageHashCache[msg.SequenceNumber] = parsedMsgBody + return parsedMsgBody, nil } func decodeUSDCMessageSent(logData []byte) ([]byte, error) { @@ -149,7 +186,7 @@ func decodeUSDCMessageSent(logData []byte) ([]byte, 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) + fullAttestationUrl := fmt.Sprintf("%s/%s/%s/0x%x", s.attestationApi, apiVersion, attestationPath, usdcMessageHash) req, err := http.NewRequestWithContext(ctx, "GET", fullAttestationUrl, nil) if err != nil { return attestationResponse{}, 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 a9f8cba800..26ba789cf1 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 @@ -19,6 +19,7 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/evm_2_evm_offramp" "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/evm_2_evm_onramp" "github.com/smartcontractkit/chainlink/v2/core/logger" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/abihelpers" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipdata" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/tokendata/usdc" @@ -36,21 +37,56 @@ type attestationResponse struct { Attestation string `json:"attestation"` } +type messageAndAttestation struct { + Message []byte + Attestation []byte +} + +func (m messageAndAttestation) AbiString() string { + return ` + [{ + "components": [ + {"name": "message", "type": "bytes"}, + {"name": "attestation", "type": "bytes"} + ], + "type": "tuple" + }]` +} + +func (m messageAndAttestation) Validate() error { + return nil +} + +type usdcPayload []byte + +func (d usdcPayload) AbiString() string { + return `[{"type": "bytes"}]` +} + +func (d usdcPayload) Validate() error { + return nil +} + func TestUSDCReader_ReadTokenData(t *testing.T) { response := attestationResponse{ Status: "complete", Attestation: "0x9049623e91719ef2aa63c55f357be2529b0e7122ae552c18aff8db58b4633c4d3920ff03d3a6d1ddf11f06bf64d7fd60d45447ac81f527ba628877dc5ca759651b08ffae25a6d3b1411749765244f0a1c131cbfe04430d687a2e12fd9d2e6dc08e118ad95d94ad832332cf3c4f7a4f3da0baa803b7be024b02db81951c0f0714de1b", } - - attestationBytes, err := hex.DecodeString(strings.TrimPrefix(response.Attestation, "0x")) + abiEncodedMessageBody, err := hexutil.Decode("0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000f80000000000000001000000020000000000048d71000000000000000000000000eb08f243e5d3fcff26a9e38ae5520a669f4019d000000000000000000000000023a04d5935ed8bc8e3eb78db3541f0abfb001c6e0000000000000000000000006cb3ed9b441eb674b58495c8b3324b59faff5243000000000000000000000000000000005425890298aed601595a70ab815c96711a31bc65000000000000000000000000ab4f961939bfe6a93567cc57c59eed7084ce2131000000000000000000000000000000000000000000000000000000000000271000000000000000000000000035e08285cfed1ef159236728f843286c55fc08610000000000000000") require.NoError(t, err) - - responseBytes, err := json.Marshal(response) + rawMessageBody, err := abihelpers.DecodeAbiStruct[usdcPayload](abiEncodedMessageBody) require.NoError(t, err) ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - _, err = w.Write(responseBytes) - require.NoError(t, err) + messageHash := utils.Keccak256Fixed(rawMessageBody) + expectedUrl := "/v1/attestations/0x" + hex.EncodeToString(messageHash[:]) + require.Equal(t, expectedUrl, r.URL.Path) + + responseBytes, err2 := json.Marshal(response) + require.NoError(t, err2) + + _, err2 = w.Write(responseBytes) + require.NoError(t, err2) })) defer ts.Close() @@ -77,19 +113,16 @@ func TestUSDCReader_ReadTokenData(t *testing.T) { }, }, nil) - expectedBody, err := hexutil.Decode("0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000f80000000000000001000000020000000000048d71000000000000000000000000eb08f243e5d3fcff26a9e38ae5520a669f4019d000000000000000000000000023a04d5935ed8bc8e3eb78db3541f0abfb001c6e0000000000000000000000006cb3ed9b441eb674b58495c8b3324b59faff5243000000000000000000000000000000005425890298aed601595a70ab815c96711a31bc65000000000000000000000000ab4f961939bfe6a93567cc57c59eed7084ce2131000000000000000000000000000000000000000000000000000000000000271000000000000000000000000035e08285cfed1ef159236728f843286c55fc08610000000000000000") - require.NoError(t, err) - eventsClient.On("GetLastUSDCMessagePriorToLogIndexInTx", mock.Anything, logIndex, common.Hash(txHash), - ).Return(expectedBody, nil) + ).Return(abiEncodedMessageBody, nil) attestationURI, err := url.ParseRequestURI(ts.URL) require.NoError(t, err) usdcService := usdc.NewUSDCTokenDataReader(logger.TestLogger(t), &eventsClient, mockUSDCTokenAddress, mockMsgTransmitter, mockOnRampAddress, attestationURI) - attestation, err := usdcService.ReadTokenData(context.Background(), internal.EVM2EVMOnRampCCIPSendRequestedWithMeta{ + msgAndAttestation, err := usdcService.ReadTokenData(context.Background(), internal.EVM2EVMOnRampCCIPSendRequestedWithMeta{ InternalEVM2EVMMessage: evm_2_evm_offramp.InternalEVM2EVMMessage{ SequenceNumber: seqNum, }, @@ -98,5 +131,14 @@ func TestUSDCReader_ReadTokenData(t *testing.T) { }) require.NoError(t, err) - require.Equal(t, attestationBytes, attestation) + attestationBytes, err := hex.DecodeString(strings.TrimPrefix(response.Attestation, "0x")) + require.NoError(t, err) + + encodeAbiStruct, err := abihelpers.EncodeAbiStruct[messageAndAttestation](messageAndAttestation{ + Message: rawMessageBody, + Attestation: attestationBytes, + }) + require.NoError(t, err) + + require.Equal(t, encodeAbiStruct, msgAndAttestation) } 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 4072f6d13c..b383b5e228 100644 --- a/core/services/ocr2/plugins/ccip/tokendata/usdc/usdc_test.go +++ b/core/services/ocr2/plugins/ccip/tokendata/usdc/usdc_test.go @@ -108,23 +108,21 @@ func TestGetUSDCMessageBody(t *testing.T) { require.Equal(t, expectedPostParse, hexutil.Encode(parsedBody)) - expectedBodyHash := utils.Keccak256Fixed(parsedBody) - sourceChainEventsMock := ccipdata.MockReader{} sourceChainEventsMock.On("GetLastUSDCMessagePriorToLogIndexInTx", mock.Anything, mock.Anything, mock.Anything).Return(expectedBody, nil) usdcService := NewUSDCTokenDataReader(logger.TestLogger(t), &sourceChainEventsMock, mockUSDCTokenAddress, mockMsgTransmitter, mockOnRampAddress, nil) // Make the first call and assert the underlying function is called - body, err := usdcService.getUSDCMessageBodyHash(context.Background(), internal.EVM2EVMOnRampCCIPSendRequestedWithMeta{}) + body, err := usdcService.getUSDCMessageBody(context.Background(), internal.EVM2EVMOnRampCCIPSendRequestedWithMeta{}) require.NoError(t, err) - require.Equal(t, body, expectedBodyHash) + require.Equal(t, body, parsedBody) sourceChainEventsMock.AssertNumberOfCalls(t, "GetLastUSDCMessagePriorToLogIndexInTx", 1) // Make another call and assert that the cache is used - body, err = usdcService.getUSDCMessageBodyHash(context.Background(), internal.EVM2EVMOnRampCCIPSendRequestedWithMeta{}) + body, err = usdcService.getUSDCMessageBody(context.Background(), internal.EVM2EVMOnRampCCIPSendRequestedWithMeta{}) require.NoError(t, err) - require.Equal(t, body, expectedBodyHash) + require.Equal(t, body, parsedBody) sourceChainEventsMock.AssertNumberOfCalls(t, "GetLastUSDCMessagePriorToLogIndexInTx", 1) }