From 5b8286d3e3f3404a9453b836e0baf15652b98525 Mon Sep 17 00:00:00 2001 From: Aleksandr Bukata Date: Fri, 7 Feb 2025 17:23:45 +0700 Subject: [PATCH] CCIP-5109 LBTC tokendata --- execute/observation_test.go | 4 +- execute/plugin_test.go | 3 +- execute/plugin_tokendata_test.go | 177 ++++++++ execute/plugin_usdc_test.go | 117 ----- execute/test_utils.go | 130 ++++-- execute/tokendata/http/http.go | 17 +- execute/tokendata/http/http_test.go | 87 +--- execute/tokendata/lbtc/attestation.go | 182 ++++++++ execute/tokendata/lbtc/lbtc.go | 156 +++++++ execute/tokendata/lbtc/lbtc_int_test.go | 475 ++++++++++++++++++++ execute/tokendata/lbtc/lbtc_test.go | 260 +++++++++++ execute/tokendata/observer/observer.go | 11 +- execute/tokendata/observer/observer_test.go | 3 +- execute/tokendata/token_data.go | 13 +- execute/tokendata/usdc/attestation.go | 5 +- execute/tokendata/usdc/attestation_test.go | 93 ++-- execute/tokendata/usdc/usdc.go | 11 +- execute/tokendata/usdc/usdc_int_test.go | 99 +++- execute/tokendata/usdc/usdc_test.go | 3 +- go.mod | 5 +- go.sum | 48 ++ internal/test_utils.go | 8 + pluginconfig/token.go | 175 ++++---- pluginconfig/token_test.go | 17 +- 24 files changed, 1714 insertions(+), 385 deletions(-) create mode 100644 execute/plugin_tokendata_test.go delete mode 100644 execute/plugin_usdc_test.go create mode 100644 execute/tokendata/lbtc/attestation.go create mode 100644 execute/tokendata/lbtc/lbtc.go create mode 100644 execute/tokendata/lbtc/lbtc_int_test.go create mode 100644 execute/tokendata/lbtc/lbtc_test.go diff --git a/execute/observation_test.go b/execute/observation_test.go index c85751a5f..6ec2f9966 100644 --- a/execute/observation_test.go +++ b/execute/observation_test.go @@ -14,7 +14,7 @@ import ( "github.com/smartcontractkit/chainlink-ccip/execute/costlymessages" "github.com/smartcontractkit/chainlink-ccip/execute/exectypes" "github.com/smartcontractkit/chainlink-ccip/execute/internal/cache" - "github.com/smartcontractkit/chainlink-ccip/execute/tokendata" + "github.com/smartcontractkit/chainlink-ccip/execute/tokendata/observer" "github.com/smartcontractkit/chainlink-ccip/internal/mocks" "github.com/smartcontractkit/chainlink-ccip/mocks/internal_/reader" readerpkg_mock "github.com/smartcontractkit/chainlink-ccip/mocks/pkg/reader" @@ -109,7 +109,7 @@ func Test_getMessagesObservation(t *testing.T) { // Create mock objects ccipReader := readerpkg_mock.NewMockCCIPReader(t) msgHasher := mocks.NewMessageHasher() - tokenDataObserver := tokendata.NoopTokenDataObserver{} + tokenDataObserver := observer.NoopTokenDataObserver{} costlyMessageObserver := costlymessages.NoopObserver{} //emptyMsgHash, err := msgHasher.Hash(ctx, cciptypes.Message{}) diff --git a/execute/plugin_test.go b/execute/plugin_test.go index 7095edfef..58279ecbf 100644 --- a/execute/plugin_test.go +++ b/execute/plugin_test.go @@ -19,10 +19,11 @@ import ( "github.com/smartcontractkit/libocr/offchainreporting2plus/types" libocrtypes "github.com/smartcontractkit/libocr/ragep2p/types" - "github.com/smartcontractkit/chainlink-ccip/execute/tokendata/observer" "github.com/smartcontractkit/chainlink-common/pkg/logger" "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" + "github.com/smartcontractkit/chainlink-ccip/execute/tokendata/observer" + "github.com/smartcontractkit/chainlink-ccip/execute/exectypes" "github.com/smartcontractkit/chainlink-ccip/internal/libs/testhelpers/rand" "github.com/smartcontractkit/chainlink-ccip/internal/plugincommon" diff --git a/execute/plugin_tokendata_test.go b/execute/plugin_tokendata_test.go new file mode 100644 index 000000000..a8874216a --- /dev/null +++ b/execute/plugin_tokendata_test.go @@ -0,0 +1,177 @@ +package execute + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + sel "github.com/smartcontractkit/chain-selectors" + + logger2 "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" + + "github.com/smartcontractkit/chainlink-ccip/internal" + "github.com/smartcontractkit/chainlink-ccip/internal/libs/testhelpers" + + "github.com/smartcontractkit/chainlink-ccip/execute/exectypes" + "github.com/smartcontractkit/chainlink-ccip/internal/mocks/inmem" + "github.com/smartcontractkit/chainlink-ccip/pkg/ocrtypecodec" + readerpkg "github.com/smartcontractkit/chainlink-ccip/pkg/reader" + cciptypes "github.com/smartcontractkit/chainlink-ccip/pkg/types/ccipocr3" +) + +func runRoundAndGetOutcome(ctx context.Context, ocrTypeCodec ocrtypecodec.ExecCodec, + t *testing.T, r *testhelpers.OCR3Runner[[]byte]) exectypes.Outcome { + result, err := r.RunRound(ctx) + require.NoError(t, err) + outcome, err := ocrTypeCodec.DecodeOutcome(result.Outcome) + require.NoError(t, err) + return outcome +} + +func Test_LBTC_USDC_Transfer(t *testing.T) { + ocrTypeCodec := ocrtypecodec.NewExecCodecJSON() + ctx := tests.Context(t) + + sourceChain := cciptypes.ChainSelector(sel.ETHEREUM_TESTNET_SEPOLIA.Selector) + destChain := cciptypes.ChainSelector(sel.ETHEREUM_MAINNET_BASE_1.Selector) + + usdcAddress := "0x3765b189a8fe4a0bc34457835f01c9d178dbea60" + usdcAddressBytes, err := cciptypes.NewUnknownAddressFromHex(usdcAddress) + require.NoError(t, err) + + lbtcAddress := "0xc791ec14ad1d566425f006eec12a300343164ab1" + lbtcAddressBytes, err := cciptypes.NewUnknownAddressFromHex(lbtcAddress) + require.NoError(t, err) + lbtcMessageHash1 := internal.MustDecode("0xa9165956caf08b3da46db4cccdd58098b2c3a90e57372f3f28d7d46672e2091b") + lbtcMessageHash2 := internal.MustDecode("0xc317b01e5a87000f8c51517227ea9ff07c9f4da646e8209c56424dc85ff50fe7") + lbtcMessageHash3 := internal.MustDecode("0x5f9b38941ce144fad0b6890a3e15bc67c7b51bb89751ab6f672f088f44e36b91") + + messages := []inmem.MessagesWithMetadata{ + makeMsgWithMetadata(102, sourceChain, destChain, false), + makeMsgWithMetadata(103, sourceChain, destChain, false), + makeMsgWithMetadata(104, sourceChain, destChain, false, withTokens(cciptypes.RampTokenAmount{ + SourcePoolAddress: usdcAddressBytes, + ExtraData: readerpkg.NewSourceTokenDataPayload(1, 0).ToBytes(), + })), + makeMsgWithMetadata(105, sourceChain, destChain, false, withTokens(cciptypes.RampTokenAmount{ + SourcePoolAddress: usdcAddressBytes, + ExtraData: readerpkg.NewSourceTokenDataPayload(2, 0).ToBytes(), + })), + makeMsgWithMetadata(106, sourceChain, destChain, false, withTokens(cciptypes.RampTokenAmount{ + SourcePoolAddress: lbtcAddressBytes, + ExtraData: lbtcMessageHash1, + })), + makeMsgWithMetadata(107, sourceChain, destChain, false, withTokens(cciptypes.RampTokenAmount{ + SourcePoolAddress: lbtcAddressBytes, + ExtraData: lbtcMessageHash2, + })), + makeMsgWithMetadata(108, sourceChain, destChain, false, + withTokens(cciptypes.RampTokenAmount{ + SourcePoolAddress: usdcAddressBytes, + ExtraData: readerpkg.NewSourceTokenDataPayload(3, 0).ToBytes(), + }, cciptypes.RampTokenAmount{ + SourcePoolAddress: lbtcAddressBytes, + ExtraData: lbtcMessageHash3, + }), + ), + } + + events := []*readerpkg.MessageSentEvent{ + newMessageSentEvent(0, 6, 1, []byte{1}), + newMessageSentEvent(0, 6, 2, []byte{2}), + newMessageSentEvent(0, 6, 3, []byte{3}), + } + + usdcAttestation104_108 := map[string]string{ + "0x0f43587da5355551d234a2ba24dde8edfe0e385346465d6d53653b6aa642992e": `{ + "status": "complete", + "attestation": "0x100001" + }`, + "0x2b235443d276ec7dd517dcf34cca9dcd34f33542ccb6f305828d98e777404b63": `{ + "status": "complete", + "attestation": "0x100003" + }`, + } + + lbtcAttestation106_108 := map[string]string{ + lbtcMessageHash1.String(): `{ + "message_hash": "0xa9165956caf08b3da46db4cccdd58098b2c3a90e57372f3f28d7d46672e2091b", + "status": "NOTARIZATION_STATUS_SESSION_APPROVED", + "attestation": "0x200001" + }`, + lbtcMessageHash3.String(): `{ + "message_hash": "0x5f9b38941ce144fad0b6890a3e15bc67c7b51bb89751ab6f672f088f44e36b91", + "status": "NOTARIZATION_STATUS_SESSION_APPROVED", + "attestation": "0x200003" + }`, + } + + intTest := SetupSimpleTest(t, logger2.Test(t), sourceChain, destChain) + intTest.WithMessages(messages, 1000, time.Now().Add(-4*time.Hour), 1) + intTest.WithUSDC(usdcAddress, usdcAttestation104_108, events) + intTest.WithLBTC(lbtcAddress, lbtcAttestation106_108) + runner := intTest.Start() + defer intTest.Close() + + // Contract Discovery round. + outcome := runRoundAndGetOutcome(ctx, ocrTypeCodec, t, runner) + require.Equal(t, exectypes.Initialized, outcome.State) + + // Round 1 - Get Commit Reports + outcome = runRoundAndGetOutcome(ctx, ocrTypeCodec, t, runner) + require.Len(t, outcome.Report.ChainReports, 0) + require.Len(t, outcome.CommitReports, 1) + + // Round 2 - Get Messages + outcome = runRoundAndGetOutcome(ctx, ocrTypeCodec, t, runner) + require.Len(t, outcome.Report.ChainReports, 0) + require.Len(t, outcome.CommitReports, 1) + + // Round 3 - Filter + // Messages 102-104,106,108 are executed, 105 and 107 don't have token data ready + outcome = runRoundAndGetOutcome(ctx, ocrTypeCodec, t, runner) + require.NoError(t, err) + assert.Len(t, outcome.Report.ChainReports, 1) + sequenceNumbers := extractSequenceNumbers(outcome.Report.ChainReports[0].Messages) + assert.ElementsMatch(t, sequenceNumbers, []cciptypes.SeqNum{102, 103, 104, 106, 108}) + //Attestation data added to the USDC + assert.Equal(t, internal.MustDecode("0x100001"), outcome.Report.ChainReports[0].OffchainTokenData[2][0]) + //Attestation data added to the LBTC + assert.Equal(t, internal.MustDecode("0x200001"), outcome.Report.ChainReports[0].OffchainTokenData[3][0]) + //Attestation data added to the USDC+LBTC + assert.Equal(t, internal.MustDecode("0x100003"), outcome.Report.ChainReports[0].OffchainTokenData[4][0]) + assert.Equal(t, internal.MustDecode("0x200003"), outcome.Report.ChainReports[0].OffchainTokenData[4][1]) + + intTest.usdcServer.AddResponse( + "0x70ef528624085241badbff913575c0ab50241e7cb6db183a5614922ab0bcba5d", + `{ + "status": "complete", + "attestation": "0x100002" + }`) + + intTest.lbtcServer.AddResponse( + lbtcMessageHash2.String(), + `{ + "message_hash": "0xc317b01e5a87000f8c51517227ea9ff07c9f4da646e8209c56424dc85ff50fe7", + "status": "NOTARIZATION_STATUS_SESSION_APPROVED", + "attestation": "0x200002" + }`) + + // Run 3 more rounds to get all attestations + for i := 0; i < 3; i++ { + outcome = runRoundAndGetOutcome(ctx, ocrTypeCodec, t, runner) + } + + assert.Len(t, outcome.Report.ChainReports, 1) + sequenceNumbers = extractSequenceNumbers(outcome.Report.ChainReports[0].Messages) + // 102, 103 and 104 are in the inflight message cache. + assert.ElementsMatch(t, sequenceNumbers, []cciptypes.SeqNum{105, 107}) + //Attestation data added to the remaining USDC messages + assert.Equal(t, internal.MustDecode("0x100002"), outcome.Report.ChainReports[0].OffchainTokenData[0][0]) + //Attestation data added to the remaining LBTC messages + assert.Equal(t, internal.MustDecode("0x200002"), outcome.Report.ChainReports[0].OffchainTokenData[1][0]) +} diff --git a/execute/plugin_usdc_test.go b/execute/plugin_usdc_test.go deleted file mode 100644 index 10bd5db97..000000000 --- a/execute/plugin_usdc_test.go +++ /dev/null @@ -1,117 +0,0 @@ -package execute - -import ( - "context" - "testing" - "time" - - "github.com/stretchr/testify/require" - - sel "github.com/smartcontractkit/chain-selectors" - logger2 "github.com/smartcontractkit/chainlink-common/pkg/logger" - "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" - - "github.com/smartcontractkit/chainlink-ccip/execute/exectypes" - "github.com/smartcontractkit/chainlink-ccip/internal/libs/testhelpers" - "github.com/smartcontractkit/chainlink-ccip/internal/libs/testhelpers/rand" - "github.com/smartcontractkit/chainlink-ccip/internal/mocks/inmem" - "github.com/smartcontractkit/chainlink-ccip/pkg/ocrtypecodec" - readerpkg "github.com/smartcontractkit/chainlink-ccip/pkg/reader" - cciptypes "github.com/smartcontractkit/chainlink-ccip/pkg/types/ccipocr3" -) - -func runRoundAndGetOutcome(ctx context.Context, ocrTypeCodec ocrtypecodec.ExecCodec, - t *testing.T, r *testhelpers.OCR3Runner[[]byte]) exectypes.Outcome { - result, err := r.RunRound(ctx) - require.NoError(t, err) - outcome, err := ocrTypeCodec.DecodeOutcome(result.Outcome) - require.NoError(t, err) - return outcome -} - -func Test_USDC_Transfer(t *testing.T) { - ocrTypeCodec := ocrtypecodec.NewExecCodecJSON() - ctx := tests.Context(t) - randomEthAddress := string(rand.RandomAddress()) - - sourceChain := cciptypes.ChainSelector(sel.ETHEREUM_TESTNET_SEPOLIA.Selector) - destChain := cciptypes.ChainSelector(sel.ETHEREUM_MAINNET_BASE_1.Selector) - - addressBytes, err := cciptypes.NewUnknownAddressFromHex(randomEthAddress) - require.NoError(t, err) - - messages := []inmem.MessagesWithMetadata{ - makeMsgWithMetadata(102, sourceChain, destChain, false), - makeMsgWithMetadata(103, sourceChain, destChain, false), - makeMsgWithMetadata(104, sourceChain, destChain, false, withTokens(cciptypes.RampTokenAmount{ - SourcePoolAddress: addressBytes, - ExtraData: readerpkg.NewSourceTokenDataPayload(1, 0).ToBytes(), - })), - makeMsgWithMetadata(105, sourceChain, destChain, false, withTokens(cciptypes.RampTokenAmount{ - SourcePoolAddress: addressBytes, - ExtraData: readerpkg.NewSourceTokenDataPayload(2, 0).ToBytes(), - })), - } - - events := []*readerpkg.MessageSentEvent{ - newMessageSentEvent(0, 6, 1, []byte{1}), - newMessageSentEvent(0, 6, 2, []byte{2}), - newMessageSentEvent(0, 6, 3, []byte{3}), - } - - attestation104 := map[string]string{ - "0x0f43587da5355551d234a2ba24dde8edfe0e385346465d6d53653b6aa642992e": `{ - "status": "complete", - "attestation": "0x720502893578a89a8a87982982ef781c18b193" - }`, - } - - intTest := SetupSimpleTest(t, logger2.Test(t), sourceChain, destChain) - intTest.WithMessages(messages, 1000, time.Now().Add(-4*time.Hour), 1) - intTest.WithUSDC(randomEthAddress, attestation104, events) - runner := intTest.Start() - defer intTest.Close() - - // Contract Discovery round. - outcome := runRoundAndGetOutcome(ctx, ocrTypeCodec, t, runner) - require.Equal(t, exectypes.Initialized, outcome.State) - - // Round 1 - Get Commit Reports - outcome = runRoundAndGetOutcome(ctx, ocrTypeCodec, t, runner) - require.Len(t, outcome.Report.ChainReports, 0) - require.Len(t, outcome.CommitReports, 1) - - // Round 2 - Get Messages - outcome = runRoundAndGetOutcome(ctx, ocrTypeCodec, t, runner) - require.Len(t, outcome.Report.ChainReports, 0) - require.Len(t, outcome.CommitReports, 1) - - // Round 3 - Filter - // Messages 102-104 are executed, 105 doesn't have token data ready - outcome = runRoundAndGetOutcome(ctx, ocrTypeCodec, t, runner) - require.NoError(t, err) - require.Len(t, outcome.Report.ChainReports, 1) - sequenceNumbers := extractSequenceNumbers(outcome.Report.ChainReports[0].Messages) - require.ElementsMatch(t, sequenceNumbers, []cciptypes.SeqNum{102, 103, 104}) - //Attestation data added to the USDC - require.NotEmpty(t, outcome.Report.ChainReports[0].OffchainTokenData[2]) - - intTest.server.AddResponse( - "0x70ef528624085241badbff913575c0ab50241e7cb6db183a5614922ab0bcba5d", - `{ - "status": "complete", - "attestation": "0x720502893578a89a8a87982982ef781c18b194" - }`) - - // Run 3 more rounds to get all attestations - for i := 0; i < 3; i++ { - outcome = runRoundAndGetOutcome(ctx, ocrTypeCodec, t, runner) - } - - require.Len(t, outcome.Report.ChainReports, 1) - sequenceNumbers = extractSequenceNumbers(outcome.Report.ChainReports[0].Messages) - // 102, 103 and 104 are in the inflight message cache. - require.ElementsMatch(t, sequenceNumbers, []cciptypes.SeqNum{105}) - //Attestation data added to the remaining USDC messages - require.NotEmpty(t, outcome.Report.ChainReports[0].OffchainTokenData[0]) -} diff --git a/execute/test_utils.go b/execute/test_utils.go index 4bfc87479..b9967cbc0 100644 --- a/execute/test_utils.go +++ b/execute/test_utils.go @@ -3,9 +3,12 @@ package execute import ( "context" "encoding/binary" + "encoding/json" + "io" "math/big" "net/http" "net/http/httptest" + "slices" "strings" "testing" "time" @@ -56,7 +59,8 @@ type IntTest struct { msgHasher cciptypes.MessageHasher ccipReader *inmem.InMemoryCCIPReader - server *ConfigurableAttestationServer + usdcServer *ConfigurableAttestationServer + lbtcServer *ConfigurableAttestationServer tokenObserverConfig []pluginconfig.TokenDataObserverConfig tokenChainReader map[cciptypes.ChainSelector]contractreader.ContractReaderFacade feeCalculator *costlymessages.CCIPMessageFeeUSD18Calculator @@ -169,26 +173,24 @@ func (it *IntTest) WithUSDC( attestations map[string]string, events []*readerpkg.MessageSentEvent, ) { - it.server = newConfigurableAttestationServer(attestations) - it.tokenObserverConfig = []pluginconfig.TokenDataObserverConfig{ - { - Type: "usdc-cctp", - Version: "1", - USDCCCTPObserverConfig: &pluginconfig.USDCCCTPObserverConfig{ - AttestationConfig: pluginconfig.AttestationConfig{ - AttestationAPI: it.server.server.URL, - AttestationAPIInterval: commonconfig.MustNewDuration(1 * time.Millisecond), - AttestationAPITimeout: commonconfig.MustNewDuration(1 * time.Second), - }, - Tokens: map[cciptypes.ChainSelector]pluginconfig.USDCCCTPTokenConfig{ - it.srcSelector: { - SourcePoolAddress: sourcePoolAddress, - SourceMessageTransmitterAddr: sourcePoolAddress, - }, + it.usdcServer = newConfigurableAttestationServer(attestations) + it.tokenObserverConfig = append(it.tokenObserverConfig, pluginconfig.TokenDataObserverConfig{ + Type: "usdc-cctp", + Version: "1", + USDCCCTPObserverConfig: &pluginconfig.USDCCCTPObserverConfig{ + AttestationConfig: pluginconfig.AttestationConfig{ + AttestationAPI: it.usdcServer.server.URL, + AttestationAPIInterval: commonconfig.MustNewDuration(1 * time.Millisecond), + AttestationAPITimeout: commonconfig.MustNewDuration(1 * time.Second), + }, + Tokens: map[cciptypes.ChainSelector]pluginconfig.USDCCCTPTokenConfig{ + it.srcSelector: { + SourcePoolAddress: sourcePoolAddress, + SourceMessageTransmitterAddr: sourcePoolAddress, }, }, }, - } + }) usdcEvents := make([]types.Sequence, len(events)) for i, e := range events { @@ -211,6 +213,28 @@ func (it *IntTest) WithUSDC( } } +func (it *IntTest) WithLBTC( + sourcePoolAddress string, + attestations map[string]string, +) { + it.lbtcServer = newConfigurableAttestationServer(attestations) + it.tokenObserverConfig = append(it.tokenObserverConfig, pluginconfig.TokenDataObserverConfig{ + Type: "lbtc", + Version: "1", + LBTCObserverConfig: &pluginconfig.LBTCObserverConfig{ + AttestationConfig: pluginconfig.AttestationConfig{ + AttestationAPI: it.lbtcServer.server.URL, + AttestationAPIInterval: commonconfig.MustNewDuration(1 * time.Millisecond), + AttestationAPITimeout: commonconfig.MustNewDuration(1 * time.Second), + }, + AttestationAPIBatchSize: 1, + SourcePoolAddressByChain: map[cciptypes.ChainSelector]string{ + it.srcSelector: sourcePoolAddress, + }, + }, + }) +} + func (it *IntTest) Start() *testhelpers.OCR3Runner[[]byte] { cfg := pluginconfig.ExecuteOffchainConfig{ MessageVisibilityInterval: *commonconfig.MustNewDuration(8 * time.Hour), @@ -301,8 +325,11 @@ func (it *IntTest) Start() *testhelpers.OCR3Runner[[]byte] { } func (it *IntTest) Close() { - if it.server != nil { - it.server.Close() + if it.usdcServer != nil { + it.usdcServer.Close() + } + if it.lbtcServer != nil { + it.lbtcServer.Close() } } @@ -373,13 +400,43 @@ func newConfigurableAttestationServer(responses map[string]string) *Configurable } server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - for url, response := range c.responses { - if strings.Contains(r.RequestURI, url) { - _, err := w.Write([]byte(response)) - if err != nil { - panic(err) + if r.Method == http.MethodGet { + for url, response := range c.responses { + if strings.Contains(r.RequestURI, url) { + _, err := w.Write([]byte(response)) + if err != nil { + panic(err) + } + return } - return + } + } + if r.Method == http.MethodPost { + var request map[string]interface{} + bodyRaw, err := io.ReadAll(r.Body) + if err != nil { + panic(err) + } + err = json.Unmarshal(bodyRaw, &request) + if err != nil { + panic(err) + } + payloadHashesUntyped := request["messageHash"].([]interface{}) + if len(payloadHashesUntyped) == 0 { + w.WriteHeader(http.StatusBadRequest) + } + payloadHashes := make([]string, len(payloadHashesUntyped)) + for i, hash := range payloadHashesUntyped { + payloadHashes[i] = hash.(string) + } + attestationResponse := attestationBatchByMessageHashes(payloadHashes, c.responses) + responseBytes, err := json.Marshal(attestationResponse) + if err != nil { + panic(err) + } + _, err = w.Write(responseBytes) + if err != nil { + panic(err) } } w.WriteHeader(http.StatusNotFound) @@ -388,8 +445,25 @@ func newConfigurableAttestationServer(responses map[string]string) *Configurable return c } -func (c *ConfigurableAttestationServer) AddResponse(url, response string) { - c.responses[url] = response +func attestationBatchByMessageHashes(payloadHashes []string, responses map[string]string) map[string]interface{} { + attestations := make([]interface{}, 0, len(responses)) + for payloadHash, attestationRaw := range responses { + if slices.Contains(payloadHashes, payloadHash) { + var attestation map[string]interface{} + err := json.Unmarshal([]byte(attestationRaw), &attestation) + if err != nil { + panic(err) + } + attestations = append(attestations, attestation) + } + } + attestationResponse := make(map[string]interface{}) + attestationResponse["attestations"] = attestations + return attestationResponse +} + +func (c *ConfigurableAttestationServer) AddResponse(key, response string) { + c.responses[key] = response } func (c *ConfigurableAttestationServer) Close() { diff --git a/execute/tokendata/http/http.go b/execute/tokendata/http/http.go index 19a9d9d49..635007f05 100644 --- a/execute/tokendata/http/http.go +++ b/execute/tokendata/http/http.go @@ -14,9 +14,10 @@ import ( "golang.org/x/time/rate" - "github.com/smartcontractkit/chainlink-ccip/execute/tokendata" "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-ccip/execute/tokendata" + "github.com/smartcontractkit/chainlink-ccip/pkg/logutil" cciptypes "github.com/smartcontractkit/chainlink-ccip/pkg/types/ccipocr3" ) @@ -129,7 +130,11 @@ func (h *httpClient) Get(ctx context.Context, requestPath string) (cciptypes.Byt return response, httpStatus, err } -func (h *httpClient) Post(ctx context.Context, requestPath string, requestData cciptypes.Bytes) (cciptypes.Bytes, HTTPStatus, error) { +func (h *httpClient) Post( + ctx context.Context, + requestPath string, + requestData cciptypes.Bytes, +) (cciptypes.Bytes, HTTPStatus, error) { lggr := logutil.WithContextValues(ctx, h.lggr) requestURL := *h.apiURL @@ -146,7 +151,13 @@ func (h *httpClient) Post(ctx context.Context, requestPath string, requestData c return response, httpStatus, err } -func (h *httpClient) callAPI(ctx context.Context, lggr logger.Logger, method string, url url.URL, body io.Reader) (cciptypes.Bytes, HTTPStatus, error) { +func (h *httpClient) callAPI( + ctx context.Context, + lggr logger.Logger, + method string, + url url.URL, + body io.Reader, +) (cciptypes.Bytes, HTTPStatus, error) { // Terminate immediately when rate limited if coolDown, duration := h.inCoolDownPeriod(); coolDown { lggr.Errorw( diff --git a/execute/tokendata/http/http_test.go b/execute/tokendata/http/http_test.go index e23ab6c7e..53ef60c5c 100644 --- a/execute/tokendata/http/http_test.go +++ b/execute/tokendata/http/http_test.go @@ -3,7 +3,6 @@ package http import ( "context" "errors" - "fmt" "net/http" "net/http/httptest" "net/url" @@ -12,14 +11,14 @@ import ( "testing" "time" - "github.com/ethereum/go-ethereum/common/hexutil" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/smartcontractkit/chainlink-ccip/execute/tokendata" "github.com/smartcontractkit/chainlink-common/pkg/logger" "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" + "github.com/smartcontractkit/chainlink-ccip/execute/tokendata" + "github.com/smartcontractkit/chainlink-ccip/internal/mocks" cciptypes "github.com/smartcontractkit/chainlink-ccip/pkg/types/ccipocr3" ) @@ -34,10 +33,6 @@ var ( "status": "complete", "attestation": "0x720502893578a89a8a87982982ef781c18b193" }`) - failedAttestationResponse = []byte(` - { - "error": "some error" - }`) ) func Test_NewHTTPClient_New(t *testing.T) { @@ -111,73 +106,6 @@ func Test_HTTPClient_Get(t *testing.T) { expectedError: tokendata.ErrTimeout, expectedStatusCode: http.StatusRequestTimeout, }, - { - name: "200 but attestation response contains error", - getTs: func(t *testing.T) *httptest.Server { - return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - _, err := w.Write(failedAttestationResponse) - require.NoError(t, err) - })) - }, - timeout: time.Hour, // not relevant to the test - expectedStatusCode: http.StatusOK, - expectedError: fmt.Errorf("attestation API error: some error"), - }, - { - name: "invalid status", - getTs: func(t *testing.T) *httptest.Server { - attestationResponse := []byte(` - { - "status": "complete", - "attestation": "0" - }`) - - return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - _, err := w.Write(attestationResponse) - require.NoError(t, err) - })) - }, - timeout: time.Hour, // not relevant to the test - expectedStatusCode: http.StatusOK, - expectedError: fmt.Errorf( - "failed to decode attestation hex: Bytes must be of at least length 2 (i.e, '0x' prefix): 0", - ), - }, - { - name: "invalid attestation", - getTs: func(t *testing.T) *httptest.Server { - attestationResponse := []byte(` - { - "status": "", - "attestation": "0x720502893578a89a8a87982982ef781c18b193" - }`) - - return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - _, err := w.Write(attestationResponse) - require.NoError(t, err) - })) - }, - timeout: time.Hour, - expectedStatusCode: http.StatusOK, - expectedError: fmt.Errorf("invalid attestation response"), - }, - { - name: "malformed response", - getTs: func(t *testing.T) *httptest.Server { - attestationResponse := []byte(` - { - "field": 2137 - }`) - - return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - _, err := w.Write(attestationResponse) - require.NoError(t, err) - })) - }, - timeout: time.Hour, - expectedStatusCode: http.StatusOK, - expectedError: fmt.Errorf("invalid attestation response"), - }, { name: "rate limit", getTs: func(t *testing.T) *httptest.Server { @@ -205,7 +133,7 @@ func Test_HTTPClient_Get(t *testing.T) { name: "success", getTs: func(t *testing.T) *httptest.Server { return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.RequestURI == "/v1/attestations/0x0102030400000000000000000000000000000000000000000000000000000000" { + if r.RequestURI == "/0x0102030400000000000000000000000000000000000000000000000000000000" { _, err := w.Write(validAttestationResponse) require.NoError(t, err) } else { @@ -216,7 +144,7 @@ func Test_HTTPClient_Get(t *testing.T) { messageHash: [32]byte{1, 2, 3, 4}, timeout: time.Hour, expectedStatusCode: http.StatusOK, - expectedResponse: hexutil.MustDecode("0x720502893578a89a8a87982982ef781c18b193"), + expectedResponse: validAttestationResponse, }, } @@ -259,7 +187,8 @@ func Test_HTTPClient_Cooldown(t *testing.T) { attestationURI, err := url.ParseRequestURI(ts.URL) require.NoError(t, err) - client, err := newHTTPClient(logger.Test(t), attestationURI.String(), 1*time.Millisecond, longTimeout, maxCoolDownDuration) + client, err := newHTTPClient(logger.Test(t), attestationURI.String(), + 1*time.Millisecond, longTimeout, maxCoolDownDuration) require.NoError(t, err) _, _, err = client.Get(tests.Context(t), cciptypes.Bytes32{1, 2, 3}.String()) require.EqualError(t, err, tokendata.ErrUnknownResponse.Error()) @@ -322,7 +251,9 @@ func Test_HTTPClient_CoolDownWithRetryHeader(t *testing.T) { attestationURI, err := url.ParseRequestURI(ts.URL) require.NoError(t, err) - client, err := newHTTPClient(logger.Test(t), attestationURI.String(), 1*time.Millisecond, time.Hour, maxCoolDownDuration) + client, err := newHTTPClient( + logger.Test(t), attestationURI.String(), 1*time.Millisecond, time.Hour, maxCoolDownDuration, + ) require.NoError(t, err) _, _, err = client.Get(tests.Context(t), cciptypes.Bytes32{1, 2, 3}.String()) require.EqualError(t, err, tokendata.ErrUnknownResponse.Error()) diff --git a/execute/tokendata/lbtc/attestation.go b/execute/tokendata/lbtc/attestation.go new file mode 100644 index 000000000..75dc21fca --- /dev/null +++ b/execute/tokendata/lbtc/attestation.go @@ -0,0 +1,182 @@ +package lbtc + +import ( + "context" + "encoding/hex" + "encoding/json" + "fmt" + "maps" + "time" + + "github.com/smartcontractkit/chainlink-common/pkg/logger" + + "github.com/smartcontractkit/chainlink-ccip/execute/tokendata" + "github.com/smartcontractkit/chainlink-ccip/execute/tokendata/http" + "github.com/smartcontractkit/chainlink-ccip/pkg/reader" + cciptypes "github.com/smartcontractkit/chainlink-ccip/pkg/types/ccipocr3" + "github.com/smartcontractkit/chainlink-ccip/pluginconfig" +) + +type AttestationStatus string + +const ( + apiVersion = "v1" + attestationPath = "deposits/getByHash" + defaultCoolDownDuration = 30 * time.Second + + attestationStatusUnspecified AttestationStatus = "NOTARIZATION_STATUS_UNSPECIFIED" + attestationStatusFailed AttestationStatus = "NOTARIZATION_STATUS_FAILED" + attestationStatusPending AttestationStatus = "NOTARIZATION_STATUS_PENDING" + attestationStatusSubmitted AttestationStatus = "NOTARIZATION_STATUS_SUBMITTED" + _ AttestationStatus = "NOTARIZATION_STATUS_SESSION_APPROVED" +) + +type attestationRequest struct { + PayloadHashes []string `json:"messageHash"` +} + +type AttestationResponse struct { + Attestations []MessageAttestationResponse `json:"attestations"` + // fields in case of error + Code int `json:"code,omitempty"` + Message string `json:"message,omitempty"` +} + +type MessageAttestationResponse struct { + MessageHash string `json:"message_hash"` + Status AttestationStatus `json:"status"` + Attestation string `json:"attestation,omitempty"` // Attestation represented by abi.encode(payload, proof) +} + +type LBTCAttestationClient struct { + lggr logger.Logger + config pluginconfig.LBTCObserverConfig + httpClient http.HTTPClient +} + +func NewLBTCAttestationClient( + lggr logger.Logger, + config pluginconfig.LBTCObserverConfig, +) (tokendata.AttestationClient, error) { + httpClient, err := http.GetHTTPClient( + lggr, + config.AttestationAPI, + config.AttestationAPIInterval.Duration(), + config.AttestationAPITimeout.Duration(), + defaultCoolDownDuration, + ) + if err != nil { + return nil, fmt.Errorf("get http client: %w", err) + } + return tokendata.NewObservedAttestationClient( + lggr, &LBTCAttestationClient{ + lggr: lggr, + config: config, + httpClient: httpClient, + }, + ), nil +} + +func (c *LBTCAttestationClient) Attestations( + ctx context.Context, + messages map[cciptypes.ChainSelector]map[reader.MessageTokenID]cciptypes.Bytes, +) (map[cciptypes.ChainSelector]map[reader.MessageTokenID]tokendata.AttestationStatus, error) { + attestations := make(map[string]tokendata.AttestationStatus) + batch := make([]string, 0, c.config.AttestationAPIBatchSize) + for _, tokenDatas := range messages { + for _, tokenData := range tokenDatas { + batch = append(batch, hex.EncodeToString(tokenData)) + if len(batch) == c.config.AttestationAPIBatchSize { + batchAttestations, err := c.fetchBatch(ctx, batch) + if err != nil { + return nil, err + } + maps.Copy(attestations, batchAttestations) + batch = make([]string, 0, c.config.AttestationAPIBatchSize) + } + } + } + if len(batch) > 0 { + batchAttestations, err := c.fetchBatch(ctx, batch) + if err != nil { + return nil, err + } + maps.Copy(attestations, batchAttestations) + } + res := make(map[cciptypes.ChainSelector]map[reader.MessageTokenID]tokendata.AttestationStatus) + for chainSelector, tokenDatas := range messages { + res[chainSelector] = make(map[reader.MessageTokenID]tokendata.AttestationStatus) + for messageTokenID, tokenData := range tokenDatas { + if attestation, ok := attestations[hex.EncodeToString(tokenData)]; ok { + res[chainSelector][messageTokenID] = attestation + } + } + } + return res, nil +} + +func (c *LBTCAttestationClient) Token() string { + return LBTCToken +} + +func (c *LBTCAttestationClient) fetchBatch( + ctx context.Context, + batch []string, +) (map[string]tokendata.AttestationStatus, error) { + request := attestationRequest{PayloadHashes: batch} + encodedRequest, err := json.Marshal(request) + if err != nil { + return nil, fmt.Errorf("failed to marshal attestation request: %w", err) + } + attestations := make(map[string]tokendata.AttestationStatus) + respRaw, _, err := c.httpClient.Post(ctx, fmt.Sprintf("bridge/%s/%s", apiVersion, attestationPath), encodedRequest) + if err != nil { + for _, inputMessageHash := range batch { + attestations[inputMessageHash] = tokendata.ErrorAttestationStatus(err) + } + // absorb api error to each token data status + return attestations, nil + } + var attestationResp AttestationResponse + err = json.Unmarshal(respRaw, &attestationResp) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal attestation response: %w", err) + } + if attestationResp.Code != 0 { + for _, inputMessageHash := range batch { + attestations[inputMessageHash] = tokendata.ErrorAttestationStatus( + fmt.Errorf("attestation request failed: %s", attestationResp.Message), + ) + } + } + for _, attestation := range attestationResp.Attestations { + attestations[attestation.MessageHash] = attestationToTokenData(attestation) + } + for _, inputMessageHash := range batch { + if _, ok := attestations[inputMessageHash]; !ok { + c.lggr.Warnw( + "Requested messageHash is missing in the response. Considering tokendata.ErrDataMissing", + "messageHash", inputMessageHash, + ) + } + } + return attestations, nil +} + +func attestationToTokenData(attestation MessageAttestationResponse) tokendata.AttestationStatus { + if attestation.Status == attestationStatusSubmitted || attestation.Status == attestationStatusPending { + return tokendata.ErrorAttestationStatus(tokendata.ErrNotReady) + } + if attestation.Status == attestationStatusFailed || attestation.Status == attestationStatusUnspecified { + return tokendata.ErrorAttestationStatus(tokendata.ErrUnknownResponse) + } + payloadHashBytes, err := cciptypes.NewBytesFromString(attestation.MessageHash) + if err != nil { + return tokendata.ErrorAttestationStatus(fmt.Errorf("failed to decode message hash in attestation: %w", err)) + } + attestationBytes, err := cciptypes.NewBytesFromString(attestation.Attestation) + if err != nil { + return tokendata.ErrorAttestationStatus(fmt.Errorf("failed to decode attestation: %w", err)) + } + return tokendata.SuccessAttestationStatus(payloadHashBytes, attestationBytes) +} diff --git a/execute/tokendata/lbtc/lbtc.go b/execute/tokendata/lbtc/lbtc.go new file mode 100644 index 000000000..f67ec3d56 --- /dev/null +++ b/execute/tokendata/lbtc/lbtc.go @@ -0,0 +1,156 @@ +package lbtc + +import ( + "context" + "fmt" + "strings" + + "github.com/smartcontractkit/chainlink-common/pkg/logger" + + "github.com/smartcontractkit/chainlink-ccip/execute/exectypes" + "github.com/smartcontractkit/chainlink-ccip/execute/tokendata" + "github.com/smartcontractkit/chainlink-ccip/pkg/reader" + cciptypes "github.com/smartcontractkit/chainlink-ccip/pkg/types/ccipocr3" + "github.com/smartcontractkit/chainlink-ccip/pluginconfig" +) + +const ( + LBTCToken = "lbtc" +) + +type LBTCTokenDataObserver struct { + lggr logger.Logger + destChainSelector cciptypes.ChainSelector + supportedPoolsBySelector map[cciptypes.ChainSelector]string + client tokendata.AttestationClient +} + +func NewLBTCTokenDataObserver( + lggr logger.Logger, + destChainSelector cciptypes.ChainSelector, + config pluginconfig.LBTCObserverConfig, +) (*LBTCTokenDataObserver, error) { + client, err := NewLBTCAttestationClient(lggr, config) + if err != nil { + return nil, fmt.Errorf("create attestation client: %w", err) + } + return &LBTCTokenDataObserver{ + lggr: lggr, + destChainSelector: destChainSelector, + supportedPoolsBySelector: config.SourcePoolAddressByChain, + client: client, + }, nil +} + +func InitLBTCTokenDataObserver( + lggr logger.Logger, + destChainSelector cciptypes.ChainSelector, + supportedPoolsBySelector map[cciptypes.ChainSelector]string, + client tokendata.AttestationClient, +) *LBTCTokenDataObserver { + return &LBTCTokenDataObserver{ + lggr: lggr, + destChainSelector: destChainSelector, + supportedPoolsBySelector: supportedPoolsBySelector, + client: client, + } +} + +func (o *LBTCTokenDataObserver) Observe( + ctx context.Context, + observations exectypes.MessageObservations, +) (exectypes.TokenDataObservations, error) { + // 1. Pick LBTC messages + lbtcMessages := o.pickOnlyLBTCMessages(observations) + // 2. Request attestations + attestations, err := o.client.Attestations(ctx, lbtcMessages) + if err != nil { + return nil, err + } + // 3. Map to result + return o.createTokenDataObservations(observations, attestations) +} + +// IsTokenSupported returns true if the token is supported by the observer. +func (o *LBTCTokenDataObserver) IsTokenSupported( + sourceChain cciptypes.ChainSelector, + msgToken cciptypes.RampTokenAmount, +) bool { + return strings.EqualFold(o.supportedPoolsBySelector[sourceChain], msgToken.SourcePoolAddress.String()) && + len(msgToken.ExtraData) == 32 +} + +// Close closes the observer and releases any resources. +func (o *LBTCTokenDataObserver) Close() error { + return nil +} + +func (o *LBTCTokenDataObserver) pickOnlyLBTCMessages( + messageObservations exectypes.MessageObservations, +) map[cciptypes.ChainSelector]map[reader.MessageTokenID]cciptypes.Bytes { + lbtcMessages := make(map[cciptypes.ChainSelector]map[reader.MessageTokenID]cciptypes.Bytes) + for chainSelector, messages := range messageObservations { + lbtcMessages[chainSelector] = make(map[reader.MessageTokenID]cciptypes.Bytes) + for seqNum, message := range messages { + for i, tokenAmount := range message.TokenAmounts { + isLBTC := o.IsTokenSupported(chainSelector, tokenAmount) + if isLBTC { + lbtcMessages[chainSelector][reader.NewMessageTokenID(seqNum, i)] = tokenAmount.ExtraData + } + o.lggr.Debugw( + "Scanning message's tokens for LBTC data", + "isLBTC", isLBTC, + "seqNum", seqNum, + "sourceChainSelector", chainSelector, + "sourcePoolAddress", tokenAmount.SourcePoolAddress.String(), + "destTokenAddress", tokenAmount.DestTokenAddress.String(), + ) + } + } + } + return lbtcMessages +} + +func (o *LBTCTokenDataObserver) createTokenDataObservations( + messages exectypes.MessageObservations, + attestations map[cciptypes.ChainSelector]map[reader.MessageTokenID]tokendata.AttestationStatus, +) (exectypes.TokenDataObservations, error) { + tokenObservations := make(exectypes.TokenDataObservations) + for chainSelector, chainMessages := range messages { + tokenObservations[chainSelector] = make(map[cciptypes.SeqNum]exectypes.MessageTokenData) + for seqNum, message := range chainMessages { + tokenData := make([]exectypes.TokenData, len(message.TokenAmounts)) + for i, tokenAmount := range message.TokenAmounts { + if !o.IsTokenSupported(chainSelector, tokenAmount) { + o.lggr.Debugw( + "Ignoring unsupported token", + "seqNum", seqNum, + "sourceChainSelector", chainSelector, + "sourcePoolAddress", tokenAmount.SourcePoolAddress.String(), + "destTokenAddress", tokenAmount.DestTokenAddress.String(), + ) + tokenData[i] = exectypes.NotSupportedTokenData() + } else { + tokenData[i] = o.attestationToTokenData(seqNum, i, attestations[chainSelector]) + } + } + tokenObservations[chainSelector][seqNum] = exectypes.NewMessageTokenData(tokenData...) + } + } + return tokenObservations, nil +} + +func (o *LBTCTokenDataObserver) attestationToTokenData( + seqNr cciptypes.SeqNum, + tokenIndex int, + attestations map[reader.MessageTokenID]tokendata.AttestationStatus, +) exectypes.TokenData { + status, ok := attestations[reader.NewMessageTokenID(seqNr, tokenIndex)] + if !ok { + return exectypes.NewErrorTokenData(tokendata.ErrDataMissing) + } + if status.Error != nil { + return exectypes.NewErrorTokenData(status.Error) + } + return exectypes.NewSuccessTokenData(status.Attestation) +} diff --git a/execute/tokendata/lbtc/lbtc_int_test.go b/execute/tokendata/lbtc/lbtc_int_test.go new file mode 100644 index 000000000..514cacf4a --- /dev/null +++ b/execute/tokendata/lbtc/lbtc_int_test.go @@ -0,0 +1,475 @@ +package lbtc_test + +import ( + "context" + "encoding/hex" + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "slices" + "testing" + "time" + + "github.com/stretchr/testify/require" + + sel "github.com/smartcontractkit/chain-selectors" + + commonconfig "github.com/smartcontractkit/chainlink-common/pkg/config" + "github.com/smartcontractkit/chainlink-common/pkg/logger" + + "github.com/smartcontractkit/chainlink-ccip/execute/exectypes" + "github.com/smartcontractkit/chainlink-ccip/execute/tokendata" + "github.com/smartcontractkit/chainlink-ccip/execute/tokendata/lbtc" + "github.com/smartcontractkit/chainlink-ccip/internal" + cciptypes "github.com/smartcontractkit/chainlink-ccip/pkg/types/ccipocr3" + "github.com/smartcontractkit/chainlink-ccip/pluginconfig" +) + +type lbtcMessage struct { + // payloadHash is the array of hex string which represents hashes of LBTC payload to request attestation + payloadHash []string + // attestationResponse is the response from the Attestation API + attestationResponse string + // attestationResponseStatus is the status code of the response from the Attestation API + attestationResponseStatus int +} + +func (u *lbtcMessage) tokenData(idx int) []byte { + var result map[string]interface{} + + err := json.Unmarshal([]byte(u.attestationResponse), &result) + if err != nil { + panic(err) + } + + attestations, ok := result["attestations"].([]interface{}) + if !ok { + panic("attestations not found") + } + attestation, ok := attestations[idx].(map[string]interface{}) + if !ok { + panic("attestation not found") + } + bytes, err := cciptypes.NewBytesFromString(attestation["attestation"].(string)) + if err != nil { + panic(err) + } + return bytes +} + +//nolint:lll +var ( + // binance_smart_chain-mainnet -> ethereum-mainnet-base-1 + // https://bscscan.com/tx/0x5ccf7f9737673e6e044147d50848686ff279b629094a4ea711e3b7ceb617197e + // https://mainnet.prod.lombard.finance/api/bridge/v1/deposits/getByHash -> {"messageHash": ["0x117f49bfccd85ce2d0ad3a2c9bc27af2abd43eed0cbaeb2ddf5098cbd6bb8bcf"]} + m1 = lbtcMessage{ + payloadHash: []string{"0x117f49bfccd85ce2d0ad3a2c9bc27af2abd43eed0cbaeb2ddf5098cbd6bb8bcf"}, + attestationResponse: `{ + "attestations": [{ + "message_hash":"0x117f49bfccd85ce2d0ad3a2c9bc27af2abd43eed0cbaeb2ddf5098cbd6bb8bcf", + "attestation":"0x0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000000e45c70a5050000000000000000000000000000000000000000000000000000000000000038000000000000000000000000a869817b48b25eee986bdf4be04062e6fd2c418b0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000a869817b48b25eee986bdf4be04062e6fd2c418b000000000000000000000000097bcc72a1d3d09c13c6fcb489ad1f8776d9bacc000000000000000000000000000000000000000000000000000000000002848800000000000000000000000000000000000000000000000000000000000003da0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002c00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000001c000000000000000000000000000000000000000000000000000000000000002200000000000000000000000000000000000000000000000000000000000000040d9933ebfc1d76440c89ef681353707ed74d83e01cd1742df5fbe16cef03f63f22ce03b18e5656cd068227907e1dfdb6ad661b3398e6b8c361f81f505fa3587200000000000000000000000000000000000000000000000000000000000000040a3b634e583ac7ae8d9efc45f2543286ef5255b52a34583ab3cf176c9566ddd267da002cb471638c9944acaef22e79d776b72947c3d856d179dd7b5c6cbbe94350000000000000000000000000000000000000000000000000000000000000040f21186c33eb7e81bd8823ba9456d1f433493da9a490b63011320f2af3f4369fd0c3e7d70cace00ced689ceda62d22e060d88fb9b2aa6a3df8937313b5f7cbfed00000000000000000000000000000000000000000000000000000000000000404df4ffedfeccf2939bed8b1155e3f396b366f02fec4f99c127df768be9cbb7cc09333ec3a0c174050f5902faa660de8132a867b2ea034c65cd35b4ffe5e92a450000000000000000000000000000000000000000000000000000000000000040ce766bb75675c315df57263790a6a7c3e65b518e837c9b6910909070757872d82eee9dcaaa23143d18e2266dc0910fb358b27a26fca545bb53a1f76ec02ce917", + "status":"NOTARIZATION_STATUS_SESSION_APPROVED" + }] + }`, + attestationResponseStatus: 200, + } + + // binance_smart_chain-mainnet -> ethereum-mainnet-base-1 + // https://bscscan.com/tx/0x317fc5482bbd0975525bdc476d4203655286761b33bbd1f400a6674356da6f7c + // https://mainnet.prod.lombard.finance/api/bridge/v1/deposits/getByHash -> {"messageHash": ["0x27bf6eb2920da82a6a1294ceff503733c5a46a36d6d6c56a006f8720c399574b"]} + m2 = lbtcMessage{ + payloadHash: []string{"0xbca4f38f27d1aaec0d36ceda9990f3508f72aa44fa90371962bc23a6d7b6429d", "0x27bf6eb2920da82a6a1294ceff503733c5a46a36d6d6c56a006f8720c399574b", "0x5455ad825ac854ec2bfee200961d62ea57269bd248b782ed727ab33fd698e061"}, + attestationResponse: `{ + "attestations": [ + { + "message_hash":"0xbca4f38f27d1aaec0d36ceda9990f3508f72aa44fa90371962bc23a6d7b6429d", + "attestation":"0x0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000000e45c70a5050000000000000000000000000000000000000000000000000000000000000001000000000000000000000000a869817b48b25eee986bdf4be04062e6fd2c418b0000000000000000000000000000000000000000000000000000000000002105000000000000000000000000a869817b48b25eee986bdf4be04062e6fd2c418b000000000000000000000000f4dc338b1b1184f84a461f0bb2f974fc90a814560000000000000000000000000000000000000000000000000000000008f0d18000000000000000000000000000000000000000000000000000000000000000760000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002c00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000001c000000000000000000000000000000000000000000000000000000000000002200000000000000000000000000000000000000000000000000000000000000040cc1da209c50d5df4b03c9fc9d3053ccef515b648354598114c10416d3fd692462bd3ec04d11b2c09eb119c13b4e8236a3b22c60d7f2534c9cbda23e31402984c0000000000000000000000000000000000000000000000000000000000000040a92241c515ad60095a7b823266922f8daad601915b59d232f4a6d21ce0f08650671e73a4e447ae0dd5ccdd47b3b825a8f6dfb8ebf4c53c4a2192d21e56da2b1000000000000000000000000000000000000000000000000000000000000000409e598a93d1ac8672da38aa9eb072070b0edc100d97f53c8b4b642aed9173d94b07f72630b320ae7f8fb0bf7082f15f5ed92f0f0b23c8a1c0ccd9336f6debcb6e00000000000000000000000000000000000000000000000000000000000000404999635b1bd3cc9a6051bed7e5940b2345f3b88ac39d508b3fb5fc2c32961bed451b15675ea97a58c822097934981c228d234a48ed5f4a77c6e2a41196a69845000000000000000000000000000000000000000000000000000000000000004014bb3ed8be0f6bd73167cac19eb922674dc513defbbc3ead2df54d37bdf3924422a384ad0c1ef41aa8a9003fabadb5ba91c7e32de96d370061e9474836cd1532", + "status":"NOTARIZATION_STATUS_SESSION_APPROVED" + }, + { + "message_hash":"0x27bf6eb2920da82a6a1294ceff503733c5a46a36d6d6c56a006f8720c399574b", + "attestation":"0x0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000000e45c70a5050000000000000000000000000000000000000000000000000000000000000038000000000000000000000000a869817b48b25eee986bdf4be04062e6fd2c418b0000000000000000000000000000000000000000000000000000000000002105000000000000000000000000a869817b48b25eee986bdf4be04062e6fd2c418b0000000000000000000000002608f4b20ef1ad0a95dbedb1b1d3e59f1b8ca401000000000000000000000000000000000000000000000000000000000013491c00000000000000000000000000000000000000000000000000000000000003d40000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002c00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000001c000000000000000000000000000000000000000000000000000000000000002200000000000000000000000000000000000000000000000000000000000000040bcc09d9e009a36730686ea34e496d648bac3b076bc05239d0e06afed14f7f71c73d29724519311a7c7c5a26407aaf6cd2297fc105b9b4b4b0cd697ea677c39ce0000000000000000000000000000000000000000000000000000000000000040e326031805bbd52604688e303d3a0b8f1a312cce7d4d6794f5b67f2e8d3890dc73e1a6c8c0ce62d39796f64e6f2f86741d35518a0b0bbfdd648cd20ace2ae4a90000000000000000000000000000000000000000000000000000000000000040adf606c947c91bdfb7e443a870a9afd49a41c7852e0ad41781f3d7b09bb9e4bf4b3916d5571c9622a90b34a2350a68d39eb5d0f5f4adef8fadf1e68c461106ad000000000000000000000000000000000000000000000000000000000000004070e824e6f8180e7aee99f39af2ff8a0f1c3a94f7902b73fedfa827518ad722ac0eb9fb934f8b2d0e76236ef0ccb64a7faa43e8ef3d940ee1918c77be8eb3263c00000000000000000000000000000000000000000000000000000000000000407ac5168c3302549e8fd98c9e576d66801b3c3c675989f76356aee511cd8017ca7c42210c01d1abf7c69acfd250a25f325c9c16597fddde000a3cf8fdd6a13182", + "status":"NOTARIZATION_STATUS_SESSION_APPROVED" + }, + { + "message_hash":"0x5455ad825ac854ec2bfee200961d62ea57269bd248b782ed727ab33fd698e061", + "attestation":"0x0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000000e45c70a5050000000000000000000000000000000000000000000000000000000000000038000000000000000000000000a869817b48b25eee986bdf4be04062e6fd2c418b0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000a869817b48b25eee986bdf4be04062e6fd2c418b0000000000000000000000006975a0ff0bd36fb0cb94f43c095936cfb07de9d500000000000000000000000000000000000000000000000000000000000304a800000000000000000000000000000000000000000000000000000000000003de0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002c00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000001c000000000000000000000000000000000000000000000000000000000000002200000000000000000000000000000000000000000000000000000000000000040e598e75fa7d0f4167ab451faa7813af965bcfa00dc9e3c98cbe87c2faef390b616a20d9507d7dc5c1861559265c9e2cbd331de94f6e43df2f8ff426223fbbfbd0000000000000000000000000000000000000000000000000000000000000040e8d4eef7d9e384e1e45bce1ab2c6c272940829e4e97c30d19f7eba89e59ecfdb418cf7e1aa7fd37c98f94f994c9e1bef25afc2c88b3bf04b2fc1bfbd1529cc780000000000000000000000000000000000000000000000000000000000000040dcdb9dd9dbf4313ece1eb4949e04883116225d3c5907f43d6a6ddb37ffbf3240203969641a3cbabace340315cbfa1d6954d8e63a2541dfafe58b5e17d53e7dd0000000000000000000000000000000000000000000000000000000000000004092330a9da5ddaffb34da37526172d3b7b8bac70c101a8b1dd2722541afb2504335430549a4a4f9647088b18b8700b68f03a2eced918141d30686fa0bf5ae045d000000000000000000000000000000000000000000000000000000000000004058b9f40e888281284be0b5f84e1549d91bba2e7de47b5065a0f4468e4c07f6f016493eae6239c81ae11161005b80883fa620a24405c5512710dd6da1aa40316c", + "status":"NOTARIZATION_STATUS_SESSION_APPROVED" + } + ] + }`, + attestationResponseStatus: 200, + } + + // https://mainnet.prod.lombard.finance/api/bridge/v1/deposits/getByHash -> {"messageHash": ["0x48302ed57e4c10905ed6ae5acc432919723cc65ba0a8319b59061472515047ef", "0x82fe5d9633b805ab473e302a4c20b9a91bcfc52e41c86d12dc1416726b11dcf7", "0xfec6a8389b7a6aa0afc3d154ce7c7b60bbad11631bf4890fa29c044790bbac54", "0xfceb7130de362329c1e1402774a62da95bc2a4300004633bc45cdea6afcc7a85"]} + m3 = lbtcMessage{ + payloadHash: []string{"0x48302ed57e4c10905ed6ae5acc432919723cc65ba0a8319b59061472515047ef", "0x82fe5d9633b805ab473e302a4c20b9a91bcfc52e41c86d12dc1416726b11dcf7", "0xfec6a8389b7a6aa0afc3d154ce7c7b60bbad11631bf4890fa29c044790bbac54", "0xfceb7130de362329c1e1402774a62da95bc2a4300004633bc45cdea6afcc7a85"}, + attestationResponse: `{ + "attestations": [ + { + "message_hash":"0x48302ed57e4c10905ed6ae5acc432919723cc65ba0a8319b59061472515047ef", + "attestation":"0x0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000000e45c70a5050000000000000000000000000000000000000000000000000000000000000001000000000000000000000000a869817b48b25eee986bdf4be04062e6fd2c418b0000000000000000000000000000000000000000000000000000000000002105000000000000000000000000a869817b48b25eee986bdf4be04062e6fd2c418b0000000000000000000000001b648ade1ef219c87987cd60eba069a7faf1621f0000000000000000000000000000000000000000000000000000000008583b0000000000000000000000000000000000000000000000000000000000000000950000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002c00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000001c000000000000000000000000000000000000000000000000000000000000002200000000000000000000000000000000000000000000000000000000000000040c26183b4e4752b062f7e5dbdccba71f37997c0af2ce6a28db97e6b55648ea5cb5deca115b16f7fd969135f0279abc639f8e26a4d93e4713532993f387f90ad6e00000000000000000000000000000000000000000000000000000000000000404ca4577b7936deff2aa26b9dd906eb352c35c18a280a59141e742969927cb78d1561b919675980b0eaa9fc41d1985688db89910b4e5c1576b1edf49cde88c101000000000000000000000000000000000000000000000000000000000000004028d437358f7af12b48879cfb4ff2df72bae54e3dd7d87c5722b185577e6c363068736c23ea67422670a2e8eeb19572adddfec9fd04460e7b43c7c40ecb0c533f0000000000000000000000000000000000000000000000000000000000000040cbd3d5231a50d7e98216607202c000f0665b339d045438133cb5fe56520f40f3529be0773afa3aceb0fdaa99bce1818ec0e5335ae3f421663c4fff07baea701a0000000000000000000000000000000000000000000000000000000000000040fdd02f81bd45951a1344d84a1f29f8269aead3f637f450384b4aa7960d065e015d1e4a288097b11cb7103605d5064f081ec0be5fda48074c1f280fae3db3a946", + "status":"NOTARIZATION_STATUS_SESSION_APPROVED" + }, + { + "message_hash":"0x82fe5d9633b805ab473e302a4c20b9a91bcfc52e41c86d12dc1416726b11dcf7", + "attestation":"0x0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000000e45c70a5050000000000000000000000000000000000000000000000000000000000000001000000000000000000000000a869817b48b25eee986bdf4be04062e6fd2c418b0000000000000000000000000000000000000000000000000000000000002105000000000000000000000000a869817b48b25eee986bdf4be04062e6fd2c418b0000000000000000000000002eae9ac17edc1966ff12003afecdd1cad1a7de30000000000000000000000000000000000000000000000000000000000098fbac00000000000000000000000000000000000000000000000000000000000000960000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002c00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000001c000000000000000000000000000000000000000000000000000000000000002200000000000000000000000000000000000000000000000000000000000000040c985a727df981d734b86007f33531620e93e4bdb30f260f7142c643e2f20da5264ecbfa5fb1fb3d086d1c1b2fd5fa93e082d2788e58fa5c8466f7b035f6a138a00000000000000000000000000000000000000000000000000000000000000403c07e222472606fc3400bca7581b08d464007b54519c8bc729a7b129f3da0eae4612670a96bc8d47ec43bfb0107a36e80cde024733b0ded58a3ce3c0ff27ecfb000000000000000000000000000000000000000000000000000000000000004022b155feb1a193cfbfe77f7df605ea890bbd3eea8a72869c97ef0f156d3b99676d37557fed35eca2b8c52590f1df4b37f58dfc0c75dbddf18df09b2e6565503500000000000000000000000000000000000000000000000000000000000000405c6f3dbfcb1e21647f037410e00871068e56730bd27bf429830bf2cb065a8c7c1b5e3e9bcc33fb80aae7eeead1922dfb86ed48259503c6ac7b2d078c2427a2f200000000000000000000000000000000000000000000000000000000000000409f5f36668b844b7f9e4ef404c3def915ceeaf16a93079fe2b35592deee2d120f45c423ffc0ab07fd8be3390e1f7440c5cb60f37968e7d4742eec12a627c36c56", + "status":"NOTARIZATION_STATUS_SESSION_APPROVED" + }, + { + "message_hash":"0xfec6a8389b7a6aa0afc3d154ce7c7b60bbad11631bf4890fa29c044790bbac54", + "attestation":"0x0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000000e45c70a5050000000000000000000000000000000000000000000000000000000000000001000000000000000000000000a869817b48b25eee986bdf4be04062e6fd2c418b0000000000000000000000000000000000000000000000000000000000002105000000000000000000000000a869817b48b25eee986bdf4be04062e6fd2c418b00000000000000000000000086867e7552417f4e31ee72946116e196617524440000000000000000000000000000000000000000000000000000000004f5b55000000000000000000000000000000000000000000000000000000000000000970000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002c00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000001c0000000000000000000000000000000000000000000000000000000000000022000000000000000000000000000000000000000000000000000000000000000404ef41420152776c1385916532647ee1db973a9f3fb5a3af2fd4fec1a709e383170f8a940c70e5aacb22dc75522386963bbdc7d650dea11219c7a163f84322317000000000000000000000000000000000000000000000000000000000000004030bd23fae9ca9eda68d8b724c3a3c326c2aa86ca37cf84dab9b88b4e6a14acaa49ce5f9fc5b3c7222992a17d2afe9aba23863b4a583f2b998adeef0c618e47410000000000000000000000000000000000000000000000000000000000000040e208fdb10d80fbff04b93ff6896c049e9350e0bcc077a0cb627120c957e9632f5cd1bcabc50bef567439c7fcfe14119e4a1fea5b433b2a919f08da2d3247804100000000000000000000000000000000000000000000000000000000000000406eb0bea43ea23056f074152068cebca78430d9906664eb44745a234f11ceb74e3c33d6e4ae251af44b0444cfe08ad15a54627d600c941dc6dc0de61a2d18234d00000000000000000000000000000000000000000000000000000000000000409e9bf6bcff0a09a8d0c1bdbde9af77ca58513f3c70211c6f6571d64326054c180e9e067467dd1510bf2fbef0412debeeb1c6a3113c8b84ca8558ef810d0fd4e8", + "status":"NOTARIZATION_STATUS_SESSION_APPROVED" + }, + { + "message_hash":"0xfceb7130de362329c1e1402774a62da95bc2a4300004633bc45cdea6afcc7a85", + "status":"NOTARIZATION_STATUS_SESSION_APPROVED", + "attestation":"0x0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000000e45c70a5050000000000000000000000000000000000000000000000000000000000000001000000000000000000000000a869817b48b25eee986bdf4be04062e6fd2c418b0000000000000000000000000000000000000000000000000000000000002105000000000000000000000000a869817b48b25eee986bdf4be04062e6fd2c418b0000000000000000000000001984e3b342f1bc8476557f1686e73b24b76799ed0000000000000000000000000000000000000000000000000000000000009c4000000000000000000000000000000000000000000000000000000000000000980000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002c00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000001c000000000000000000000000000000000000000000000000000000000000002200000000000000000000000000000000000000000000000000000000000000040b7d67457824915a99c624cdff679558fc0f958eacc59423b2204f4ec91222c5b3938ab6b8848150e923b071c77b3e71103387400d6f41faab5a6b553a11b71a80000000000000000000000000000000000000000000000000000000000000040e0b35545caf0787c5c4917f6eb47e37c78b1875d0eeaa7163488bc01e6e74df826eead971a9ee2c31eb75efe52de71edb8b98aea0da840ac4d81a719fc0c090b00000000000000000000000000000000000000000000000000000000000000403a2246d825a67baf616df4613d04ed388253886a7f22785c0ae5a385965678d30095b66320a96d76988de9b7934aec95b652c0583a77db3937fc4df2190eb1ea0000000000000000000000000000000000000000000000000000000000000040de87d0b8dc4c2fd0ddd66a49171eb6cad344025b67897042f8330fb7c5756c6922a59af84245e50728e0f17abc06ef86af784252d0ddf65d684723eda72ee858000000000000000000000000000000000000000000000000000000000000004059b9328a6efa95c7c969a67bb7fec3fe0e08e7522b1cf4443b230d9de7aa2495301fc48243aae0a66cf6e42cd0f681e3245a56056529b577ccfd381ee7699da7" + } + ] + }`, + attestationResponseStatus: 200, + } + + // These are all approved, but testing all possible statuses + // https://mainnet.prod.lombard.finance/api/bridge/v1/deposits/getByHash -> {"messageHash": ["0x74fd1a76d356b1e331e7fc586c27f52cb22f5207962f562f566eeeecfb2e6454", "0xfd9574739cbc2f206226ce545b5ee9c9022ac9828add7667d3a0506e3cabac3b", "0x6567a3bec9881c1101e90224cb5f6b0f13d1f2923d3c310c4acde91accd3f474", "0x33dca299c74ca51a70936eab2668a549cacf5c99be556021577b3f3b77b7b850", "0x1531c52169ad7aa7edeb520aac02c91d272aff9ed2099a3a7638a805db62b3bb"]} + m4 = lbtcMessage{ + payloadHash: []string{"0x74fd1a76d356b1e331e7fc586c27f52cb22f5207962f562f566eeeecfb2e6454", "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "0xfd9574739cbc2f206226ce545b5ee9c9022ac9828add7667d3a0506e3cabac3b", "0x6567a3bec9881c1101e90224cb5f6b0f13d1f2923d3c310c4acde91accd3f474", "0x33dca299c74ca51a70936eab2668a549cacf5c99be556021577b3f3b77b7b850", "0x1531c52169ad7aa7edeb520aac02c91d272aff9ed2099a3a7638a805db62b3bb"}, + attestationResponse: `{ + "attestations": [ + { + "message_hash":"0x74fd1a76d356b1e331e7fc586c27f52cb22f5207962f562f566eeeecfb2e6454", + "attestation":"0x0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000000e45c70a5050000000000000000000000000000000000000000000000000000000000000001000000000000000000000000a869817b48b25eee986bdf4be04062e6fd2c418b0000000000000000000000000000000000000000000000000000000000002105000000000000000000000000a869817b48b25eee986bdf4be04062e6fd2c418b0000000000000000000000000a05c538da9e321ad07f6f18424fae7fc4fa25db0000000000000000000000000000000000000000000000000000000014dc938000000000000000000000000000000000000000000000000000000000000000990000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002c00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000001c000000000000000000000000000000000000000000000000000000000000002200000000000000000000000000000000000000000000000000000000000000040dfcb983a714595ac5ce52704a202da95e09dbef64a2fb53e12f4cb1377c521ed0e447278ac8cbfb21abaad60dcb829a27ee61b0c803770846b16894a0bce3daa000000000000000000000000000000000000000000000000000000000000004021585159861085ebf4a52f352872df047533cb286f44497d7adec228349c12556fa55cf8f127f190666d2884b8b3c1c1baff71cbdd6eccb3703146421325e45d00000000000000000000000000000000000000000000000000000000000000404628962fca1e75619230d056e5054050d39beb8ae407b0282391e92302257b9f524cec0b6acc7166efe58e3e693279d51e9c49ac1aeaa4ce29e760e3ed602b2a0000000000000000000000000000000000000000000000000000000000000040b2658a5da29fa33599bd555601b93a91900cdfd715b9d17f2ce123e0f544338771d772d6498e8db689d63239875b1b2f2f502d69d71a1700886a458c15e8dfd60000000000000000000000000000000000000000000000000000000000000040e8f915415fde22ad637c498d07fe33d2b577e9292cca9917fbad21ec4be0477b51a40571cbe3bdf6d8a375bee7e0368dc1dabec6062094cf0440e45b28e600f4", + "status":"NOTARIZATION_STATUS_SESSION_APPROVED" + }, + { + "message_hash":"0xfd9574739cbc2f206226ce545b5ee9c9022ac9828add7667d3a0506e3cabac3b", + "status":"NOTARIZATION_STATUS_PENDING" + }, + { + "message_hash":"0x6567a3bec9881c1101e90224cb5f6b0f13d1f2923d3c310c4acde91accd3f474", + "status":"NOTARIZATION_STATUS_SUBMITTED" + }, + { + "message_hash":"0x33dca299c74ca51a70936eab2668a549cacf5c99be556021577b3f3b77b7b850", + "status":"NOTARIZATION_STATUS_FAILED" + }, + { + "message_hash":"0x1531c52169ad7aa7edeb520aac02c91d272aff9ed2099a3a7638a805db62b3bb", + "status":"NOTARIZATION_STATUS_UNSPECIFIED" + } + ] + }`, + attestationResponseStatus: 200, + } + + // https://mainnet.prod.lombard.finance/api/bridge/v1/deposits/getByHash -> {"messageHash": ["0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"]} + m5 = lbtcMessage{ + payloadHash: []string{"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + attestationResponse: `{ + "code": 13, + "message": "failed to get deposits by hash set: rpc error: code = InvalidArgument desc = invalid hash" + }`, + attestationResponseStatus: 500, + } +) + +// This test focuses on almost e2e flows for USDC message +// It aims to make it as real as possible by using real payloads from the chain and +// real responses from the Attestation API. As long as the Attestation API is supporting old events +// you should be able to just copy-paste block explorer and attestation requests to the browser +// and see those responses in action +func Test_LBTC_Flow(t *testing.T) { + bscPool := internal.RandBytes().String() + ethPool := internal.RandBytes().String() + bscChain := cciptypes.ChainSelector(sel.BINANCE_SMART_CHAIN_MAINNET.Selector) + ethChain := cciptypes.ChainSelector(sel.ETHEREUM_MAINNET.Selector) + + httpMessages := []lbtcMessage{m1, m2, m3, m4, m5} + + // Mock http server to return proper payloads + server := mockHTTPServerResponse(t, httpMessages) + defer server.Close() + + config := pluginconfig.LBTCObserverConfig{ + AttestationConfig: pluginconfig.AttestationConfig{ + AttestationAPI: server.URL, + AttestationAPIInterval: commonconfig.MustNewDuration(1 * time.Microsecond), + AttestationAPITimeout: commonconfig.MustNewDuration(1 * time.Second), + }, + SourcePoolAddressByChain: map[cciptypes.ChainSelector]string{ + bscChain: bscPool, + ethChain: ethPool, + }, + } + + baseObserver, err := lbtc.NewLBTCTokenDataObserver( + logger.Test(t), + cciptypes.ChainSelector(sel.ETHEREUM_MAINNET_BASE_1.Selector), + config, + ) + require.NoError(t, err) + + tt := []struct { + name string + messages exectypes.MessageObservations + want exectypes.TokenDataObservations + }{ + { + name: "single valid message from bsc to base", + messages: exectypes.MessageObservations{ + bscChain: { + 1: cciptypes.Message{ + TokenAmounts: []cciptypes.RampTokenAmount{ + createToken(bscPool, m1.payloadHash[0]), + }, + }, + }, + }, + want: exectypes.TokenDataObservations{ + bscChain: { + 1: exectypes.MessageTokenData{ + TokenData: []exectypes.TokenData{ + exectypes.NewSuccessTokenData(m1.tokenData(0)), + }, + }, + }, + }, + }, + { + name: "multiple valid messages and tokens from bsc to base", + messages: exectypes.MessageObservations{ + bscChain: { + 1: cciptypes.Message{ + TokenAmounts: []cciptypes.RampTokenAmount{ + createToken(bscPool, m2.payloadHash[0]), + createToken(bscPool, m2.payloadHash[1]), + }, + }, + 2: cciptypes.Message{ + TokenAmounts: []cciptypes.RampTokenAmount{ + createToken(bscPool, m2.payloadHash[2]), + }, + }, + }, + }, + want: exectypes.TokenDataObservations{ + bscChain: { + 1: exectypes.MessageTokenData{ + TokenData: []exectypes.TokenData{ + exectypes.NewSuccessTokenData(m2.tokenData(0)), + exectypes.NewSuccessTokenData(m2.tokenData(1)), + }, + }, + 2: exectypes.MessageTokenData{ + TokenData: []exectypes.TokenData{ + exectypes.NewSuccessTokenData(m2.tokenData(2)), + }, + }, + }, + }, + }, + { + name: "multiple source chain tokens", + messages: exectypes.MessageObservations{ + bscChain: { + 1: cciptypes.Message{ + TokenAmounts: []cciptypes.RampTokenAmount{ + createToken(bscPool, m3.payloadHash[0]), + createToken(bscPool, m3.payloadHash[1]), + }, + }, + }, + ethChain: { + 10: cciptypes.Message{ + TokenAmounts: []cciptypes.RampTokenAmount{ + createToken(ethPool, m3.payloadHash[2]), + }, + }, + 11: cciptypes.Message{ + TokenAmounts: []cciptypes.RampTokenAmount{ + createToken(ethPool, m3.payloadHash[3]), + }, + }, + }, + }, + want: exectypes.TokenDataObservations{ + bscChain: { + 1: exectypes.MessageTokenData{ + TokenData: []exectypes.TokenData{ + exectypes.NewSuccessTokenData(m3.tokenData(0)), + exectypes.NewSuccessTokenData(m3.tokenData(1)), + }, + }, + }, + ethChain: { + 10: exectypes.MessageTokenData{ + TokenData: []exectypes.TokenData{ + exectypes.NewSuccessTokenData(m3.tokenData(2)), + }, + }, + 11: exectypes.MessageTokenData{ + TokenData: []exectypes.TokenData{ + exectypes.NewSuccessTokenData(m3.tokenData(3)), + }, + }, + }, + }, + }, + { + name: "messages with tokens failing to get ready", + messages: exectypes.MessageObservations{ + bscChain: { + 1: cciptypes.Message{ + TokenAmounts: []cciptypes.RampTokenAmount{ + createToken(bscPool, m4.payloadHash[0]), + }, + }, + 2: cciptypes.Message{ + TokenAmounts: []cciptypes.RampTokenAmount{ + // attestation missing + createToken(bscPool, m4.payloadHash[1]), + }, + }, + 3: cciptypes.Message{ + TokenAmounts: []cciptypes.RampTokenAmount{ + // attestation pending + createToken(bscPool, m4.payloadHash[2]), + }, + }, + }, + ethChain: { + 10: cciptypes.Message{ + TokenAmounts: []cciptypes.RampTokenAmount{ + // attestation submitted + createToken(ethPool, m4.payloadHash[3]), + }, + }, + 11: cciptypes.Message{ + TokenAmounts: []cciptypes.RampTokenAmount{ + // attestation failed + createToken(ethPool, m4.payloadHash[4]), + }, + }, + 12: cciptypes.Message{ + TokenAmounts: []cciptypes.RampTokenAmount{ + // attestation unspecified + createToken(ethPool, m4.payloadHash[5]), + }, + }, + }, + }, + want: exectypes.TokenDataObservations{ + bscChain: { + 1: exectypes.MessageTokenData{ + TokenData: []exectypes.TokenData{ + exectypes.NewSuccessTokenData(m4.tokenData(0)), + }, + }, + 2: exectypes.MessageTokenData{ + TokenData: []exectypes.TokenData{ + exectypes.NewErrorTokenData(tokendata.ErrDataMissing), + }, + }, + 3: exectypes.MessageTokenData{ + TokenData: []exectypes.TokenData{ + exectypes.NewErrorTokenData(tokendata.ErrNotReady), + }, + }, + }, + ethChain: { + 10: exectypes.MessageTokenData{ + TokenData: []exectypes.TokenData{ + exectypes.NewErrorTokenData(tokendata.ErrNotReady), + }, + }, + 11: exectypes.MessageTokenData{ + TokenData: []exectypes.TokenData{ + exectypes.NewErrorTokenData(tokendata.ErrUnknownResponse), + }, + }, + 12: exectypes.MessageTokenData{ + TokenData: []exectypes.TokenData{ + exectypes.NewErrorTokenData(tokendata.ErrUnknownResponse), + }, + }, + }, + }, + }, + { + name: "attestation api internal server error", + messages: exectypes.MessageObservations{ + bscChain: { + 1: cciptypes.Message{ + TokenAmounts: []cciptypes.RampTokenAmount{ + createToken(bscPool, m5.payloadHash[0]), + }, + }, + }, + }, + want: exectypes.TokenDataObservations{ + bscChain: { + 1: exectypes.MessageTokenData{ + TokenData: []exectypes.TokenData{ + exectypes.NewErrorTokenData(tokendata.ErrUnknownResponse), + }, + }, + }, + }, + }, + } + + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + got, err1 := baseObserver.Observe(context.Background(), tc.messages) + require.NoError(t, err1) + require.Equal(t, tc.want, got) + }) + + } +} + +func createToken(pool string, payloadHash string) cciptypes.RampTokenAmount { + sourcePoolAddress, err := hex.DecodeString(pool) + if err != nil { + panic(err) + } + extraData, err := hex.DecodeString(payloadHash) + if err != nil { + panic(err) + } + return cciptypes.RampTokenAmount{ + SourcePoolAddress: sourcePoolAddress, + ExtraData: extraData, + Amount: cciptypes.NewBigIntFromInt64(100), + } +} + +func mockHTTPServerResponse(t *testing.T, messages []lbtcMessage) *httptest.Server { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + bodyRaw, err := io.ReadAll(r.Body) + require.NoError(t, err) + var body map[string][]string + err = json.Unmarshal(bodyRaw, &body) + require.NoError(t, err) + payloadHashes, ok := body["messageHash"] + require.True(t, ok) + for _, message := range messages { + matches := true + for _, payloadHash := range message.payloadHash { + matches = matches && slices.Contains(payloadHashes, payloadHash) + } + if matches { + w.WriteHeader(message.attestationResponseStatus) + //resp, err := json.Marshal(message.attestationResponse) + //require.NoError(t, err) + _, err = w.Write([]byte(message.attestationResponse)) + require.NoError(t, err) + return + } + } + w.WriteHeader(http.StatusInternalServerError) + })) + return ts +} diff --git a/execute/tokendata/lbtc/lbtc_test.go b/execute/tokendata/lbtc/lbtc_test.go new file mode 100644 index 000000000..3a561ddf7 --- /dev/null +++ b/execute/tokendata/lbtc/lbtc_test.go @@ -0,0 +1,260 @@ +package lbtc_test + +import ( + "context" + "fmt" + "testing" + + "github.com/ethereum/go-ethereum/crypto" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink-common/pkg/logger" + + "github.com/smartcontractkit/chainlink-ccip/execute/exectypes" + "github.com/smartcontractkit/chainlink-ccip/execute/tokendata" + "github.com/smartcontractkit/chainlink-ccip/execute/tokendata/lbtc" + "github.com/smartcontractkit/chainlink-ccip/internal" + cciptypes "github.com/smartcontractkit/chainlink-ccip/pkg/types/ccipocr3" +) + +func TestTokenDataObserver_Observe_LBTCAndRegularTokens(t *testing.T) { + ethereumLBTCPool := internal.RandBytes().String() + avalancheLBTCPool := internal.RandBytes().String() + supportedPoolsBySelector := map[cciptypes.ChainSelector]string{ + cciptypes.ChainSelector(1): ethereumLBTCPool, + cciptypes.ChainSelector(2): avalancheLBTCPool, + } + messageWithTokensAndExtraDataMixedSize := + MessageWithTokensAndExtraData32(t, ethereumLBTCPool, internal.RandBytes().String()) + messageWithTokensAndExtraDataMixedSize.TokenAmounts[0].ExtraData = + messageWithTokensAndExtraDataMixedSize.TokenAmounts[0].ExtraData[:16] + tests := []struct { + name string + messageObservations exectypes.MessageObservations + attestationClient tokendata.AttestationClient + expectedTokenData exectypes.TokenDataObservations + }{ + { + name: "no messages", + messageObservations: exectypes.MessageObservations{}, + expectedTokenData: exectypes.TokenDataObservations{}, + attestationClient: &tokendata.FakeAttestationClient{}, + }, + { + name: "no LBTC messages", + messageObservations: exectypes.MessageObservations{ + 1: { + 10: internal.MessageWithTokens(t, internal.RandBytes().String()), + 11: internal.MessageWithTokens(t), + }, + }, + expectedTokenData: exectypes.TokenDataObservations{ + 1: { + 10: exectypes.NewMessageTokenData(exectypes.NotSupportedTokenData()), + 11: exectypes.NewMessageTokenData(), + }, + }, + attestationClient: &tokendata.FakeAttestationClient{}, + }, + { + name: "single LBTC message per chain with non 32 bytes extra data", + messageObservations: exectypes.MessageObservations{ + 1: { + 10: MessageWithTokensAndExtraData16(t, ethereumLBTCPool), + }, + 2: { + 12: MessageWithTokensAndExtraData16(t, avalancheLBTCPool), + }, + }, + expectedTokenData: exectypes.TokenDataObservations{ + 1: { + 10: exectypes.NewMessageTokenData(exectypes.NotSupportedTokenData()), + }, + 2: { + 12: exectypes.NewMessageTokenData(exectypes.NotSupportedTokenData()), + }, + }, + attestationClient: &tokendata.FakeAttestationClient{}, + }, + { + name: "single LBTC message per chain", + messageObservations: exectypes.MessageObservations{ + 1: { + 10: MessageWithTokensAndExtraData32(t, ethereumLBTCPool), + }, + 2: { + 12: MessageWithTokensAndExtraData32(t, avalancheLBTCPool), + }, + }, + attestationClient: &tokendata.FakeAttestationClient{ + Data: map[string]tokendata.AttestationStatus{ + string(bytes32From(ethereumLBTCPool, 0)): {Attestation: []byte{10_0}}, + string(bytes32From(avalancheLBTCPool, 0)): {Attestation: []byte{12_0}}, + }, + }, + expectedTokenData: exectypes.TokenDataObservations{ + 1: { + 10: exectypes.NewMessageTokenData(newReadyTokenData([]byte{10_0})), + }, + 2: { + 12: exectypes.NewMessageTokenData(newReadyTokenData([]byte{12_0})), + }, + }, + }, + { + name: "LBTC messages mixed with regular within a single msg chain", + messageObservations: exectypes.MessageObservations{ + 1: { + 9: messageWithTokensAndExtraDataMixedSize, + 10: MessageWithTokensAndExtraData32(t, ethereumLBTCPool, internal.RandBytes().String()), + 11: MessageWithTokensAndExtraData32( + t, internal.RandBytes().String(), ethereumLBTCPool, internal.RandBytes().String(), + ), + 12: MessageWithTokensAndExtraData32( + t, internal.RandBytes().String(), internal.RandBytes().String(), ethereumLBTCPool, + ), + 13: internal.MessageWithTokens(t), + }, + }, + attestationClient: &tokendata.FakeAttestationClient{ + Data: map[string]tokendata.AttestationStatus{ + string(bytes32From(ethereumLBTCPool, 0)): {Attestation: []byte{10_0}}, + string(bytes32From(ethereumLBTCPool, 1)): {Attestation: []byte{11_1}}, + string(bytes32From(ethereumLBTCPool, 2)): {Attestation: []byte{12_2}}, + }, + }, + expectedTokenData: exectypes.TokenDataObservations{ + 1: { + 9: exectypes.NewMessageTokenData( + exectypes.NotSupportedTokenData(), + exectypes.NotSupportedTokenData(), + ), + 10: exectypes.NewMessageTokenData( + newReadyTokenData([]byte{10_0}), + exectypes.NotSupportedTokenData(), + ), + 11: exectypes.NewMessageTokenData( + exectypes.NotSupportedTokenData(), + newReadyTokenData([]byte{11_1}), + exectypes.NotSupportedTokenData(), + ), + 12: exectypes.NewMessageTokenData( + exectypes.NotSupportedTokenData(), + exectypes.NotSupportedTokenData(), + newReadyTokenData([]byte{12_2}), + ), + 13: exectypes.NewMessageTokenData(), + }, + }, + }, + { + name: "multiple LBTC transfer in a single message", + messageObservations: exectypes.MessageObservations{ + 1: { + 10: MessageWithTokensAndExtraData32(t, ethereumLBTCPool, ethereumLBTCPool, ethereumLBTCPool), + }, + 2: { + 12: MessageWithTokensAndExtraData32(t, avalancheLBTCPool, avalancheLBTCPool), + }, + }, + attestationClient: &tokendata.FakeAttestationClient{ + Data: map[string]tokendata.AttestationStatus{ + string(bytes32From(ethereumLBTCPool, 0)): {Attestation: []byte{10_0}}, + string(bytes32From(ethereumLBTCPool, 1)): {Attestation: []byte{10_1}}, + string(bytes32From(ethereumLBTCPool, 2)): {Attestation: []byte{10_2}}, + string(bytes32From(avalancheLBTCPool, 0)): {Attestation: []byte{12_0}}, + string(bytes32From(avalancheLBTCPool, 1)): {Attestation: []byte{12_1}}, + }, + }, + expectedTokenData: exectypes.TokenDataObservations{ + 1: { + 10: exectypes.NewMessageTokenData( + newReadyTokenData([]byte{10_0}), + newReadyTokenData([]byte{10_1}), + newReadyTokenData([]byte{10_2}), + ), + }, + 2: { + 12: exectypes.NewMessageTokenData( + newReadyTokenData([]byte{12_0}), + newReadyTokenData([]byte{12_1}), + ), + }, + }, + }, + { + name: "not ready messages are populated to the result set", + messageObservations: exectypes.MessageObservations{ + 1: { + 10: MessageWithTokensAndExtraData32(t, ethereumLBTCPool, ethereumLBTCPool, internal.RandBytes().String()), + }, + }, + attestationClient: &tokendata.FakeAttestationClient{ + Data: map[string]tokendata.AttestationStatus{ + string(bytes32From(ethereumLBTCPool, 0)): {Attestation: []byte{10_0}}, + string(bytes32From(ethereumLBTCPool, 1)): {Error: tokendata.ErrNotReady}, + }, + }, + expectedTokenData: exectypes.TokenDataObservations{ + 1: { + 10: exectypes.NewMessageTokenData( + newReadyTokenData([]byte{10_0}), + exectypes.TokenData{ + Ready: false, + Data: nil, + Error: tokendata.ErrNotReady, + Supported: true, + }, + exectypes.NotSupportedTokenData(), + ), + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + observer := lbtc.InitLBTCTokenDataObserver( + logger.Test(t), + 1, + supportedPoolsBySelector, + test.attestationClient, + ) + + tkData, err := observer.Observe(context.Background(), test.messageObservations) + require.NoError(t, err) + + assert.Equal(t, test.expectedTokenData, tkData) + }) + } +} + +func newReadyTokenData(data []byte) exectypes.TokenData { + return exectypes.TokenData{ + Ready: true, + Error: nil, + Data: data, + Supported: true, + } +} + +func MessageWithTokensAndExtraData32(t *testing.T, tokenPoolAddr ...string) cciptypes.Message { + message := internal.MessageWithTokens(t, tokenPoolAddr...) + for i := range message.TokenAmounts { + message.TokenAmounts[i].ExtraData = bytes32From(tokenPoolAddr[i], i) + } + return message +} + +func MessageWithTokensAndExtraData16(t *testing.T, tokenPoolAddr ...string) cciptypes.Message { + message := internal.MessageWithTokens(t, tokenPoolAddr...) + for i := range message.TokenAmounts { + message.TokenAmounts[i].ExtraData = internal.RandBytes()[:16] + } + return message +} + +func bytes32From(address string, idx int) []byte { + return crypto.Keccak256([]byte(fmt.Sprintf("%s%d", address, idx))) +} diff --git a/execute/tokendata/observer/observer.go b/execute/tokendata/observer/observer.go index d86df4f70..e37a5e14b 100644 --- a/execute/tokendata/observer/observer.go +++ b/execute/tokendata/observer/observer.go @@ -64,12 +64,13 @@ func NewConfigBasedCompositeObservers( // e.g. observers[i] := config.CreateTokenDataObserver() switch { case c.USDCCCTPObserverConfig != nil: - observer, err := usdc.NewUSDCTokenDataObserver(ctx, lggr, destChainSelector, *c.USDCCCTPObserverConfig, encoder.EncodeUSDC, readers) + observer, err := usdc.NewUSDCTokenDataObserver(ctx, lggr, destChainSelector, + *c.USDCCCTPObserverConfig, encoder.EncodeUSDC, readers) if err != nil { return nil, fmt.Errorf("create USDC/CCTP token observer: %w", err) } - if *c.USDCCCTPObserverConfig.NumWorkers == 0 { + if c.USDCCCTPObserverConfig.NumWorkers == 0 { lggr.Info("Using foreground observer for USDC/CCTP") observers[i] = observer } else { @@ -77,7 +78,7 @@ func NewConfigBasedCompositeObservers( observers[i] = NewBackgroundObserver( lggr, observer, - *c.USDCCCTPObserverConfig.NumWorkers, + c.USDCCCTPObserverConfig.NumWorkers, c.USDCCCTPObserverConfig.CacheExpirationInterval.Duration(), c.USDCCCTPObserverConfig.CacheCleanupInterval.Duration(), c.USDCCCTPObserverConfig.ObserveTimeout.Duration(), @@ -89,7 +90,7 @@ func NewConfigBasedCompositeObservers( return nil, fmt.Errorf("create LBTC token observer: %w", err) } - if *c.LBTCObserverConfig.NumWorkers == 0 { + if c.LBTCObserverConfig.NumWorkers == 0 { lggr.Info("Using foreground observer for LBTC") observers[i] = observer } else { @@ -97,7 +98,7 @@ func NewConfigBasedCompositeObservers( observers[i] = NewBackgroundObserver( lggr, observer, - *c.LBTCObserverConfig.NumWorkers, + c.LBTCObserverConfig.NumWorkers, c.LBTCObserverConfig.CacheExpirationInterval.Duration(), c.LBTCObserverConfig.CacheCleanupInterval.Duration(), c.LBTCObserverConfig.ObserveTimeout.Duration(), diff --git a/execute/tokendata/observer/observer_test.go b/execute/tokendata/observer/observer_test.go index af5dc02d5..5e89794db 100644 --- a/execute/tokendata/observer/observer_test.go +++ b/execute/tokendata/observer/observer_test.go @@ -7,10 +7,11 @@ import ( "github.com/stretchr/testify/require" - "github.com/smartcontractkit/chainlink-ccip/execute/tokendata/observer" "github.com/smartcontractkit/chainlink-common/pkg/logger" "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" + "github.com/smartcontractkit/chainlink-ccip/execute/tokendata/observer" + "github.com/smartcontractkit/chainlink-ccip/execute/exectypes" "github.com/smartcontractkit/chainlink-ccip/internal" cciptypes "github.com/smartcontractkit/chainlink-ccip/pkg/types/ccipocr3" diff --git a/execute/tokendata/token_data.go b/execute/tokendata/token_data.go index 563f37e33..9ba072b93 100644 --- a/execute/tokendata/token_data.go +++ b/execute/tokendata/token_data.go @@ -2,17 +2,18 @@ package tokendata import ( "context" + "encoding/hex" "errors" "time" - "github.com/ethereum/go-ethereum/common/hexutil" "github.com/patrickmn/go-cache" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-ccip/pkg/reader" cciptypes "github.com/smartcontractkit/chainlink-ccip/pkg/types/ccipocr3" - "github.com/smartcontractkit/chainlink-common/pkg/logger" ) const ( @@ -116,9 +117,9 @@ func (o *ObservedAttestationClient) Attestations( } duration := time.Since(start) - for _, msgIdToAttestation := range attestations { - for _, attestation := range msgIdToAttestation { - o.trackTimeToAttestation(start, hexutil.Encode(attestation.ID), attestation.Error) + for _, msgIDToAttestation := range attestations { + for _, attestation := range msgIDToAttestation { + o.trackTimeToAttestation(start, hex.EncodeToString(attestation.ID), attestation.Error) promAttestationDurations. WithLabelValues(o.Token()). Observe(float64(duration)) @@ -146,7 +147,7 @@ func (o *ObservedAttestationClient) trackTimeToAttestation(start time.Time, id s // and we don't need to track that if seen { duration := time.Since(messageSeenFirst.(time.Time)) - promAttestationDurations.WithLabelValues(o.Token()).Observe(float64(duration)) + promTimeToAttestation.WithLabelValues(o.Token()).Observe(float64(duration)) o.lggr.Infow("Observed time to attestation for a message", "token", o.Token(), "hash", id, diff --git a/execute/tokendata/usdc/attestation.go b/execute/tokendata/usdc/attestation.go index 95b67091f..70f77a419 100644 --- a/execute/tokendata/usdc/attestation.go +++ b/execute/tokendata/usdc/attestation.go @@ -6,11 +6,12 @@ import ( "fmt" "time" - "github.com/smartcontractkit/chainlink-ccip/execute/tokendata" - "github.com/smartcontractkit/chainlink-ccip/execute/tokendata/http" "github.com/smartcontractkit/chainlink-common/pkg/hashutil" "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-ccip/execute/tokendata" + "github.com/smartcontractkit/chainlink-ccip/execute/tokendata/http" + "github.com/smartcontractkit/chainlink-ccip/pkg/logutil" "github.com/smartcontractkit/chainlink-ccip/pkg/reader" cciptypes "github.com/smartcontractkit/chainlink-ccip/pkg/types/ccipocr3" diff --git a/execute/tokendata/usdc/attestation_test.go b/execute/tokendata/usdc/attestation_test.go index aec9f1023..62ab0e327 100644 --- a/execute/tokendata/usdc/attestation_test.go +++ b/execute/tokendata/usdc/attestation_test.go @@ -8,13 +8,14 @@ import ( "testing" "time" - "github.com/ethereum/go-ethereum/common/hexutil" "github.com/stretchr/testify/require" - "github.com/smartcontractkit/chainlink-ccip/execute/tokendata" commonconfig "github.com/smartcontractkit/chainlink-common/pkg/config" "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" + "github.com/smartcontractkit/chainlink-ccip/execute/tokendata" + "github.com/smartcontractkit/chainlink-ccip/internal" + "github.com/smartcontractkit/chainlink-ccip/internal/mocks" "github.com/smartcontractkit/chainlink-ccip/pkg/reader" cciptypes "github.com/smartcontractkit/chainlink-ccip/pkg/types/ccipocr3" @@ -68,7 +69,9 @@ func Test_AttestationClient(t *testing.T) { }, expected: map[cciptypes.ChainSelector]map[reader.MessageTokenID]tokendata.AttestationStatus{ cciptypes.ChainSelector(1): { - reader.NewMessageTokenID(1, 1): tokendata.SuccessAttestationStatus(messageA.hash, hexutil.MustDecode(messageA.keccak)), + reader.NewMessageTokenID(1, 1): tokendata.SuccessAttestationStatus( + messageA.hash, internal.MustDecode(messageA.keccak), + ), }, }, }, @@ -100,11 +103,17 @@ func Test_AttestationClient(t *testing.T) { }, expected: map[cciptypes.ChainSelector]map[reader.MessageTokenID]tokendata.AttestationStatus{ cciptypes.ChainSelector(1): { - reader.NewMessageTokenID(1, 1): tokendata.SuccessAttestationStatus(messageA.hash, hexutil.MustDecode(messageA.keccak)), - reader.NewMessageTokenID(1, 2): tokendata.SuccessAttestationStatus(messageB.hash, hexutil.MustDecode(messageB.keccak)), + reader.NewMessageTokenID(1, 1): tokendata.SuccessAttestationStatus( + messageA.hash, internal.MustDecode(messageA.keccak), + ), + reader.NewMessageTokenID(1, 2): tokendata.SuccessAttestationStatus( + messageB.hash, internal.MustDecode(messageB.keccak), + ), }, cciptypes.ChainSelector(2): { - reader.NewMessageTokenID(2, 1): tokendata.SuccessAttestationStatus(messageC.hash, hexutil.MustDecode(messageC.keccak)), + reader.NewMessageTokenID(2, 1): tokendata.SuccessAttestationStatus( + messageC.hash, internal.MustDecode(messageC.keccak), + ), }, }, }, @@ -147,34 +156,42 @@ func Test_AttestationClient(t *testing.T) { }, expected: map[cciptypes.ChainSelector]map[reader.MessageTokenID]tokendata.AttestationStatus{ cciptypes.ChainSelector(1): { - reader.NewMessageTokenID(1, 1): tokendata.SuccessAttestationStatus(messageA.hash, hexutil.MustDecode(messageA.keccak)), + reader.NewMessageTokenID(1, 1): tokendata.SuccessAttestationStatus( + messageA.hash, internal.MustDecode(messageA.keccak), + ), }, cciptypes.ChainSelector(2): { reader.NewMessageTokenID(2, 1): tokendata.ErrorAttestationStatus(tokendata.ErrNotReady), }, cciptypes.ChainSelector(3): { - reader.NewMessageTokenID(3, 1): tokendata.SuccessAttestationStatus(messageC.hash, hexutil.MustDecode(messageC.keccak)), + reader.NewMessageTokenID(3, 1): tokendata.SuccessAttestationStatus( + messageC.hash, internal.MustDecode(messageC.keccak), + ), }, }, }, } for _, tc := range tt { - t.Run(tc.name, func(t *testing.T) { - handler.updateURIs(tc.success, tc.pending) + t.Run( + tc.name, func(t *testing.T) { + handler.updateURIs(tc.success, tc.pending) - client, err := NewSequentialAttestationClient(mocks.NullLogger, pluginconfig.USDCCCTPObserverConfig{ - AttestationConfig: pluginconfig.AttestationConfig{ - AttestationAPI: server.URL, - AttestationAPIInterval: commonconfig.MustNewDuration(1 * time.Millisecond), - AttestationAPITimeout: commonconfig.MustNewDuration(5 * time.Second), - }, - }) - require.NoError(t, err) - attestations, err := client.Attestations(tests.Context(t), tc.input) - require.NoError(t, err) - require.Equal(t, tc.expected, attestations) - }) + client, err := NewSequentialAttestationClient( + mocks.NullLogger, pluginconfig.USDCCCTPObserverConfig{ + AttestationConfig: pluginconfig.AttestationConfig{ + AttestationAPI: server.URL, + AttestationAPIInterval: commonconfig.MustNewDuration(1 * time.Millisecond), + AttestationAPITimeout: commonconfig.MustNewDuration(5 * time.Second), + }, + }, + ) + require.NoError(t, err) + attestations, err := client.Attestations(tests.Context(t), tc.input) + require.NoError(t, err) + require.Equal(t, tc.expected, attestations) + }, + ) } } @@ -191,7 +208,7 @@ func Test_httpResponse(t *testing.T) { Status: attestationStatusSuccess, Attestation: "0x720502893578a89a8a87982982ef781c18b193", }, - expectedAttestation: hexutil.MustDecode("0x720502893578a89a8a87982982ef781c18b193"), + expectedAttestation: internal.MustDecode("0x720502893578a89a8a87982982ef781c18b193"), }, { name: "pending", @@ -215,18 +232,20 @@ func Test_httpResponse(t *testing.T) { } for _, tc := range tt { - t.Run(tc.name, func(t *testing.T) { - err := tc.response.validate() - if tc.expectedError != nil { - require.EqualError(t, err, tc.expectedError.Error()) - return - } + t.Run( + tc.name, func(t *testing.T) { + err := tc.response.validate() + if tc.expectedError != nil { + require.EqualError(t, err, tc.expectedError.Error()) + return + } - require.NoError(t, err) - attestation, err1 := tc.response.attestationToBytes() - require.NoError(t, err1) - require.Equal(t, tc.expectedAttestation, attestation) - }) + require.NoError(t, err) + attestation, err1 := tc.response.attestationToBytes() + require.NoError(t, err1) + require.Equal(t, tc.expectedAttestation, attestation) + }, + ) } } @@ -267,11 +286,13 @@ func (h *mockHandler) updateURIs(success, pending []string) { } func (h *mockHandler) writeJSONResponse(w http.ResponseWriter, status, attestation string) { - response := fmt.Sprintf(` + response := fmt.Sprintf( + ` { "status": "%s", "attestation": "%s" - }`, status, attestation) + }`, status, attestation, + ) _, err := w.Write([]byte(response)) require.NoError(h.t, err) } diff --git a/execute/tokendata/usdc/usdc.go b/execute/tokendata/usdc/usdc.go index 234c4e489..068b5f853 100644 --- a/execute/tokendata/usdc/usdc.go +++ b/execute/tokendata/usdc/usdc.go @@ -7,9 +7,10 @@ import ( "golang.org/x/exp/maps" + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-ccip/execute/tokendata" "github.com/smartcontractkit/chainlink-ccip/pkg/contractreader" - "github.com/smartcontractkit/chainlink-common/pkg/logger" "github.com/smartcontractkit/chainlink-ccip/execute/exectypes" "github.com/smartcontractkit/chainlink-ccip/pkg/logutil" @@ -116,7 +117,7 @@ func (u *USDCTokenDataObserver) IsTokenSupported( sourceChain cciptypes.ChainSelector, msgToken cciptypes.RampTokenAmount, ) bool { - return strings.ToLower(u.supportedPoolsBySelector[sourceChain]) == strings.ToLower(msgToken.SourcePoolAddress.String()) + return strings.EqualFold(u.supportedPoolsBySelector[sourceChain], msgToken.SourcePoolAddress.String()) } func (u *USDCTokenDataObserver) Close() error { @@ -243,9 +244,3 @@ func (u *USDCTokenDataObserver) attestationToTokenData( } return exectypes.NewSuccessTokenData(tokenData) } - -func sourceTokenIdentifier(chainSelector cciptypes.ChainSelector, sourcePoolAddress string) string { - // Lowercase the sourcePoolAddress to make the identifier case-insensitive. - // It makes the code immune to the differences between checksum and non-checksum addresses. - return fmt.Sprintf("%d-%s", chainSelector, strings.ToLower(sourcePoolAddress)) -} diff --git a/execute/tokendata/usdc/usdc_int_test.go b/execute/tokendata/usdc/usdc_int_test.go index 45ee52997..a7141036e 100644 --- a/execute/tokendata/usdc/usdc_int_test.go +++ b/execute/tokendata/usdc/usdc_int_test.go @@ -4,6 +4,8 @@ import ( "context" "encoding/hex" "encoding/json" + "errors" + "fmt" "net/http" "net/http/httptest" "strings" @@ -15,12 +17,13 @@ import ( sel "github.com/smartcontractkit/chain-selectors" - "github.com/smartcontractkit/chainlink-ccip/execute/tokendata" commonconfig "github.com/smartcontractkit/chainlink-common/pkg/config" "github.com/smartcontractkit/chainlink-common/pkg/logger" "github.com/smartcontractkit/chainlink-common/pkg/types" "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" + "github.com/smartcontractkit/chainlink-ccip/execute/tokendata" + "github.com/smartcontractkit/chainlink-ccip/execute/exectypes" "github.com/smartcontractkit/chainlink-ccip/execute/tokendata/usdc" "github.com/smartcontractkit/chainlink-ccip/internal" @@ -164,6 +167,43 @@ var ( attestationResponse: `Internal Server Error`, attestationResponseStatus: 500, } + + // malformed responses below + m8 = usdcMessage{ + sourceDomain: 0, // Ethereum Sepolia + nonce: 266463, + eventPayload: "00000000000000000000000600000000000410DF0000000000000000000000009F3B8679C73C2FEF8B59B4F3444D4E156FB70AA50000000000000000000000009F3B8679C73C2FEF8B59B4F3444D4E156FB70AA50000000000000000000000005931822F394BABC2AACF4588E98FC77A9F5AA8C9000000000000000000000000000000001C7D4B196CB0C7B01D743FBC6116A902379C7238000000000000000000000000ABC1BCD3E0A2E25003E679E0D2CD6BF67350B22900000000000000000000000000000000000000000000000000000000000F4240000000000000000000000000AFF3FE524EA94118EF09DADBE3C77BA6AA0005EC", + urlMessageHash: "0x099a94f15947bf3c51dd32a39eebc9ba5b630bd7d56dbb7bbbc6adc7639a84ec", + attestationResponse: `{ "error": "some error" }`, + attestationResponseStatus: 200, + } + + m9 = usdcMessage{ + sourceDomain: 0, // Ethereum Sepolia + nonce: 265012, + eventPayload: "0000000000000000000000060000000000040B340000000000000000000000009F3B8679C73C2FEF8B59B4F3444D4E156FB70AA50000000000000000000000009F3B8679C73C2FEF8B59B4F3444D4E156FB70AA50000000000000000000000005931822F394BABC2AACF4588E98FC77A9F5AA8C9000000000000000000000000000000001C7D4B196CB0C7B01D743FBC6116A902379C72380000000000000000000000002D356B3FBFF7C93B750331B4FA32C59AFC59DB430000000000000000000000000000000000000000000000000000000000000001000000000000000000000000AFF3FE524EA94118EF09DADBE3C77BA6AA0005EC", + urlMessageHash: "0x8b5ca0ad74898bd2126b547c2869ea18b2ced2b484f6b8d64e818ee0401c8805", + attestationResponse: `{ "status": "complete", "attestation": "0" }`, + attestationResponseStatus: 200, + } + + m10 = usdcMessage{ + sourceDomain: 0, // Ethereum Sepolia + nonce: 265013, + eventPayload: "0000000000000000000000060000000000040B350000000000000000000000009F3B8679C73C2FEF8B59B4F3444D4E156FB70AA50000000000000000000000009F3B8679C73C2FEF8B59B4F3444D4E156FB70AA50000000000000000000000005931822F394BABC2AACF4588E98FC77A9F5AA8C9000000000000000000000000000000001C7D4B196CB0C7B01D743FBC6116A902379C72380000000000000000000000002D356B3FBFF7C93B750331B4FA32C59AFC59DB430000000000000000000000000000000000000000000000000000000000000001000000000000000000000000AFF3FE524EA94118EF09DADBE3C77BA6AA0005EC", + urlMessageHash: "0xab14e391c81233cfabe30056ddd28a0a33c11313e24c4a2435a17c82d9d74900", + attestationResponse: `{ "status": "", "attestation": "0x720502893578a89a8a87982982ef781c18b193" }`, + attestationResponseStatus: 200, + } + + m11 = usdcMessage{ + sourceDomain: 0, // Ethereum Sepolia + nonce: 265014, + eventPayload: "0000000000000000000000060000000000040B360000000000000000000000009F3B8679C73C2FEF8B59B4F3444D4E156FB70AA50000000000000000000000009F3B8679C73C2FEF8B59B4F3444D4E156FB70AA50000000000000000000000005931822F394BABC2AACF4588E98FC77A9F5AA8C9000000000000000000000000000000001C7D4B196CB0C7B01D743FBC6116A902379C72380000000000000000000000002D356B3FBFF7C93B750331B4FA32C59AFC59DB430000000000000000000000000000000000000000000000000000000000000001000000000000000000000000AFF3FE524EA94118EF09DADBE3C77BA6AA0005EC", + urlMessageHash: "0x422ad18cd36a4009033711848e85126430516aa84292cfb75c92d45d14e29601", + attestationResponse: `{ "field": 2137 }`, + attestationResponseStatus: 200, + } ) // This test focuses on almost e2e flows for USDC message @@ -183,8 +223,8 @@ func Test_USDC_CCTP_Flow(t *testing.T) { baseChain := cciptypes.ChainSelector(sel.ETHEREUM_TESTNET_SEPOLIA_BASE_1.Selector) fuji := []usdcMessage{m1, m2, m3} - sepolia := []usdcMessage{m4, m5, m6, m7} - httpMessages := []usdcMessage{m1, m2, m3, m4, m5, m6, m7} + sepolia := []usdcMessage{m4, m5, m6, m7, m8, m9, m10, m11} + httpMessages := []usdcMessage{m1, m2, m3, m4, m5, m6, m7, m8, m9, m10, m11} // Mock http server to return proper payloads server := mockHTTPServerResponse(t, httpMessages) @@ -414,6 +454,59 @@ func Test_USDC_CCTP_Flow(t *testing.T) { }, }, }, + { + name: "usdc attestation api malformed responses", + messages: exectypes.MessageObservations{ + sepoliaChain: { + 1: cciptypes.Message{ + TokenAmounts: []cciptypes.RampTokenAmount{ + createToken(t, m8.nonce, m8.sourceDomain, sepoliaPool), + }, + }, + 2: cciptypes.Message{ + TokenAmounts: []cciptypes.RampTokenAmount{ + createToken(t, m9.nonce, m9.sourceDomain, sepoliaPool), + }, + }, + 3: cciptypes.Message{ + TokenAmounts: []cciptypes.RampTokenAmount{ + createToken(t, m10.nonce, m10.sourceDomain, sepoliaPool), + }, + }, + 4: cciptypes.Message{ + TokenAmounts: []cciptypes.RampTokenAmount{ + createToken(t, m11.nonce, m11.sourceDomain, sepoliaPool), + }, + }, + }, + }, + want: exectypes.TokenDataObservations{ + sepoliaChain: { + 1: exectypes.MessageTokenData{ + TokenData: []exectypes.TokenData{ + exectypes.NewErrorTokenData(errors.New("attestation API error: some error")), + }, + }, + 2: exectypes.MessageTokenData{ + TokenData: []exectypes.TokenData{ + exectypes.NewErrorTokenData(fmt.Errorf("failed to decode attestation hex: %w", + errors.New("Bytes must be of at least length 2 (i.e, '0x' prefix): 0"), + )), + }, + }, + 3: exectypes.MessageTokenData{ + TokenData: []exectypes.TokenData{ + exectypes.NewErrorTokenData(errors.New("invalid attestation response")), + }, + }, + 4: exectypes.MessageTokenData{ + TokenData: []exectypes.TokenData{ + exectypes.NewErrorTokenData(errors.New("invalid attestation response")), + }, + }, + }, + }, + }, } for _, tc := range tt { diff --git a/execute/tokendata/usdc/usdc_test.go b/execute/tokendata/usdc/usdc_test.go index 2629875a6..8c7f5c659 100644 --- a/execute/tokendata/usdc/usdc_test.go +++ b/execute/tokendata/usdc/usdc_test.go @@ -6,6 +6,8 @@ import ( "github.com/stretchr/testify/require" + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-ccip/execute/exectypes" "github.com/smartcontractkit/chainlink-ccip/execute/tokendata" "github.com/smartcontractkit/chainlink-ccip/execute/tokendata/usdc" @@ -13,7 +15,6 @@ import ( "github.com/smartcontractkit/chainlink-ccip/internal/libs/testhelpers" "github.com/smartcontractkit/chainlink-ccip/pkg/reader" cciptypes "github.com/smartcontractkit/chainlink-ccip/pkg/types/ccipocr3" - "github.com/smartcontractkit/chainlink-common/pkg/logger" ) func TestTokenDataObserver_Observe_USDCAndRegularTokens(t *testing.T) { diff --git a/go.mod b/go.mod index cd17ecaaf..a217d9b55 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.23.3 require ( github.com/deckarep/golang-set/v2 v2.6.0 + github.com/ethereum/go-ethereum v1.13.8 github.com/patrickmn/go-cache v2.1.0+incompatible github.com/prometheus/client_golang v1.20.0 github.com/prometheus/client_model v0.6.1 @@ -22,10 +23,12 @@ require ( require ( github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/beorn7/perks v1.0.1 // indirect + github.com/btcsuite/btcd v0.22.0-beta // indirect + github.com/btcsuite/btcd/btcec/v2 v2.2.0 // indirect github.com/buger/jsonparser v1.1.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/ethereum/go-ethereum v1.13.8 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-viper/mapstructure/v2 v2.1.0 // indirect diff --git a/go.sum b/go.sum index 446644497..0fee4b6dd 100644 --- a/go.sum +++ b/go.sum @@ -1,19 +1,42 @@ +github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= +github.com/btcsuite/btcd v0.22.0-beta h1:LTDpDKUM5EeOFBPM8IXpinEcmZ6FWfNZbE3lfrfdnWo= +github.com/btcsuite/btcd v0.22.0-beta/go.mod h1:9n5ntfhhHQBIhUvlhDvD3Qg6fRUj4jkN0VB8L8svzOA= +github.com/btcsuite/btcd/btcec/v2 v2.2.0 h1:fzn1qaOt32TuLjFlkzYSsBC35Q3KUjT1SwPxiMSCF5k= +github.com/btcsuite/btcd/btcec/v2 v2.2.0/go.mod h1:U7MHm051Al6XmscBQ0BoNydpOTsFAn707034b5nY8zU= +github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= +github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= +github.com/btcsuite/btcutil v1.0.3-0.20201208143702-a53e38424cce/go.mod h1:0DVlHczLPewLcPGEIeUEzfOJhqGPQ0mJJRDBtD307+o= +github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg= +github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY= +github.com/btcsuite/goleveldb v1.0.0/go.mod h1:QiK9vBlgftBg6rWQIj6wFzbPfRjiykIEhBH4obrXJ/I= +github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= +github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= +github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= +github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/deckarep/golang-set/v2 v2.6.0 h1:XfcQbWM1LlMB8BsJ8N9vW5ehnnPVIw0je80NsVHagjM= github.com/deckarep/golang-set/v2 v2.6.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= +github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0= +github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= +github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218= github.com/ethereum/go-ethereum v1.13.8 h1:1od+thJel3tM52ZUNQwvpYOeRHlbkVFZ5S8fhi0Lgsg= github.com/ethereum/go-ethereum v1.13.8/go.mod h1:sc48XYQxCzH3fG9BcrXCOOgQk2JfZzNAmIKnceogzsA= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fxamacker/cbor/v2 v2.5.0 h1:oHsG0V/Q6E/wqTS2O1Cozzsy69nqCiguo5Q1a1ADivE= github.com/fxamacker/cbor/v2 v2.5.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -23,15 +46,21 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-viper/mapstructure/v2 v2.1.0 h1:gHnMa2Y/pIxElCH2GlZZ1lZSsn6XMtufpGyP1XxdC/w= github.com/go-viper/mapstructure/v2 v2.1.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/holiman/uint256 v1.2.4 h1:jUc4Nk8fm9jZabQuqr2JzednajVmBpC+oiTiXZJEApU= github.com/holiman/uint256 v1.2.4/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/invopop/jsonschema v0.12.0 h1:6ovsNSuvn9wEQVOyc72aycBMVQFKz7cPdMJn10CvzRI= github.com/invopop/jsonschema v0.12.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0= +github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= +github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= @@ -46,6 +75,10 @@ github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= @@ -101,16 +134,28 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk= golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY= +golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= @@ -126,6 +171,9 @@ google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojt gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/test_utils.go b/internal/test_utils.go index 857001009..57b8e1792 100644 --- a/internal/test_utils.go +++ b/internal/test_utils.go @@ -50,3 +50,11 @@ func CounterFromHistogramByLabels(t *testing.T, histogramVec *prometheus.Histogr return int(pb.GetHistogram().GetSampleCount()) } + +func MustDecode(s string) cciptypes.Bytes { + b, err := cciptypes.NewBytesFromString(s) + if err != nil { + panic(err) + } + return b +} diff --git a/pluginconfig/token.go b/pluginconfig/token.go index 2fa26fbe7..8f604f3fb 100644 --- a/pluginconfig/token.go +++ b/pluginconfig/token.go @@ -64,8 +64,8 @@ func (t TokenDataObserverConfig) WellFormed() error { return nil } if t.IsLBTC() { - if t.USDCCCTPObserverConfig == nil { - return errors.New("USDCCCTPObserverConfig is empty") + if t.LBTCObserverConfig == nil { + return errors.New("LBTCObserverConfig is empty") } return nil } @@ -92,45 +92,17 @@ func (t TokenDataObserverConfig) IsLBTC() bool { return t.Type == LBTCHandlerType } -type AttestationConfig struct { +type USDCCCTPObserverConfig struct { + Tokens map[cciptypes.ChainSelector]USDCCCTPTokenConfig `json:"tokens"` + AttestationAPI string `json:"attestationAPI"` // AttestationAPITimeout defines the timeout for the attestation API. AttestationAPITimeout *commonconfig.Duration `json:"attestationAPITimeout"` // AttestationAPIInterval defines the rate in requests per second that the attestation API can be called. // Default set according to the APIs documentated 10 requests per second rate limit. AttestationAPIInterval *commonconfig.Duration `json:"attestationAPIInterval"` -} - -func (p *AttestationConfig) setDefaults() { - // Default to 1 second if AttestationAPITimeout is not set - if p.AttestationAPITimeout == nil { - p.AttestationAPITimeout = commonconfig.MustNewDuration(5 * time.Second) - } - - // Default to 100 millis if AttestationAPIInterval is not set this is set according to the APIs documented - // 10 requests per second rate limit. - if p.AttestationAPIInterval == nil { - p.AttestationAPIInterval = commonconfig.MustNewDuration(100 * time.Millisecond) - } -} - -func (p *AttestationConfig) Validate() error { - p.setDefaults() - if p.AttestationAPI == "" { - return errors.New("AttestationAPI not set") - } - if p.AttestationAPIInterval == nil || p.AttestationAPIInterval.Duration() == 0 { - return errors.New("AttestationAPIInterval not set") - } - if p.AttestationAPITimeout == nil || p.AttestationAPITimeout.Duration() == 0 { - return errors.New("AttestationAPITimeout not set") - } - return nil -} - -type BackgroundWorkerConfig struct { // NumWorkers is the number of concurrent workers. - NumWorkers *int `json:"numWorkers"` + NumWorkers int `json:"numWorkers"` // CacheExpirationInterval is the interval after which the cached token data will expire. CacheExpirationInterval *commonconfig.Duration `json:"cacheExpirationInterval"` // CacheCleanupInterval is the interval after which the cache expired data will be cleaned up. @@ -139,62 +111,31 @@ type BackgroundWorkerConfig struct { ObserveTimeout *commonconfig.Duration `json:"observeTimeout"` } -func (c *BackgroundWorkerConfig) setDefaults() { - // Default to 10 workers if NumWorkers is not set - if c.NumWorkers == nil { - n := 10 - c.NumWorkers = &n - } - +func (c *USDCCCTPObserverConfig) setDefaults() { // Default to 10 minutes if CacheExpirationInterval is not set if c.CacheExpirationInterval == nil { c.CacheExpirationInterval = commonconfig.MustNewDuration(10 * time.Minute) } - // Default to 15 minutes if CacheCleanupInterval is not set if c.CacheCleanupInterval == nil { c.CacheCleanupInterval = commonconfig.MustNewDuration(15 * time.Minute) } - // Default to 5 seconds if ObserveTimeout is not set if c.ObserveTimeout == nil { c.ObserveTimeout = commonconfig.MustNewDuration(5 * time.Second) } -} - -func (c *BackgroundWorkerConfig) Validate() error { - c.setDefaults() - if c.NumWorkers == nil { - return errors.New("NumWorkers not set") - } - if c.CacheExpirationInterval == nil || c.CacheExpirationInterval.Duration() == 0 { - return errors.New("CacheExpirationInterval not set") - } - if c.CacheCleanupInterval == nil || c.CacheCleanupInterval.Duration() == 0 { - return errors.New("CacheCleanupInterval not set") + // Default to 1 second if AttestationAPITimeout is not set + if p.AttestationAPITimeout == nil { + p.AttestationAPITimeout = commonconfig.MustNewDuration(5 * time.Second) } - if c.ObserveTimeout == nil || c.ObserveTimeout.Duration() == 0 { - return errors.New("ObserveTimeout not set") + // Default to 100 millis if AttestationAPIInterval is not set this is set according to the APIs documented + // 10 requests per second rate limit. + if p.AttestationAPIInterval == nil { + p.AttestationAPIInterval = commonconfig.MustNewDuration(100 * time.Millisecond) } - return nil } -type USDCCCTPObserverConfig struct { - AttestationConfig - BackgroundWorkerConfig - Tokens map[cciptypes.ChainSelector]USDCCCTPTokenConfig `json:"tokens"` -} - -func (p *USDCCCTPObserverConfig) Validate() error { - err := p.AttestationConfig.Validate() - if err != nil { - return err - } - err = p.BackgroundWorkerConfig.Validate() - if err != nil { - return err - } - +func (c *USDCCCTPObserverConfig) Validate() error { if len(p.Tokens) == 0 { return errors.New("tokens not set") } @@ -203,6 +144,27 @@ func (p *USDCCCTPObserverConfig) Validate() error { return err } } + if p.AttestationAPI == "" { + return errors.New("AttestationAPI not set") + } + if p.AttestationAPIInterval == nil || p.AttestationAPIInterval.Duration() == 0 { + return errors.New("AttestationAPIInterval not set") + } + if p.AttestationAPITimeout == nil || p.AttestationAPITimeout.Duration() == 0 { + return errors.New("AttestationAPITimeout not set") + } + if err != nil { + return err + } + if c.CacheExpirationInterval == nil || c.CacheExpirationInterval.Duration() == 0 { + return errors.New("CacheExpirationInterval not set") + } + if c.CacheCleanupInterval == nil || c.CacheCleanupInterval.Duration() == 0 { + return errors.New("CacheCleanupInterval not set") + } + if c.ObserveTimeout == nil || c.ObserveTimeout.Duration() == 0 { + return errors.New("ObserveTimeout not set") + } return nil } @@ -226,28 +188,73 @@ func (t USDCCCTPTokenConfig) Validate() error { } type LBTCObserverConfig struct { - AttestationConfig - BackgroundWorkerConfig SourcePoolAddressByChain map[cciptypes.ChainSelector]string `json:"sourcePoolAddressByChain"` - AttestationAPIBatchSize int `json:"attestationAPIBatchSize"` + + AttestationAPI string `json:"attestationAPI"` + // AttestationAPITimeout defines the timeout for the attestation API. + AttestationAPITimeout *commonconfig.Duration `json:"attestationAPITimeout"` + // AttestationAPIInterval defines the rate in requests per second that the attestation API can be called. + // Default set according to the APIs documentated 10 requests per second rate limit. + AttestationAPIInterval *commonconfig.Duration `json:"attestationAPIInterval"` + // AttestationAPITimeout defines size of the batch that can be made in one API request + AttestationAPIBatchSize int `json:"attestationAPIBatchSize"` + // NumWorkers is the number of concurrent workers. + NumWorkers int `json:"numWorkers"` + // CacheExpirationInterval is the interval after which the cached token data will expire. + CacheExpirationInterval *commonconfig.Duration `json:"cacheExpirationInterval"` + // CacheCleanupInterval is the interval after which the cache expired data will be cleaned up. + CacheCleanupInterval *commonconfig.Duration `json:"cacheCleanupInterval"` + // ObserveTimeout is the timeout for the actual synchronous Observe calls. + ObserveTimeout *commonconfig.Duration `json:"observeTimeout"` } func (c *LBTCObserverConfig) setDefaults() { + // Default to 10 minutes if CacheExpirationInterval is not set + if c.CacheExpirationInterval == nil { + c.CacheExpirationInterval = commonconfig.MustNewDuration(10 * time.Minute) + } + // Default to 15 minutes if CacheCleanupInterval is not set + if c.CacheCleanupInterval == nil { + c.CacheCleanupInterval = commonconfig.MustNewDuration(15 * time.Minute) + } + // Default to 5 seconds if ObserveTimeout is not set + if c.ObserveTimeout == nil { + c.ObserveTimeout = commonconfig.MustNewDuration(5 * time.Second) + } + // Default to 1 second if AttestationAPITimeout is not set + if p.AttestationAPITimeout == nil { + p.AttestationAPITimeout = commonconfig.MustNewDuration(5 * time.Second) + } + // Default to 100 millis if AttestationAPIInterval is not set this is set according to the APIs documented + // 10 requests per second rate limit. + if p.AttestationAPIInterval == nil { + p.AttestationAPIInterval = commonconfig.MustNewDuration(100 * time.Millisecond) + } if c.AttestationAPIBatchSize == 0 { c.AttestationAPIBatchSize = 50 } } func (c *LBTCObserverConfig) Validate() error { - err := c.AttestationConfig.Validate() - if err != nil { - return err + c.setDefaults() + if p.AttestationAPI == "" { + return errors.New("AttestationAPI not set") } - err = c.BackgroundWorkerConfig.Validate() - if err != nil { - return err + if p.AttestationAPIInterval == nil || p.AttestationAPIInterval.Duration() == 0 { + return errors.New("AttestationAPIInterval not set") + } + if p.AttestationAPITimeout == nil || p.AttestationAPITimeout.Duration() == 0 { + return errors.New("AttestationAPITimeout not set") + } + if c.CacheExpirationInterval == nil || c.CacheExpirationInterval.Duration() == 0 { + return errors.New("CacheExpirationInterval not set") + } + if c.CacheCleanupInterval == nil || c.CacheCleanupInterval.Duration() == 0 { + return errors.New("CacheCleanupInterval not set") + } + if c.ObserveTimeout == nil || c.ObserveTimeout.Duration() == 0 { + return errors.New("ObserveTimeout not set") } - c.setDefaults() if c.AttestationAPIBatchSize == 0 { return errors.New("AttestationAPIBatchSize is not set") } diff --git a/pluginconfig/token_test.go b/pluginconfig/token_test.go index fea7f13a6..df8a79bf6 100644 --- a/pluginconfig/token_test.go +++ b/pluginconfig/token_test.go @@ -1,6 +1,7 @@ package pluginconfig import ( + "encoding/json" "fmt" "testing" "time" @@ -90,8 +91,8 @@ func Test_TokenDataObserver_Unmarshall(t *testing.T) { AttestationAPITimeout: commonconfig.MustNewDuration(time.Second), AttestationAPIInterval: commonconfig.MustNewDuration(500 * time.Millisecond), }, - BackgroundWorkerConfig: BackgroundWorkerConfig{ - NumWorkers: Ptr(10), + WorkerConfig: WorkerConfig{ + NumWorkers: 10, CacheExpirationInterval: commonconfig.MustNewDuration(5 * time.Second), CacheCleanupInterval: commonconfig.MustNewDuration(6 * time.Second), ObserveTimeout: commonconfig.MustNewDuration(7 * time.Second), @@ -152,7 +153,7 @@ func Test_TokenDataObserver_Unmarshall(t *testing.T) { }, }, } - + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { finalJSON := fmt.Sprintf(baseJSON, tt.json) @@ -162,6 +163,8 @@ func Test_TokenDataObserver_Unmarshall(t *testing.T) { require.Error(t, err) require.ErrorContains(t, err, tt.errMsg) } else { + marshal, err := json.Marshal(e.TokenDataObservers) + fmt.Println(string(marshal)) require.NoError(t, err) require.Equal(t, tt.want, e.TokenDataObservers) } @@ -189,8 +192,8 @@ func Test_TokenDataObserver_Validation(t *testing.T) { AttestationAPITimeout: commonconfig.MustNewDuration(time.Second), AttestationAPIInterval: commonconfig.MustNewDuration(500 * time.Millisecond), }, - BackgroundWorkerConfig: BackgroundWorkerConfig{ - NumWorkers: Ptr(10), + WorkerConfig: WorkerConfig{ + NumWorkers: 10, CacheExpirationInterval: commonconfig.MustNewDuration(5 * time.Second), CacheCleanupInterval: commonconfig.MustNewDuration(5 * time.Second), ObserveTimeout: commonconfig.MustNewDuration(5 * time.Second), @@ -314,7 +317,3 @@ func Test_TokenDataObserver_Validation(t *testing.T) { }) } } - -func Ptr[T any](value T) *T { - return &value -}