From e7fd86d9019430a8e47573cd24401923627bde21 Mon Sep 17 00:00:00 2001 From: Nour Elrashidy Date: Thu, 28 Nov 2024 23:43:25 +0100 Subject: [PATCH] Add skeleton for lbtc attestation API integration --- .../ocr2/plugins/ccip/config/config.go | 36 +++- .../ccip/internal/ccipdata/lbtc_reader.go | 5 + .../ccip/tokendata/http/http_client.go | 10 +- .../ocr2/plugins/ccip/tokendata/lbtc/lbtc.go | 162 ++++++++++++++++++ core/services/relay/evm/exec_provider.go | 5 + 5 files changed, 213 insertions(+), 5 deletions(-) create mode 100644 core/services/ocr2/plugins/ccip/internal/ccipdata/lbtc_reader.go create mode 100644 core/services/ocr2/plugins/ccip/tokendata/lbtc/lbtc.go diff --git a/core/services/ocr2/plugins/ccip/config/config.go b/core/services/ocr2/plugins/ccip/config/config.go index a24a6edfd13..82651abe531 100644 --- a/core/services/ocr2/plugins/ccip/config/config.go +++ b/core/services/ocr2/plugins/ccip/config/config.go @@ -108,6 +108,7 @@ func (c *DynamicPriceGetterConfig) Validate() error { type ExecPluginJobSpecConfig struct { SourceStartBlock, DestStartBlock uint64 // Only for first time job add. USDCConfig USDCConfig + LBTCConfig LBTCConfig } type USDCConfig struct { @@ -119,10 +120,20 @@ type USDCConfig struct { AttestationAPIIntervalMilliseconds int } +type LBTCConfig struct { + SourceTokenAddress common.Address + SourceMessageTransmitterAddress common.Address + AttestationAPI string + AttestationAPITimeoutSeconds uint + // AttestationAPIIntervalMilliseconds can be set to -1 to disable or 0 to use a default interval. + AttestationAPIIntervalMilliseconds int +} + type ExecPluginConfig struct { SourceStartBlock, DestStartBlock uint64 // Only for first time job add. IsSourceProvider bool USDCConfig USDCConfig + LBTCConfig LBTCConfig JobID string } @@ -136,16 +147,33 @@ func (e ExecPluginConfig) Encode() ([]byte, error) { func (uc *USDCConfig) ValidateUSDCConfig() error { if uc.AttestationAPI == "" { - return errors.New("AttestationAPI is required") + return errors.New("USDCConfig: AttestationAPI is required") } if uc.AttestationAPIIntervalMilliseconds < -1 { - return errors.New("AttestationAPIIntervalMilliseconds must be -1 to disable, 0 for default or greater to define the exact interval") + return errors.New("USDCConfig: AttestationAPIIntervalMilliseconds must be -1 to disable, 0 for default or greater to define the exact interval") } if uc.SourceTokenAddress == utils.ZeroAddress { - return errors.New("SourceTokenAddress is required") + return errors.New("USDCConfig: SourceTokenAddress is required") } if uc.SourceMessageTransmitterAddress == utils.ZeroAddress { - return errors.New("SourceMessageTransmitterAddress is required") + return errors.New("USDCConfig: SourceMessageTransmitterAddress is required") + } + + return nil +} + +func (lc *LBTCConfig) ValidateLBTCConfig() error { + if lc.AttestationAPI == "" { + return errors.New("LBTCConfig: AttestationAPI is required") + } + if lc.AttestationAPIIntervalMilliseconds < -1 { + return errors.New("LBTCConfig: AttestationAPIIntervalMilliseconds must be -1 to disable, 0 for default or greater to define the exact interval") + } + if lc.SourceTokenAddress == utils.ZeroAddress { + return errors.New("LBTCConfig: SourceTokenAddress is required") + } + if lc.SourceMessageTransmitterAddress == utils.ZeroAddress { + return errors.New("LBTCConfig: SourceMessageTransmitterAddress is required") } return nil diff --git a/core/services/ocr2/plugins/ccip/internal/ccipdata/lbtc_reader.go b/core/services/ocr2/plugins/ccip/internal/ccipdata/lbtc_reader.go new file mode 100644 index 00000000000..1136d6e228f --- /dev/null +++ b/core/services/ocr2/plugins/ccip/internal/ccipdata/lbtc_reader.go @@ -0,0 +1,5 @@ +package ccipdata + +// TODO: Implement lbtc token reader +type LBTCReader interface { +} diff --git a/core/services/ocr2/plugins/ccip/tokendata/http/http_client.go b/core/services/ocr2/plugins/ccip/tokendata/http/http_client.go index 79ec21b1b83..d8e80b914cb 100644 --- a/core/services/ocr2/plugins/ccip/tokendata/http/http_client.go +++ b/core/services/ocr2/plugins/ccip/tokendata/http/http_client.go @@ -12,8 +12,11 @@ import ( ) type IHttpClient interface { - // Get issue a GET request to the given url and return the response body and status code. + // Get issues a GET request to the given url and returns the response body and status code. Get(ctx context.Context, url string, timeout time.Duration) ([]byte, int, http.Header, error) + + // Post issues a POST request to the given url with the given request data and returns the response body and status code. + Post(ctx context.Context, url string, requestData io.Reader, timeout time.Duration) ([]byte, int, http.Header, error) } type HttpClient struct { @@ -46,3 +49,8 @@ func (s *HttpClient) Get(ctx context.Context, url string, timeout time.Duration) body, err := io.ReadAll(res.Body) return body, res.StatusCode, res.Header, err } + +func (s *HttpClient) Post(ctx context.Context, url string, requestData io.Reader, timeout time.Duration) ([]byte, int, http.Header, error) { + // TODO: Implement + return nil, 0, nil, nil +} diff --git a/core/services/ocr2/plugins/ccip/tokendata/lbtc/lbtc.go b/core/services/ocr2/plugins/ccip/tokendata/lbtc/lbtc.go new file mode 100644 index 00000000000..97dd8e8b472 --- /dev/null +++ b/core/services/ocr2/plugins/ccip/tokendata/lbtc/lbtc.go @@ -0,0 +1,162 @@ +package lbtc + +import ( + "context" + "errors" + "net/url" + "sync" + "time" + + "github.com/ethereum/go-ethereum/common" + cciptypes "github.com/smartcontractkit/chainlink-common/pkg/types/ccip" + "github.com/smartcontractkit/chainlink/v2/core/logger" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipdata" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/tokendata" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/tokendata/http" + "golang.org/x/time/rate" +) + +// TODO: double check the validty of default values for lombard's API after checking docs +const ( + apiVersion = "v1" + attestationPath = "deposits/getByHash" + defaultAttestationTimeout = 5 * time.Second + + // defaultCoolDownDurationSec defines the default time to wait after getting rate limited. + // this value is only used if the 429 response does not contain the Retry-After header + defaultCoolDownDuration = 5 * time.Minute + + // maxCoolDownDuration defines the maximum duration we can wait till firing the next request + maxCoolDownDuration = 10 * time.Minute + + // defaultRequestInterval defines the rate in requests per second that the attestation API can be called. + // this is set according to the APIs documentated 10 requests per second rate limit. + defaultRequestInterval = 100 * time.Millisecond + + // APIIntervalRateLimitDisabled is a special value to disable the rate limiting. + APIIntervalRateLimitDisabled = -1 + // APIIntervalRateLimitDefault is a special value to select the default rate limit interval. + APIIntervalRateLimitDefault = 0 +) + +type attestationStatus string + +const ( + attestationStatusUnspecified attestationStatus = "NOTARIZATION_STATUS_UNSPECIFIED" + attestationStatusPending attestationStatus = "NOTARIZATION_STATUS_PENDING" + attestationStatusSubmitted attestationStatus = "NOTARIZATION_STATUS_SUBMITTED" + attestationStatusSessionApproved attestationStatus = "NOTARIZATION_STATUS_SESSION_APPROVED" + attestationStatusFailed attestationStatus = "NOTARIZATION_STATUS_FAILED" +) + +var ( + ErrUnknownResponse = errors.New("unexpected response from attestation API") +) + +type TokenDataReader struct { + lggr logger.Logger + lbtcReader ccipdata.LBTCReader + httpClient http.IHttpClient + attestationApi *url.URL + attestationApiTimeout time.Duration + lbtcTokenAddress common.Address + rate *rate.Limiter + + // coolDownUntil defines whether requests are blocked or not. + coolDownUntil time.Time + coolDownMu *sync.RWMutex +} + +type messageAttestationResponse struct { + MessageHash string `json:"message_hash"` + Status attestationStatus `json:"status"` + Attestation string `json:"attestation"` +} + +// TODO: Adjust after checking API docs +type attestationResponse struct { + Attestations []messageAttestationResponse `json:"attestations"` +} + +// TODO: Implement encoding/decoding + +var _ tokendata.Reader = &TokenDataReader{} + +func NewLBTCTokenDataReader( + lggr logger.Logger, + lbtcReader ccipdata.LBTCReader, + lbtcAttestationApi *url.URL, + lbtcAttestationApiTimeoutSeconds int, + lbtcTokenAddress common.Address, + requestInterval time.Duration, +) *TokenDataReader { + timeout := time.Duration(lbtcAttestationApiTimeoutSeconds) * time.Second + if lbtcAttestationApiTimeoutSeconds == 0 { + timeout = defaultAttestationTimeout + } + + if requestInterval == APIIntervalRateLimitDisabled { + requestInterval = 0 + } else if requestInterval == APIIntervalRateLimitDefault { + requestInterval = defaultRequestInterval + } + + return &TokenDataReader{ + lggr: lggr, + lbtcReader: lbtcReader, + httpClient: http.NewObservedIHttpClient(&http.HttpClient{}), + attestationApi: lbtcAttestationApi, + attestationApiTimeout: timeout, + lbtcTokenAddress: lbtcTokenAddress, + coolDownMu: &sync.RWMutex{}, + rate: rate.NewLimiter(rate.Every(requestInterval), 1), + } +} + +func NewLBTCTokenDataReaderWithHttpClient( + origin TokenDataReader, + httpClient http.IHttpClient, + lbtcTokenAddress common.Address, + requestInterval time.Duration, +) *TokenDataReader { + return &TokenDataReader{ + lggr: origin.lggr, + lbtcReader: origin.lbtcReader, + httpClient: httpClient, + attestationApi: origin.attestationApi, + attestationApiTimeout: origin.attestationApiTimeout, + coolDownMu: origin.coolDownMu, + lbtcTokenAddress: lbtcTokenAddress, + rate: rate.NewLimiter(rate.Every(requestInterval), 1), + } +} + +// ReadTokenData queries the LBTC attestation API. +func (s *TokenDataReader) ReadTokenData(ctx context.Context, msg cciptypes.EVM2EVMOnRampCCIPSendRequestedWithMeta, tokenIndex int) ([]byte, error) { + // TODO: Implement + return nil, nil +} + +func (s *TokenDataReader) callAttestationApi(ctx context.Context, usdcMessageHash [32]byte) (attestationResponse, error) { + // TODO: Implement after checking API docs + return attestationResponse{}, nil +} + +func (s *TokenDataReader) setCoolDownPeriod(d time.Duration) { + s.coolDownMu.Lock() + if d > maxCoolDownDuration { + d = maxCoolDownDuration + } + s.coolDownUntil = time.Now().Add(d) + s.coolDownMu.Unlock() +} + +func (s *TokenDataReader) inCoolDownPeriod() bool { + s.coolDownMu.RLock() + defer s.coolDownMu.RUnlock() + return time.Now().Before(s.coolDownUntil) +} + +func (s *TokenDataReader) Close() error { + return nil +} diff --git a/core/services/relay/evm/exec_provider.go b/core/services/relay/evm/exec_provider.go index da190d20356..f05141e1bc2 100644 --- a/core/services/relay/evm/exec_provider.go +++ b/core/services/relay/evm/exec_provider.go @@ -41,6 +41,8 @@ type SrcExecProvider struct { usdcAttestationAPIIntervalMilliseconds int usdcSrcMsgTransmitterAddr common.Address + // TODO: Add lbtc reader & api fields + // these values are nil and are updated for Close() seenOnRampAddress *cciptypes.Address seenSourceChainSelector *uint64 @@ -71,6 +73,8 @@ func NewSrcExecProvider( } } + // TODO: Initialize lbtc reader + return &SrcExecProvider{ lggr: logger.Named(lggr, "SrcExecProvider"), versionFinder: versionFinder, @@ -188,6 +192,7 @@ func (s *SrcExecProvider) NewPriceRegistryReader(ctx context.Context, addr ccipt return } +// TODO: refactor to handle lbtc tokens. Separate methods. func (s *SrcExecProvider) NewTokenDataReader(ctx context.Context, tokenAddress cciptypes.Address) (tokenDataReader cciptypes.TokenDataReader, err error) { attestationURI, err2 := url.ParseRequestURI(s.usdcAttestationAPI) if err2 != nil {