From 79b91086d0721b303b825ea5ec066ff801f6da2e Mon Sep 17 00:00:00 2001 From: Aleksandr Bukata Date: Fri, 29 Nov 2024 13:21:37 +0000 Subject: [PATCH] CCIP-4403 skeleton: lbtc offchain attestation --- .../plugins/ccip/ccipexec/initializers.go | 14 ++ .../ocr2/plugins/ccip/config/config.go | 4 - .../ocr2/plugins/ccip/exportinternal.go | 9 ++ .../ccip/internal/ccipdata/lbtc_reader.go | 18 +++ .../ocr2/plugins/ccip/tokendata/lbtc/lbtc.go | 91 ++++++++++-- core/services/relay/evm/evm.go | 8 +- core/services/relay/evm/exec_provider.go | 129 +++++++++++------- 7 files changed, 200 insertions(+), 73 deletions(-) diff --git a/core/services/ocr2/plugins/ccip/ccipexec/initializers.go b/core/services/ocr2/plugins/ccip/ccipexec/initializers.go index aa42ff2828..ccadd45d5b 100644 --- a/core/services/ocr2/plugins/ccip/ccipexec/initializers.go +++ b/core/services/ocr2/plugins/ccip/ccipexec/initializers.go @@ -121,6 +121,20 @@ func NewExecServices(ctx context.Context, lggr logger.Logger, jb job.Job, srcPro } tokenDataProviders[cciptypes.Address(pluginConfig.USDCConfig.SourceTokenAddress.String())] = usdcReader } + // init lbtc token data provider + if pluginConfig.LBTCConfig.AttestationAPI != "" { + lggr.Infof("LBTC token data provider enabled") + err2 := pluginConfig.LBTCConfig.ValidateLBTCConfig() + if err2 != nil { + return nil, err2 + } + + lbtcReader, err2 := srcProvider.NewTokenDataReader(ctx, ccip.EvmAddrToGeneric(pluginConfig.LBTCConfig.SourceTokenAddress)) + if err2 != nil { + return nil, fmt.Errorf("new usdc reader: %w", err2) + } + tokenDataProviders[cciptypes.Address(pluginConfig.LBTCConfig.SourceTokenAddress.String())] = lbtcReader + } // Prom wrappers onRampReader = observability.NewObservedOnRampReader(onRampReader, srcChainID, ccip.ExecPluginLabel) diff --git a/core/services/ocr2/plugins/ccip/config/config.go b/core/services/ocr2/plugins/ccip/config/config.go index 82651abe53..16da4d3255 100644 --- a/core/services/ocr2/plugins/ccip/config/config.go +++ b/core/services/ocr2/plugins/ccip/config/config.go @@ -172,9 +172,5 @@ func (lc *LBTCConfig) ValidateLBTCConfig() error { 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/exportinternal.go b/core/services/ocr2/plugins/ccip/exportinternal.go index be39346984..3558a8d807 100644 --- a/core/services/ocr2/plugins/ccip/exportinternal.go +++ b/core/services/ocr2/plugins/ccip/exportinternal.go @@ -108,7 +108,16 @@ func CloseUSDCReader(lggr logger.Logger, jobID string, transmitter common.Addres return ccipdata.CloseUSDCReader(lggr, jobID, transmitter, lp) } +func NewLBTCReader(lggr logger.Logger, jobID string, transmitter common.Address, lp logpoller.LogPoller, registerFilters bool) (*ccipdata.LBTCReaderImpl, error) { + return ccipdata.NewLBTCReader(lggr, jobID, transmitter, lp, registerFilters) +} + +func CloseLBTCReader(lggr logger.Logger, jobID string, transmitter common.Address, lp logpoller.LogPoller) error { + return ccipdata.CloseLBTCReader(lggr, jobID, transmitter, lp) +} + type USDCReaderImpl = ccipdata.USDCReaderImpl +type LBTCReaderImpl = ccipdata.LBTCReaderImpl var DefaultRpcBatchSizeLimit = rpclib.DefaultRpcBatchSizeLimit var DefaultRpcBatchBackOffMultiplier = rpclib.DefaultRpcBatchBackOffMultiplier diff --git a/core/services/ocr2/plugins/ccip/internal/ccipdata/lbtc_reader.go b/core/services/ocr2/plugins/ccip/internal/ccipdata/lbtc_reader.go index 1136d6e228..d1c26f7d6c 100644 --- a/core/services/ocr2/plugins/ccip/internal/ccipdata/lbtc_reader.go +++ b/core/services/ocr2/plugins/ccip/internal/ccipdata/lbtc_reader.go @@ -1,5 +1,23 @@ package ccipdata +import ( + "github.com/ethereum/go-ethereum/common" + + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller" + "github.com/smartcontractkit/chainlink/v2/core/logger" +) + // TODO: Implement lbtc token reader type LBTCReader interface { } + +type LBTCReaderImpl struct { +} + +func NewLBTCReader(lggr logger.Logger, jobID string, transmitter common.Address, lp logpoller.LogPoller, registerFilters bool) (*LBTCReaderImpl, error) { + return &LBTCReaderImpl{}, nil +} + +func CloseLBTCReader(lggr logger.Logger, jobID string, transmitter common.Address, lp logpoller.LogPoller) error { + return nil +} diff --git a/core/services/ocr2/plugins/ccip/tokendata/lbtc/lbtc.go b/core/services/ocr2/plugins/ccip/tokendata/lbtc/lbtc.go index 97dd8e8b47..127448d754 100644 --- a/core/services/ocr2/plugins/ccip/tokendata/lbtc/lbtc.go +++ b/core/services/ocr2/plugins/ccip/tokendata/lbtc/lbtc.go @@ -2,18 +2,21 @@ package lbtc import ( "context" - "errors" + "crypto/sha256" + "fmt" "net/url" "sync" "time" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/pkg/errors" + "golang.org/x/time/rate" + 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 @@ -55,7 +58,6 @@ var ( type TokenDataReader struct { lggr logger.Logger - lbtcReader ccipdata.LBTCReader httpClient http.IHttpClient attestationApi *url.URL attestationApiTimeout time.Duration @@ -78,13 +80,19 @@ type attestationResponse struct { Attestations []messageAttestationResponse `json:"attestations"` } +type SourceTokenData struct { + sourcePoolAddress []byte + destTokenAddress []byte + extraData []byte + destGasAmount uint32 +} + // TODO: Implement encoding/decoding var _ tokendata.Reader = &TokenDataReader{} func NewLBTCTokenDataReader( lggr logger.Logger, - lbtcReader ccipdata.LBTCReader, lbtcAttestationApi *url.URL, lbtcAttestationApiTimeoutSeconds int, lbtcTokenAddress common.Address, @@ -103,7 +111,6 @@ func NewLBTCTokenDataReader( return &TokenDataReader{ lggr: lggr, - lbtcReader: lbtcReader, httpClient: http.NewObservedIHttpClient(&http.HttpClient{}), attestationApi: lbtcAttestationApi, attestationApiTimeout: timeout, @@ -121,7 +128,6 @@ func NewLBTCTokenDataReaderWithHttpClient( ) *TokenDataReader { return &TokenDataReader{ lggr: origin.lggr, - lbtcReader: origin.lbtcReader, httpClient: httpClient, attestationApi: origin.attestationApi, attestationApiTimeout: origin.attestationApiTimeout, @@ -133,15 +139,80 @@ func NewLBTCTokenDataReaderWithHttpClient( // ReadTokenData queries the LBTC attestation API. func (s *TokenDataReader) ReadTokenData(ctx context.Context, msg cciptypes.EVM2EVMOnRampCCIPSendRequestedWithMeta, tokenIndex int) ([]byte, error) { - // TODO: Implement + if tokenIndex < 0 || tokenIndex >= len(msg.TokenAmounts) { + return nil, fmt.Errorf("token index out of bounds") + } + + if s.inCoolDownPeriod() { + // rate limiting cool-down period, we prevent new requests from being sent + return nil, tokendata.ErrRequestsBlocked + } + + if s.rate != nil { + // Wait blocks until it the attestation API can be called or the + // context is Done. + if waitErr := s.rate.Wait(ctx); waitErr != nil { + return nil, fmt.Errorf("lbtc rate limiting error: %w", waitErr) + } + } + + messageBody, err := s.getLBTCMessageBody(ctx, msg, tokenIndex) + if err != nil { + return []byte{}, errors.Wrap(err, "failed getting the LBTC message body") + } + + msgID := hexutil.Encode(msg.MessageID[:]) + messageBodyHash := sha256.Sum256(messageBody) + messageBodyHashHex := hexutil.Encode(messageBodyHash[:]) + s.lggr.Infow("Calling attestation API", "messageBodyHash", messageBodyHashHex, "messageID", msgID) + + attestationResp, err := s.callAttestationApi(ctx, messageBodyHash) + if err != nil { + return nil, err + } + if attestationResp.Attestations == nil || len(attestationResp.Attestations) == 0 { + return nil, errors.New("attestation response is empty") + } + if len(attestationResp.Attestations) > 1 { + s.lggr.Warnw("Multiple attestations received, expected one", "attestations", attestationResp.Attestations) + } + var attestation messageAttestationResponse + for _, attestationCandidate := range attestationResp.Attestations { + if attestationCandidate.MessageHash == messageBodyHashHex { + attestation = attestationCandidate + } + } + s.lggr.Infow("Got response from attestation API", "messageID", msgID, + "attestationStatus", attestation.Status, "attestation", attestation) + switch attestation.Status { + case attestationStatusSessionApproved: + messageAndAttestation, err := encodeMessageAndAttestation(messageBody, attestation.Attestation) + if err != nil { + return nil, fmt.Errorf("failed to encode messageAndAttestation : %w", err) + } + return messageAndAttestation, nil + case attestationStatusPending: + return nil, tokendata.ErrNotReady + case attestationStatusSubmitted: + return nil, tokendata.ErrNotReady + default: + s.lggr.Errorw("Unexpected response from attestation API", "attestation", attestation) + return nil, ErrUnknownResponse + } +} + +func (s *TokenDataReader) getLBTCMessageBody(ctx context.Context, msg cciptypes.EVM2EVMOnRampCCIPSendRequestedWithMeta, tokenIndex int) ([]byte, error) { return nil, nil } -func (s *TokenDataReader) callAttestationApi(ctx context.Context, usdcMessageHash [32]byte) (attestationResponse, error) { - // TODO: Implement after checking API docs +func (s *TokenDataReader) callAttestationApi(ctx context.Context, lbtcMessageHash [32]byte) (attestationResponse, error) { return attestationResponse{}, nil } +func encodeMessageAndAttestation(messageBody []byte, attestation string) ([]byte, error) { + return nil, nil +} + func (s *TokenDataReader) setCoolDownPeriod(d time.Duration) { s.coolDownMu.Lock() if d > maxCoolDownDuration { diff --git a/core/services/relay/evm/evm.go b/core/services/relay/evm/evm.go index 1dff3e7b7a..516b418ac9 100644 --- a/core/services/relay/evm/evm.go +++ b/core/services/relay/evm/evm.go @@ -563,8 +563,6 @@ func (r *Relayer) NewCCIPExecProvider(rargs commontypes.RelayArgs, pargs commont return nil, err } - usdcConfig := execPluginConfig.USDCConfig - feeEstimatorConfig := estimatorconfig.NewFeeEstimatorConfigService() // CCIPExec reads when dest chain is mantle, and uses it to calc boosting in batching @@ -591,10 +589,8 @@ func (r *Relayer) NewCCIPExecProvider(rargs commontypes.RelayArgs, pargs commont r.chain.LogPoller(), execPluginConfig.SourceStartBlock, execPluginConfig.JobID, - usdcConfig.AttestationAPI, - int(usdcConfig.AttestationAPITimeoutSeconds), - usdcConfig.AttestationAPIIntervalMilliseconds, - usdcConfig.SourceMessageTransmitterAddress, + execPluginConfig.USDCConfig, + execPluginConfig.LBTCConfig, feeEstimatorConfig, ) } diff --git a/core/services/relay/evm/exec_provider.go b/core/services/relay/evm/exec_provider.go index 215dc42e25..2b167ed74f 100644 --- a/core/services/relay/evm/exec_provider.go +++ b/core/services/relay/evm/exec_provider.go @@ -10,12 +10,13 @@ import ( "go.uber.org/multierr" "github.com/ethereum/go-ethereum/accounts/abi/bind" - "github.com/ethereum/go-ethereum/common" ocrtypes "github.com/smartcontractkit/libocr/offchainreporting2plus/types" "github.com/smartcontractkit/chainlink-common/pkg/types" commontypes "github.com/smartcontractkit/chainlink-common/pkg/types" cciptypes "github.com/smartcontractkit/chainlink-common/pkg/types/ccip" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/config" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/tokendata/lbtc" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/gas" @@ -29,18 +30,17 @@ import ( ) type SrcExecProvider struct { - lggr logger.Logger - versionFinder ccip.VersionFinder - client client.Client - lp logpoller.LogPoller - startBlock uint64 - estimator gas.EvmFeeEstimator - maxGasPrice *big.Int - usdcReader *ccip.USDCReaderImpl - usdcAttestationAPI string - usdcAttestationAPITimeoutSeconds int - usdcAttestationAPIIntervalMilliseconds int - usdcSrcMsgTransmitterAddr common.Address + lggr logger.Logger + versionFinder ccip.VersionFinder + client client.Client + lp logpoller.LogPoller + startBlock uint64 + estimator gas.EvmFeeEstimator + maxGasPrice *big.Int + usdcReader *ccip.USDCReaderImpl + usdcConfig config.USDCConfig + lbtcReader *ccip.LBTCReaderImpl + lbtcConfig config.LBTCConfig feeEstimatorConfig estimatorconfig.FeeEstimatorConfigProvider @@ -61,37 +61,39 @@ func NewSrcExecProvider( lp logpoller.LogPoller, startBlock uint64, jobID string, - usdcAttestationAPI string, - usdcAttestationAPITimeoutSeconds int, - usdcAttestationAPIIntervalMilliseconds int, - usdcSrcMsgTransmitterAddr common.Address, + usdcConfig config.USDCConfig, + lbtcConfig config.LBTCConfig, feeEstimatorConfig estimatorconfig.FeeEstimatorConfigProvider, ) (commontypes.CCIPExecProvider, error) { var usdcReader *ccip.USDCReaderImpl var err error - if usdcAttestationAPI != "" { - usdcReader, err = ccip.NewUSDCReader(lggr, jobID, usdcSrcMsgTransmitterAddr, lp, true) + if usdcConfig.AttestationAPI != "" { + usdcReader, err = ccip.NewUSDCReader(lggr, jobID, usdcConfig.SourceMessageTransmitterAddress, lp, true) + if err != nil { + return nil, fmt.Errorf("new usdc reader: %w", err) + } + } + var lbtcReader *ccip.LBTCReaderImpl + if lbtcConfig.AttestationAPI != "" { + lbtcReader, err = ccip.NewLBTCReader(lggr, jobID, lbtcConfig.SourceMessageTransmitterAddress, lp, true) if err != nil { return nil, fmt.Errorf("new usdc reader: %w", err) } } - - // TODO: Initialize lbtc reader return &SrcExecProvider{ - lggr: lggr, - versionFinder: versionFinder, - client: client, - estimator: estimator, - maxGasPrice: maxGasPrice, - lp: lp, - startBlock: startBlock, - usdcReader: usdcReader, - usdcAttestationAPI: usdcAttestationAPI, - usdcAttestationAPITimeoutSeconds: usdcAttestationAPITimeoutSeconds, - usdcAttestationAPIIntervalMilliseconds: usdcAttestationAPIIntervalMilliseconds, - usdcSrcMsgTransmitterAddr: usdcSrcMsgTransmitterAddr, - feeEstimatorConfig: feeEstimatorConfig, + lggr: lggr, + versionFinder: versionFinder, + client: client, + estimator: estimator, + maxGasPrice: maxGasPrice, + lp: lp, + startBlock: startBlock, + usdcReader: usdcReader, + usdcConfig: usdcConfig, + lbtcReader: lbtcReader, + lbtcConfig: lbtcConfig, + feeEstimatorConfig: feeEstimatorConfig, }, nil } @@ -120,10 +122,16 @@ func (s *SrcExecProvider) Close() error { return ccip.CloseOnRampReader(s.lggr, versionFinder, *s.seenSourceChainSelector, *s.seenDestChainSelector, *s.seenOnRampAddress, s.lp, s.client) }) unregisterFuncs = append(unregisterFuncs, func() error { - if s.usdcAttestationAPI == "" { + if s.usdcConfig.AttestationAPI == "" { + return nil + } + return ccip.CloseUSDCReader(s.lggr, s.lggr.Name(), s.usdcConfig.SourceMessageTransmitterAddress, s.lp) + }) + unregisterFuncs = append(unregisterFuncs, func() error { + if s.lbtcConfig.AttestationAPI == "" { return nil } - return ccip.CloseUSDCReader(s.lggr, s.lggr.Name(), s.usdcSrcMsgTransmitterAddr, s.lp) + return ccip.CloseLBTCReader(s.lggr, s.lggr.Name(), s.lbtcConfig.SourceMessageTransmitterAddress, s.lp) }) var multiErr error for _, fn := range unregisterFuncs { @@ -199,25 +207,40 @@ 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 { - return nil, fmt.Errorf("failed to parse USDC attestation API: %w", err2) +func (s *SrcExecProvider) NewTokenDataReader(ctx context.Context, tokenAddress cciptypes.Address) (cciptypes.TokenDataReader, error) { + tokenAddr, err := ccip.GenericAddrToEvm(tokenAddress) + if err != nil { + return nil, fmt.Errorf("failed to parse token address: %w", err) } - tokenAddr, err2 := ccip.GenericAddrToEvm(tokenAddress) - if err2 != nil { - return nil, fmt.Errorf("failed to parse token address: %w", err2) + switch tokenAddr { + case s.usdcConfig.SourceTokenAddress: + attestationURI, err := url.ParseRequestURI(s.usdcConfig.AttestationAPI) + if err != nil { + return nil, fmt.Errorf("failed to parse USDC attestation API: %w", err) + } + return usdc.NewUSDCTokenDataReader( + s.lggr, + s.usdcReader, + attestationURI, + int(s.usdcConfig.AttestationAPITimeoutSeconds), + tokenAddr, + time.Duration(s.usdcConfig.AttestationAPIIntervalMilliseconds)*time.Millisecond, + ), nil + case s.lbtcConfig.SourceTokenAddress: + attestationURI, err := url.ParseRequestURI(s.lbtcConfig.AttestationAPI) + if err != nil { + return nil, fmt.Errorf("failed to parse USDC attestation API: %w", err) + } + return lbtc.NewLBTCTokenDataReader( + s.lggr, + attestationURI, + int(s.lbtcConfig.AttestationAPITimeoutSeconds), + tokenAddr, + time.Duration(s.lbtcConfig.AttestationAPIIntervalMilliseconds)*time.Millisecond, + ), nil + default: + return nil, fmt.Errorf("unsupported token address: %s", tokenAddress) } - tokenDataReader = usdc.NewUSDCTokenDataReader( - s.lggr, - s.usdcReader, - attestationURI, - s.usdcAttestationAPITimeoutSeconds, - tokenAddr, - time.Duration(s.usdcAttestationAPIIntervalMilliseconds)*time.Millisecond, - ) - return } func (s *SrcExecProvider) NewTokenPoolBatchedReader(ctx context.Context, offRampAddr cciptypes.Address, sourceChainSelector uint64) (cciptypes.TokenPoolBatchedReader, error) {