From ff55000d8e78f2138fd3d5849a8ae110429508cd Mon Sep 17 00:00:00 2001 From: tcar121293 Date: Wed, 14 Aug 2024 16:02:28 +0200 Subject: [PATCH] feat: upload btc execution metadata to ipfs (#339) --- app/app.go | 6 +- chains/btc/executor/executor.go | 29 +++++++++ chains/btc/uploader/ipfs.go | 103 ++++++++++++++++++++++++++++++++ config/config_test.go | 57 ++++++++++++++++++ config/relayer/config.go | 9 +++ example/app/app.go | 7 ++- go.mod | 2 +- go.sum | 2 + tss/ecdsa/common/base.go | 2 +- tss/frost/common/base.go | 2 +- 10 files changed, 213 insertions(+), 6 deletions(-) create mode 100644 chains/btc/uploader/ipfs.go diff --git a/app/app.go b/app/app.go index ef7b3929..ac7e8612 100644 --- a/app/app.go +++ b/app/app.go @@ -17,6 +17,7 @@ import ( "github.com/ChainSafe/sygma-relayer/chains" "github.com/ChainSafe/sygma-relayer/chains/btc" "github.com/ChainSafe/sygma-relayer/chains/btc/mempool" + "github.com/ChainSafe/sygma-relayer/chains/btc/uploader" "github.com/ChainSafe/sygma-relayer/chains/evm" "github.com/ChainSafe/sygma-relayer/chains/evm/calls/contracts/bridge" "github.com/ChainSafe/sygma-relayer/chains/evm/calls/events" @@ -331,6 +332,8 @@ func Run() error { mempool := mempool.NewMempoolAPI(config.MempoolUrl) mh := &btcExecutor.BtcMessageHandler{} + uploader := uploader.NewIPFSUploader(configuration.RelayerConfig.UploaderConfig) + executor := btcExecutor.NewExecutor( propStore, host, @@ -341,7 +344,8 @@ func Run() error { mempool, resources, config.Network, - exitLock) + exitLock, + uploader) btcChain := btc.NewBtcChain(listener, executor, mh, *config.GeneralChainConfig.Id) domains[*config.GeneralChainConfig.Id] = btcChain diff --git a/chains/btc/executor/executor.go b/chains/btc/executor/executor.go index a1a75e02..cbe6b01e 100644 --- a/chains/btc/executor/executor.go +++ b/chains/btc/executor/executor.go @@ -10,6 +10,7 @@ import ( "github.com/ChainSafe/sygma-relayer/chains/btc/config" "github.com/ChainSafe/sygma-relayer/chains/btc/connection" "github.com/ChainSafe/sygma-relayer/chains/btc/mempool" + "github.com/ChainSafe/sygma-relayer/chains/btc/uploader" "github.com/ChainSafe/sygma-relayer/comm" "github.com/ChainSafe/sygma-relayer/store" "github.com/ChainSafe/sygma-relayer/tss" @@ -60,6 +61,7 @@ type Executor struct { propMutex sync.Mutex exitLock *sync.RWMutex + uploader uploader.Uploader } func NewExecutor( @@ -73,6 +75,7 @@ func NewExecutor( resources map[[32]byte]config.Resource, chainCfg chaincfg.Params, exitLock *sync.RWMutex, + uploader uploader.Uploader, ) *Executor { return &Executor{ propStorer: propStorer, @@ -85,6 +88,7 @@ func NewExecutor( resources: resources, mempool: mempool, chainCfg: chainCfg, + uploader: uploader, } } @@ -273,6 +277,8 @@ func (e *Executor) rawTx(proposals []*BtcTransferProposal, resource config.Resou func (e *Executor) outputs(tx *wire.MsgTx, proposals []*BtcTransferProposal) (uint64, error) { outputAmount := uint64(0) + var dataToUpload []map[string]interface{} + for _, prop := range proposals { addr, err := btcutil.DecodeAddress(prop.Data.Recipient, &e.chainCfg) if err != nil { @@ -282,10 +288,33 @@ func (e *Executor) outputs(tx *wire.MsgTx, proposals []*BtcTransferProposal) (ui if err != nil { return 0, err } + + dataToUpload = append(dataToUpload, map[string]interface{}{ + "sourceDomain": prop.Source, + "depositNonce": prop.Data.DepositNonce, + }) + txOut := wire.NewTxOut(int64(prop.Data.Amount), destinationAddrByte) tx.AddTxOut(txOut) + outputAmount += prop.Data.Amount } + + // Upload to IPFS + cid, err := e.uploader.Upload(dataToUpload) + if err != nil { + log.Error().Err(err).Msg("Error occured while uploading metadata to ipfs") + } + + // Store the CID in OP_RETURN + opReturnData := []byte("syg_" + cid) + opReturnScript, err := txscript.NullDataScript(opReturnData) + if err != nil { + log.Error().Err(err).Msg("Error occured while constructiong OP_RETURN data") + } + + opReturnOut := wire.NewTxOut(0, opReturnScript) + tx.AddTxOut(opReturnOut) return outputAmount, nil } diff --git a/chains/btc/uploader/ipfs.go b/chains/btc/uploader/ipfs.go new file mode 100644 index 00000000..20737e19 --- /dev/null +++ b/chains/btc/uploader/ipfs.go @@ -0,0 +1,103 @@ +package uploader + +import ( + "bytes" + "encoding/json" + "errors" + "io" + "mime/multipart" + "net/http" + "time" + + "github.com/rs/zerolog/log" + + "github.com/ChainSafe/sygma-relayer/config/relayer" + "github.com/cenkalti/backoff/v4" +) + +type Uploader interface { + Upload(proposals []map[string]interface{}) (string, error) +} + +type IPFSUploader struct { + config relayer.UploaderConfig +} + +func NewIPFSUploader(config relayer.UploaderConfig) *IPFSUploader { + return &IPFSUploader{config: config} +} + +type IPFSResponse struct { + IpfsHash string `json:"IpfsHash"` +} + +func (i *IPFSUploader) Upload(dataToUpload []map[string]interface{}) (string, error) { + jsonData, err := json.Marshal(dataToUpload) + if err != nil { + return "", err + } + + body := new(bytes.Buffer) + writer := multipart.NewWriter(body) + part, err := writer.CreateFormFile("file", "metadata.json") + if err != nil { + return "", err + } + _, err = part.Write(jsonData) + if err != nil { + return "", err + } + writer.Close() + + req, err := http.NewRequest("POST", i.config.URL, body) + if err != nil { + return "", err + } + + req.Header.Add("Authorization", "Bearer "+i.config.AuthToken) + req.Header.Add("Content-Type", writer.FormDataContentType()) + + var ipfsResponse IPFSResponse + + // Define the operation to be retried + operation := func() error { + return i.performRequest(req, &ipfsResponse) + } + expBackoff := backoff.NewExponentialBackOff() + expBackoff.MaxElapsedTime = i.config.MaxElapsedTime + + notify := func(err error, duration time.Duration) { + log.Warn().Err(err).Msg("Unable to upload metadata to ipfs") + } + + err = backoff.RetryNotify(operation, backoff.WithMaxRetries(expBackoff, i.config.MaxRetries), notify) + if err != nil { + return "", err + } + + return ipfsResponse.IpfsHash, nil +} + +func (i *IPFSUploader) performRequest(req *http.Request, ipfsResponse *IPFSResponse) error { + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + return errors.New("received non-200 status code") + } + + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return err + } + + if err := json.Unmarshal(respBody, &ipfsResponse); err != nil { + return err + } + + return nil +} diff --git a/config/config_test.go b/config/config_test.go index 32181505..c9dcd8de 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -109,6 +109,10 @@ func (s *GetConfigTestSuite) Test_GetConfigFromENV() { ElectionWaitTime: 2 * time.Second, BullyWaitTime: 3 * time.Minute, }, + UploaderConfig: relayer.UploaderConfig{ + MaxRetries: 5, + MaxElapsedTime: 300000, + }, }, ChainConfigs: []map[string]interface{}{ { @@ -212,6 +216,10 @@ func (s *GetConfigTestSuite) Test_SharedConfigLengthMismatch() { ElectionWaitTime: 2 * time.Second, BullyWaitTime: 3 * time.Minute, }, + UploaderConfig: relayer.UploaderConfig{ + MaxRetries: 5, + MaxElapsedTime: 300000, + }, }, ChainConfigs: []map[string]interface{}{ { @@ -279,6 +287,12 @@ func (s *GetConfigTestSuite) Test_GetConfigFromFile() { PingInterval: "1s", ElectionWaitTime: "1s", }, + UploaderConfig: relayer.UploaderConfig{ + URL: "https://testIPFSProvider.com", + AuthToken: "testToken", + MaxRetries: 5, + MaxElapsedTime: 5 * time.Minute, + }, }, }, shouldFail: true, @@ -297,6 +311,12 @@ func (s *GetConfigTestSuite) Test_GetConfigFromFile() { Path: "path", }, }, + UploaderConfig: relayer.UploaderConfig{ + URL: "https://testIPFSProvider.com", + AuthToken: "testToken", + MaxRetries: 5, + MaxElapsedTime: 5 * time.Minute, + }, }, ChainConfigs: []map[string]interface{}{{ "type": "evm", @@ -326,6 +346,12 @@ func (s *GetConfigTestSuite) Test_GetConfigFromFile() { PingInterval: "", ElectionWaitTime: "", }, + UploaderConfig: relayer.UploaderConfig{ + URL: "https://testIPFSProvider.com", + AuthToken: "testToken", + MaxRetries: 5, + MaxElapsedTime: 5 * time.Minute, + }, }, ChainConfigs: []map[string]interface{}{{ "type": "evm", @@ -345,7 +371,14 @@ func (s *GetConfigTestSuite) Test_GetConfigFromFile() { TopologyConfiguration: relayer.TopologyConfiguration{}, Port: "2020", }, + UploaderConfig: relayer.UploaderConfig{ + URL: "https://testIPFSProvider.com", + AuthToken: "testToken", + MaxRetries: 5, + MaxElapsedTime: 5 * time.Minute, + }, }, + ChainConfigs: []map[string]interface{}{{ "type": "evm", "name": "chain1", @@ -369,6 +402,12 @@ func (s *GetConfigTestSuite) Test_GetConfigFromFile() { }, // Port: use default value, }, + UploaderConfig: relayer.UploaderConfig{ + URL: "https://testIPFSProvider.com", + AuthToken: "testToken", + MaxRetries: 5, + MaxElapsedTime: 5 * time.Minute, + }, }, ChainConfigs: []map[string]interface{}{{ "type": "evm", @@ -399,6 +438,12 @@ func (s *GetConfigTestSuite) Test_GetConfigFromFile() { ElectionWaitTime: 2 * time.Second, BullyWaitTime: 3 * time.Minute, }, + UploaderConfig: relayer.UploaderConfig{ + URL: "https://testIPFSProvider.com", + AuthToken: "testToken", + MaxRetries: 5, + MaxElapsedTime: 5 * time.Minute, + }, }, ChainConfigs: []map[string]interface{}{{ "type": "evm", @@ -431,6 +476,12 @@ func (s *GetConfigTestSuite) Test_GetConfigFromFile() { ElectionWaitTime: "1s", BullyWaitTime: "1s", }, + UploaderConfig: relayer.UploaderConfig{ + URL: "https://testIPFSProvider.com", + AuthToken: "testToken", + MaxRetries: 5, + MaxElapsedTime: 5 * time.Minute, + }, }, ChainConfigs: []map[string]interface{}{{ "type": "evm", @@ -463,6 +514,12 @@ func (s *GetConfigTestSuite) Test_GetConfigFromFile() { ElectionWaitTime: time.Second, BullyWaitTime: time.Second, }, + UploaderConfig: relayer.UploaderConfig{ + URL: "https://testIPFSProvider.com", + AuthToken: "testToken", + MaxRetries: 5, + MaxElapsedTime: 5 * time.Minute, + }, }, ChainConfigs: []map[string]interface{}{{ "type": "evm", diff --git a/config/relayer/config.go b/config/relayer/config.go index 982039f8..2024102f 100644 --- a/config/relayer/config.go +++ b/config/relayer/config.go @@ -21,6 +21,7 @@ type RelayerConfig struct { Id string MpcConfig MpcRelayerConfig BullyConfig BullyConfig + UploaderConfig UploaderConfig } type MpcRelayerConfig struct { @@ -46,6 +47,12 @@ type TopologyConfiguration struct { Path string `mapstructure:"Path" json:"path"` } +type UploaderConfig struct { + URL string `mapstructure:"url"` + AuthToken string `mapstructure:"authToken"` + MaxRetries uint64 `mapstructure:"MaxRetries" json:"maxRetries" default:"5"` + MaxElapsedTime time.Duration `mapstructure:"MaxElapsedTime" json:"maxElapsedTime" default:"300000"` // 5 min +} type RawRelayerConfig struct { OpenTelemetryCollectorURL string `mapstructure:"OpenTelemetryCollectorURL" json:"opentelemetryCollectorURL"` LogLevel string `mapstructure:"LogLevel" json:"logLevel" default:"info"` @@ -55,6 +62,7 @@ type RawRelayerConfig struct { Id string `mapstructure:"Id" json:"id"` MpcConfig RawMpcRelayerConfig `mapstructure:"MpcConfig" json:"mpcConfig"` BullyConfig RawBullyConfig `mapstructure:"BullyConfig" json:"bullyConfig"` + UploaderConfig UploaderConfig `mapstructure:"uploaderConfig"` } type RawMpcRelayerConfig struct { @@ -123,6 +131,7 @@ func NewRelayerConfig(rawConfig RawRelayerConfig) (RelayerConfig, error) { config.BullyConfig = bullyConfig config.Env = rawConfig.Env config.Id = rawConfig.Id + config.UploaderConfig = rawConfig.UploaderConfig return config, nil } diff --git a/example/app/app.go b/example/app/app.go index 86e4ba9d..9e5568a3 100644 --- a/example/app/app.go +++ b/example/app/app.go @@ -14,10 +14,12 @@ import ( "github.com/ChainSafe/sygma-relayer/chains/btc" "github.com/ChainSafe/sygma-relayer/chains/btc/mempool" + "github.com/ChainSafe/sygma-relayer/chains/btc/uploader" substrateListener "github.com/ChainSafe/sygma-relayer/chains/substrate/listener" substratePallet "github.com/ChainSafe/sygma-relayer/chains/substrate/pallet" "github.com/ChainSafe/sygma-relayer/relayer/transfer" propStore "github.com/ChainSafe/sygma-relayer/store" + "github.com/ChainSafe/sygma-relayer/tss" "github.com/sygmaprotocol/sygma-core/chains/evm/listener" "github.com/sygmaprotocol/sygma-core/chains/evm/transactor/gas" "github.com/sygmaprotocol/sygma-core/chains/evm/transactor/transaction" @@ -61,7 +63,6 @@ import ( "github.com/ChainSafe/sygma-relayer/config" "github.com/ChainSafe/sygma-relayer/keyshare" "github.com/ChainSafe/sygma-relayer/topology" - "github.com/ChainSafe/sygma-relayer/tss" evmClient "github.com/sygmaprotocol/sygma-core/chains/evm/client" ) @@ -274,6 +275,7 @@ func Run() error { mempool := mempool.NewMempoolAPI(config.MempoolUrl) mh := &btcExecutor.BtcMessageHandler{} + uploader := uploader.NewIPFSUploader(configuration.RelayerConfig.UploaderConfig) executor := btcExecutor.NewExecutor( propStore, host, @@ -284,7 +286,8 @@ func Run() error { mempool, resources, config.Network, - exitLock) + exitLock, + uploader) btcChain := btc.NewBtcChain(listener, executor, mh, *config.GeneralChainConfig.Id) chains[*config.GeneralChainConfig.Id] = btcChain diff --git a/go.mod b/go.mod index c6b391ab..0f2db091 100644 --- a/go.mod +++ b/go.mod @@ -34,7 +34,7 @@ require ( github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f // indirect github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd // indirect github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 // indirect - github.com/cenkalti/backoff/v4 v4.2.1 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/consensys/bavard v0.1.13 // indirect github.com/consensys/gnark-crypto v0.12.1 // indirect github.com/crate-crypto/go-kzg-4844 v0.3.0 // indirect diff --git a/go.sum b/go.sum index d4bd1121..8863740c 100644 --- a/go.sum +++ b/go.sum @@ -123,6 +123,8 @@ github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46f github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/centrifuge/go-substrate-rpc-client/v4 v4.2.1 h1:io49TJ8IOIlzipioJc9pJlrjgdJvqktpUWYxVY5AUjE= github.com/centrifuge/go-substrate-rpc-client/v4 v4.2.1/go.mod h1:k61SBXqYmnZO4frAJyH3iuqjolYrYsq79r8EstmklDY= diff --git a/tss/ecdsa/common/base.go b/tss/ecdsa/common/base.go index b08b40f4..aec2ec7a 100644 --- a/tss/ecdsa/common/base.go +++ b/tss/ecdsa/common/base.go @@ -49,7 +49,7 @@ func (b *BaseTss) PopulatePartyStore(parties tss.SortedPartyIDs) { func (b *BaseTss) ProcessInboundMessages(ctx context.Context, msgChan chan *comm.WrappedMessage) (err error) { defer func() { if r := recover(); r != nil { - err = fmt.Errorf(string(debug.Stack())) + err = fmt.Errorf("%s", string(debug.Stack())) } }() diff --git a/tss/frost/common/base.go b/tss/frost/common/base.go index 5ed4362c..7fdd37f6 100644 --- a/tss/frost/common/base.go +++ b/tss/frost/common/base.go @@ -37,7 +37,7 @@ type BaseFrostTss struct { func (k *BaseFrostTss) ProcessInboundMessages(ctx context.Context, msgChan chan *comm.WrappedMessage) (err error) { defer func() { if r := recover(); r != nil { - err = fmt.Errorf(string(debug.Stack())) + err = fmt.Errorf("%s", string(debug.Stack())) } }()