Skip to content

Commit

Permalink
add message to usdc payload (#158)
Browse files Browse the repository at this point in the history
  • Loading branch information
RensR authored Sep 25, 2023
1 parent ba1a042 commit 0031e07
Show file tree
Hide file tree
Showing 3 changed files with 142 additions and 65 deletions.
131 changes: 84 additions & 47 deletions core/services/ocr2/plugins/ccip/tokendata/usdc/usdc.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand All @@ -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 {
Expand All @@ -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 {
Expand All @@ -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()

Expand All @@ -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) {
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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()
Expand All @@ -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,
},
Expand All @@ -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)
}
10 changes: 4 additions & 6 deletions core/services/ocr2/plugins/ccip/tokendata/usdc/usdc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

0 comments on commit 0031e07

Please sign in to comment.