From eb432060ef3fa823437e5039137320ecbbe84cc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matija=20Petruni=C4=87?= Date: Mon, 4 Nov 2024 17:58:39 +0100 Subject: [PATCH] feat: cross network retry (#337) --- Makefile | 6 +- app/app.go | 42 ++- chains/btc/chain.go | 4 +- chains/btc/executor/executor.go | 5 - chains/btc/executor/message-handler.go | 81 ++++- chains/btc/executor/message-handler_test.go | 172 +++++++++- chains/btc/executor/mock/message-handler.go | 159 +++++++++ chains/btc/listener/event-handlers.go | 77 +++-- chains/btc/listener/event-handlers_test.go | 13 +- chains/evm/calls/consts/retry.go | 117 +++++++ chains/evm/calls/events/events.go | 12 +- chains/evm/calls/events/listener.go | 41 ++- chains/evm/calls/events/listener_test.go | 10 +- chains/evm/config.go | 6 +- chains/evm/config_test.go | 2 + chains/evm/executor/message-handler.go | 68 ++++ chains/evm/executor/message-handler_test.go | 148 +++++++++ chains/evm/executor/mock/message-handler.go | 142 ++++++++ chains/evm/listener/eventHandlers/deposit.go | 91 +++++ .../listener/eventHandlers/deposit_test.go | 211 ++++++++++++ .../listener/eventHandlers/mock/listener.go | 70 +--- .../evm/listener/eventHandlers/mock/retry.go | 64 ++++ chains/evm/listener/eventHandlers/retry.go | 201 ++++++++++++ .../{event-handler_test.go => retry_test.go} | 310 ++++++------------ .../{event-handler.go => tss.go} | 203 ------------ chains/substrate/executor/message-handler.go | 73 +++++ .../executor/message-handler_test.go | 171 ++++++++++ .../executor/mock/message-handler.go | 158 +++++++++ chains/substrate/listener/event-handlers.go | 24 +- e2e/btc/btc_test.go | 2 +- e2e/evm/contracts/retry/retry.go | 39 +++ e2e/evm/evm_test.go | 27 +- e2e/evm/util.go | 36 +- example/app/app.go | 117 ++++--- example/cfg/config_evm-evm_1.json | 18 +- example/cfg/config_evm-evm_2.json | 18 +- example/cfg/config_evm-evm_3.json | 18 +- example/docker-compose.yml | 8 +- relayer/retry/retry.go | 89 +++++ relayer/retry/retry_test.go | 149 +++++++++ 40 files changed, 2532 insertions(+), 670 deletions(-) create mode 100644 chains/btc/executor/mock/message-handler.go create mode 100644 chains/evm/calls/consts/retry.go create mode 100644 chains/evm/executor/mock/message-handler.go create mode 100644 chains/evm/listener/eventHandlers/deposit.go create mode 100644 chains/evm/listener/eventHandlers/deposit_test.go create mode 100644 chains/evm/listener/eventHandlers/mock/retry.go create mode 100644 chains/evm/listener/eventHandlers/retry.go rename chains/evm/listener/eventHandlers/{event-handler_test.go => retry_test.go} (54%) rename chains/evm/listener/eventHandlers/{event-handler.go => tss.go} (50%) create mode 100644 chains/substrate/executor/mock/message-handler.go create mode 100644 e2e/evm/contracts/retry/retry.go create mode 100644 relayer/retry/retry.go create mode 100644 relayer/retry/retry_test.go diff --git a/Makefile b/Makefile index fc84ec99..74ad0868 100644 --- a/Makefile +++ b/Makefile @@ -30,12 +30,16 @@ genmocks: mockgen --package mock_tss -destination=./tss/mock/frost.go -source=./tss/frost/keygen/keygen.go mockgen -source=./tss/coordinator.go -destination=./tss/mock/coordinator.go mockgen -source=./comm/communication.go -destination=./comm/mock/communication.go - mockgen -source=./chains/evm/listener/eventHandlers/event-handler.go -destination=./chains/evm/listener/eventHandlers/mock/listener.go + mockgen -source=./chains/evm/listener/eventHandlers/deposit.go -destination=./chains/evm/listener/eventHandlers/mock/listener.go + mockgen -source=./chains/evm/listener/eventHandlers/retry.go -destination=./chains/evm/listener/eventHandlers/mock/retry.go mockgen -source=./chains/evm/calls/events/listener.go -destination=./chains/evm/calls/events/mock/listener.go mockgen -source=./chains/substrate/listener/event-handlers.go -destination=./chains/substrate/listener/mock/handlers.go mockgen -source=./chains/btc/listener/event-handlers.go -destination=./chains/btc/listener/mock/handlers.go mockgen -source=./chains/btc/listener/listener.go -destination=./chains/btc/listener/mock/listener.go mockgen -source=./topology/topology.go -destination=./topology/mock/topology.go + mockgen -source=./chains/btc/executor/message-handler.go -destination=./chains/btc/executor/mock/message-handler.go + mockgen -source=./chains/substrate/executor/message-handler.go -destination=./chains/substrate/executor/mock/message-handler.go + mockgen -source=./chains/evm/executor/message-handler.go -destination=./chains/evm/executor/mock/message-handler.go e2e-test: diff --git a/app/app.go b/app/app.go index d96e5df9..9babd382 100644 --- a/app/app.go +++ b/app/app.go @@ -23,8 +23,9 @@ import ( "github.com/ChainSafe/sygma-relayer/chains/evm/calls/events" "github.com/ChainSafe/sygma-relayer/chains/evm/executor" "github.com/ChainSafe/sygma-relayer/chains/evm/listener/depositHandlers" - hubEventHandlers "github.com/ChainSafe/sygma-relayer/chains/evm/listener/eventHandlers" + evmEventHandlers "github.com/ChainSafe/sygma-relayer/chains/evm/listener/eventHandlers" "github.com/ChainSafe/sygma-relayer/chains/substrate" + "github.com/ChainSafe/sygma-relayer/relayer/retry" "github.com/ChainSafe/sygma-relayer/relayer/transfer" propStore "github.com/ChainSafe/sygma-relayer/store" "github.com/sygmaprotocol/sygma-core/chains/evm/transactor/gas" @@ -192,11 +193,7 @@ func Run() error { bridgeContract := bridge.NewBridgeContract(client, bridgeAddress, t) depositHandler := depositHandlers.NewETHDepositHandler(bridgeContract) - mh := message.NewMessageHandler() for _, handler := range config.Handlers { - - mh.RegisterMessageHandler(transfer.TransferMessageType, &executor.TransferMessageHandler{}) - switch handler.Type { case "erc20", "native": { @@ -220,12 +217,21 @@ func Run() error { tssListener := events.NewListener(client) eventHandlers := make([]listener.EventHandler, 0) l := log.With().Str("chain", fmt.Sprintf("%v", config.GeneralChainConfig.Name)).Uint8("domainID", *config.GeneralChainConfig.Id) - eventHandlers = append(eventHandlers, hubEventHandlers.NewDepositEventHandler(depositListener, depositHandler, bridgeAddress, *config.GeneralChainConfig.Id, msgChan)) - eventHandlers = append(eventHandlers, hubEventHandlers.NewKeygenEventHandler(l, tssListener, coordinator, host, communication, keyshareStore, bridgeAddress, networkTopology.Threshold)) - eventHandlers = append(eventHandlers, hubEventHandlers.NewFrostKeygenEventHandler(l, tssListener, coordinator, host, communication, frostKeyshareStore, frostAddress, networkTopology.Threshold)) - eventHandlers = append(eventHandlers, hubEventHandlers.NewRefreshEventHandler(l, topologyProvider, topologyStore, tssListener, coordinator, host, communication, connectionGate, keyshareStore, frostKeyshareStore, bridgeAddress)) - eventHandlers = append(eventHandlers, hubEventHandlers.NewRetryEventHandler(l, tssListener, depositHandler, propStore, bridgeAddress, *config.GeneralChainConfig.Id, config.BlockConfirmations, msgChan)) + + depositEventHandler := evmEventHandlers.NewDepositEventHandler(depositListener, depositHandler, bridgeAddress, *config.GeneralChainConfig.Id, msgChan) + eventHandlers = append(eventHandlers, depositEventHandler) + eventHandlers = append(eventHandlers, evmEventHandlers.NewKeygenEventHandler(l, tssListener, coordinator, host, communication, keyshareStore, bridgeAddress, networkTopology.Threshold)) + eventHandlers = append(eventHandlers, evmEventHandlers.NewFrostKeygenEventHandler(l, tssListener, coordinator, host, communication, frostKeyshareStore, frostAddress, networkTopology.Threshold)) + eventHandlers = append(eventHandlers, evmEventHandlers.NewRefreshEventHandler(l, topologyProvider, topologyStore, tssListener, coordinator, host, communication, connectionGate, keyshareStore, frostKeyshareStore, bridgeAddress)) + eventHandlers = append(eventHandlers, evmEventHandlers.NewRetryV1EventHandler(l, tssListener, depositHandler, propStore, bridgeAddress, *config.GeneralChainConfig.Id, config.BlockConfirmations, msgChan)) + if config.Retry != "" { + eventHandlers = append(eventHandlers, evmEventHandlers.NewRetryV2EventHandler(l, tssListener, common.HexToAddress(config.Retry), *config.GeneralChainConfig.Id, msgChan)) + } evmListener := listener.NewEVMListener(client, eventHandlers, blockstore, sygmaMetrics, *config.GeneralChainConfig.Id, config.BlockRetryInterval, config.BlockConfirmations, config.BlockInterval) + + mh := message.NewMessageHandler() + mh.RegisterMessageHandler(retry.RetryMessageType, executor.NewRetryMessageHandler(depositEventHandler, client, propStore, config.BlockConfirmations, msgChan)) + mh.RegisterMessageHandler(transfer.TransferMessageType, &executor.TransferMessageHandler{}) executor := executor.NewExecutor(host, communication, coordinator, bridgeContract, keyshareStore, exitLock, config.GasLimit.Uint64(), config.TransferGas) startBlock, err := blockstore.GetStartBlock(*config.GeneralChainConfig.Id, config.StartBlock, config.GeneralChainConfig.LatestBlock, config.GeneralChainConfig.FreshStart) @@ -272,13 +278,14 @@ func Run() error { depositHandler := substrateListener.NewSubstrateDepositHandler() depositHandler.RegisterDepositHandler(transfer.FungibleTransfer, substrateListener.FungibleTransferHandler) eventHandlers := make([]coreSubstrateListener.EventHandler, 0) - eventHandlers = append(eventHandlers, substrateListener.NewFungibleTransferEventHandler(l, *config.GeneralChainConfig.Id, depositHandler, msgChan, conn)) + depositEventHandler := substrateListener.NewFungibleTransferEventHandler(l, *config.GeneralChainConfig.Id, depositHandler, msgChan, conn) eventHandlers = append(eventHandlers, substrateListener.NewRetryEventHandler(l, conn, depositHandler, *config.GeneralChainConfig.Id, msgChan)) - + eventHandlers = append(eventHandlers, depositEventHandler) substrateListener := coreSubstrateListener.NewSubstrateListener(conn, eventHandlers, blockstore, sygmaMetrics, *config.GeneralChainConfig.Id, config.BlockRetryInterval, config.BlockInterval) mh := message.NewMessageHandler() mh.RegisterMessageHandler(transfer.TransferMessageType, &substrateExecutor.SubstrateMessageHandler{}) + mh.RegisterMessageHandler(retry.RetryMessageType, substrateExecutor.NewRetryMessageHandler(depositEventHandler, conn, propStore, msgChan)) sExecutor := substrateExecutor.NewExecutor(host, communication, coordinator, bridgePallet, keyshareStore, conn, exitLock) @@ -319,17 +326,20 @@ func Run() error { } l := log.With().Str("chain", fmt.Sprintf("%v", config.GeneralChainConfig.Name)).Uint8("domainID", *config.GeneralChainConfig.Id) - depositHandler := &btcListener.BtcDepositHandler{} - eventHandlers := make([]btcListener.EventHandler, 0) resources := make(map[[32]byte]btcConfig.Resource) for _, resource := range config.Resources { resources[resource.ResourceID] = resource - eventHandlers = append(eventHandlers, btcListener.NewFungibleTransferEventHandler(l, *config.GeneralChainConfig.Id, depositHandler, msgChan, conn, resource, config.FeeAddress)) } + depositHandler := &btcListener.BtcDepositHandler{} + depositEventHandler := btcListener.NewFungibleTransferEventHandler(l, *config.GeneralChainConfig.Id, depositHandler, msgChan, conn, resources, config.FeeAddress) + eventHandlers := make([]btcListener.EventHandler, 0) + eventHandlers = append(eventHandlers, depositEventHandler) listener := btcListener.NewBtcListener(conn, eventHandlers, config, blockstore) mempool := mempool.NewMempoolAPI(config.MempoolUrl) - mh := &btcExecutor.BtcMessageHandler{} + mh := message.NewMessageHandler() + mh.RegisterMessageHandler(transfer.TransferMessageType, &btcExecutor.FungibleMessageHandler{}) + mh.RegisterMessageHandler(retry.RetryMessageType, btcExecutor.NewRetryMessageHandler(depositEventHandler, conn, config.BlockConfirmations, propStore, msgChan)) uploader := uploader.NewIPFSUploader(configuration.RelayerConfig.UploaderConfig) executor := btcExecutor.NewExecutor( diff --git a/chains/btc/chain.go b/chains/btc/chain.go index 6a524e61..3049808f 100644 --- a/chains/btc/chain.go +++ b/chains/btc/chain.go @@ -25,7 +25,7 @@ type BtcChain struct { listener EventListener executor *executor.Executor - mh *executor.BtcMessageHandler + mh *message.MessageHandler startBlock *big.Int logger zerolog.Logger @@ -34,7 +34,7 @@ type BtcChain struct { func NewBtcChain( listener EventListener, executor *executor.Executor, - mh *executor.BtcMessageHandler, + mh *message.MessageHandler, id uint8, ) *BtcChain { return &BtcChain{ diff --git a/chains/btc/executor/executor.go b/chains/btc/executor/executor.go index 10378d3b..17d1383d 100644 --- a/chains/btc/executor/executor.go +++ b/chains/btc/executor/executor.go @@ -41,11 +41,6 @@ type MempoolAPI interface { Utxos(address string) ([]mempool.Utxo, error) } -type PropStorer interface { - StorePropStatus(source, destination uint8, depositNonce uint64, status store.PropStatus) error - PropStatus(source, destination uint8, depositNonce uint64) (store.PropStatus, error) -} - type Executor struct { coordinator *tss.Coordinator host host.Host diff --git a/chains/btc/executor/message-handler.go b/chains/btc/executor/message-handler.go index 1c37813c..8c1d66fc 100644 --- a/chains/btc/executor/message-handler.go +++ b/chains/btc/executor/message-handler.go @@ -5,12 +5,17 @@ package executor import ( "errors" + "fmt" "math/big" + "github.com/btcsuite/btcd/btcjson" + "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/sygmaprotocol/sygma-core/relayer/message" "github.com/sygmaprotocol/sygma-core/relayer/proposal" + "github.com/ChainSafe/sygma-relayer/relayer/retry" "github.com/ChainSafe/sygma-relayer/relayer/transfer" + "github.com/ChainSafe/sygma-relayer/store" ) type BtcTransferProposalData struct { @@ -26,9 +31,9 @@ type BtcTransferProposal struct { Data BtcTransferProposalData } -type BtcMessageHandler struct{} +type FungibleMessageHandler struct{} -func (h *BtcMessageHandler) HandleMessage(msg *message.Message) (*proposal.Proposal, error) { +func (h *FungibleMessageHandler) HandleMessage(msg *message.Message) (*proposal.Proposal, error) { transferMessage := &transfer.TransferMessage{ Source: msg.Source, Destination: msg.Destination, @@ -70,3 +75,75 @@ func ERC20MessageHandler(msg *transfer.TransferMessage) (*proposal.Proposal, err ResourceId: msg.Data.ResourceId, }, msg.ID, transfer.TransferProposalType), nil } + +type BlockFetcher interface { + GetBlockVerboseTx(*chainhash.Hash) (*btcjson.GetBlockVerboseTxResult, error) + GetBestBlockHash() (*chainhash.Hash, error) +} + +type PropStorer interface { + StorePropStatus(source, destination uint8, depositNonce uint64, status store.PropStatus) error + PropStatus(source, destination uint8, depositNonce uint64) (store.PropStatus, error) +} + +type DepositProcessor interface { + ProcessDeposits(blockNumber *big.Int) (map[uint8][]*message.Message, error) +} + +type RetryMessageHandler struct { + depositProcessor DepositProcessor + blockFetcher BlockFetcher + blockConfirmations *big.Int + propStorer PropStorer + msgChan chan []*message.Message +} + +func NewRetryMessageHandler( + depositProcessor DepositProcessor, + blockFetcher BlockFetcher, + blockConfirmations *big.Int, + propStorer PropStorer, + msgChan chan []*message.Message) *RetryMessageHandler { + return &RetryMessageHandler{ + depositProcessor: depositProcessor, + blockFetcher: blockFetcher, + blockConfirmations: blockConfirmations, + propStorer: propStorer, + msgChan: msgChan, + } +} + +func (h *RetryMessageHandler) HandleMessage(msg *message.Message) (*proposal.Proposal, error) { + retryData := msg.Data.(retry.RetryMessageData) + hash, err := h.blockFetcher.GetBestBlockHash() + if err != nil { + return nil, err + } + block, err := h.blockFetcher.GetBlockVerboseTx(hash) + if err != nil { + return nil, err + } + latestBlock := big.NewInt(block.Height) + if latestBlock.Cmp(new(big.Int).Add(retryData.BlockHeight, h.blockConfirmations)) != 1 { + return nil, fmt.Errorf( + "latest block %s higher than receipt block number + block confirmations %s", + latestBlock, + new(big.Int).Add(retryData.BlockHeight, h.blockConfirmations), + ) + } + + domainDeposits, err := h.depositProcessor.ProcessDeposits(retryData.BlockHeight) + if err != nil { + return nil, err + } + filteredDeposits, err := retry.FilterDeposits(h.propStorer, domainDeposits, retryData.ResourceID, retryData.DestinationDomainID) + if err != nil { + return nil, err + } + if len(filteredDeposits) == 0 { + return nil, nil + } + + h.msgChan <- filteredDeposits + return nil, nil +} diff --git a/chains/btc/executor/message-handler_test.go b/chains/btc/executor/message-handler_test.go index d66f5409..28538c3d 100644 --- a/chains/btc/executor/message-handler_test.go +++ b/chains/btc/executor/message-handler_test.go @@ -5,7 +5,14 @@ import ( "testing" "github.com/ChainSafe/sygma-relayer/chains/btc/executor" + mock_executor "github.com/ChainSafe/sygma-relayer/chains/btc/executor/mock" + "github.com/ChainSafe/sygma-relayer/e2e/evm" + "github.com/ChainSafe/sygma-relayer/relayer/retry" "github.com/ChainSafe/sygma-relayer/relayer/transfer" + "github.com/ChainSafe/sygma-relayer/store" + "github.com/btcsuite/btcd/btcjson" + "github.com/ethereum/go-ethereum/common" + "github.com/golang/mock/gomock" "github.com/stretchr/testify/suite" "github.com/sygmaprotocol/sygma-core/relayer/message" "github.com/sygmaprotocol/sygma-core/relayer/proposal" @@ -35,7 +42,7 @@ func (s *BtcMessageHandlerTestSuite) Test_ERC20HandleMessage_ValidMessage() { Type: transfer.TransferMessageType, } - mh := executor.BtcMessageHandler{} + mh := executor.FungibleMessageHandler{} prop, err := mh.HandleMessage(message) s.Nil(err) @@ -68,7 +75,7 @@ func (s *BtcMessageHandlerTestSuite) Test_ERC20HandleMessage_ValidMessage_Dust() Type: transfer.TransferMessageType, } - mh := executor.BtcMessageHandler{} + mh := executor.FungibleMessageHandler{} prop, err := mh.HandleMessage(message) s.Nil(err) @@ -100,7 +107,7 @@ func (s *BtcMessageHandlerTestSuite) Test_ERC20HandleMessage_IncorrectDataLen() Type: transfer.TransferMessageType, } - mh := executor.BtcMessageHandler{} + mh := executor.FungibleMessageHandler{} prop, err := mh.HandleMessage(message) s.Nil(prop) @@ -124,7 +131,7 @@ func (s *BtcMessageHandlerTestSuite) Test_ERC20HandleMessage_IncorrectAmount() { Type: transfer.TransferMessageType, } - mh := executor.BtcMessageHandler{} + mh := executor.FungibleMessageHandler{} prop, err := mh.HandleMessage(message) s.Nil(prop) @@ -147,7 +154,7 @@ func (s *BtcMessageHandlerTestSuite) Test_ERC20HandleMessage_IncorrectRecipient( Type: transfer.TransferMessageType, } - mh := executor.BtcMessageHandler{} + mh := executor.FungibleMessageHandler{} prop, err := mh.HandleMessage(message) s.Nil(prop) @@ -170,9 +177,162 @@ func (s *BtcMessageHandlerTestSuite) Test_HandleMessage_InvalidType() { Type: transfer.TransferMessageType, } - mh := executor.BtcMessageHandler{} + mh := executor.FungibleMessageHandler{} prop, err := mh.HandleMessage(message) s.Nil(prop) s.NotNil(err) } + +type RetryMessageHandlerTestSuite struct { + suite.Suite + + messageHandler *executor.RetryMessageHandler + mockBlockFetcher *mock_executor.MockBlockFetcher + mockDepositProcessor *mock_executor.MockDepositProcessor + mockPropStorer *mock_executor.MockPropStorer + msgChan chan []*message.Message +} + +func TestRunRetryMessageHandlerTestSuite(t *testing.T) { + suite.Run(t, new(RetryMessageHandlerTestSuite)) +} + +func (s *RetryMessageHandlerTestSuite) SetupTest() { + ctrl := gomock.NewController(s.T()) + s.mockBlockFetcher = mock_executor.NewMockBlockFetcher(ctrl) + s.mockDepositProcessor = mock_executor.NewMockDepositProcessor(ctrl) + s.mockPropStorer = mock_executor.NewMockPropStorer(ctrl) + s.msgChan = make(chan []*message.Message, 1) + s.messageHandler = executor.NewRetryMessageHandler( + s.mockDepositProcessor, + s.mockBlockFetcher, + big.NewInt(5), + s.mockPropStorer, + s.msgChan) +} + +func (s *RetryMessageHandlerTestSuite) Test_HandleMessage_RetryTooNew() { + s.mockBlockFetcher.EXPECT().GetBestBlockHash().Return(nil, nil) + s.mockBlockFetcher.EXPECT().GetBlockVerboseTx(gomock.Any()).Return(&btcjson.GetBlockVerboseTxResult{ + Height: 105, + }, nil) + + message := &message.Message{ + Source: 1, + Destination: 3, + Data: retry.RetryMessageData{ + SourceDomainID: 3, + DestinationDomainID: 4, + BlockHeight: big.NewInt(100), + ResourceID: [32]byte{}, + }, + Type: transfer.TransferMessageType, + } + + prop, err := s.messageHandler.HandleMessage(message) + + s.Nil(prop) + s.NotNil(err) +} + +func (s *RetryMessageHandlerTestSuite) Test_HandleMessage_NoDeposits() { + s.mockBlockFetcher.EXPECT().GetBestBlockHash().Return(nil, nil) + s.mockBlockFetcher.EXPECT().GetBlockVerboseTx(gomock.Any()).Return(&btcjson.GetBlockVerboseTxResult{ + Height: 106, + }, nil) + s.mockDepositProcessor.EXPECT().ProcessDeposits(big.NewInt(100)).Return(make(map[uint8][]*message.Message), nil) + + message := &message.Message{ + Source: 1, + Destination: 3, + Data: retry.RetryMessageData{ + SourceDomainID: 3, + DestinationDomainID: 4, + BlockHeight: big.NewInt(100), + ResourceID: [32]byte{}, + }, + Type: transfer.TransferMessageType, + } + + prop, err := s.messageHandler.HandleMessage(message) + + s.Nil(prop) + s.Nil(err) + s.Equal(len(s.msgChan), 0) +} + +func (s *RetryMessageHandlerTestSuite) Test_HandleMessage_ValidDeposits() { + s.mockBlockFetcher.EXPECT().GetBestBlockHash().Return(nil, nil) + s.mockBlockFetcher.EXPECT().GetBlockVerboseTx(gomock.Any()).Return(&btcjson.GetBlockVerboseTxResult{ + Height: 106, + }, nil) + + validResource := evm.SliceTo32Bytes(common.LeftPadBytes([]byte{3}, 31)) + invalidResource := evm.SliceTo32Bytes(common.LeftPadBytes([]byte{4}, 31)) + invalidDomain := uint8(3) + validDomain := uint8(4) + + executedNonce := uint64(1) + failedNonce := uint64(3) + + deposits := make(map[uint8][]*message.Message) + deposits[invalidDomain] = []*message.Message{ + { + Destination: invalidDomain, + Data: transfer.TransferMessageData{ + DepositNonce: 1, + ResourceId: validResource, + }, + }, + } + deposits[validDomain] = []*message.Message{ + { + Source: invalidDomain, + Destination: validDomain, + Data: transfer.TransferMessageData{ + DepositNonce: executedNonce, + ResourceId: validResource, + }, + }, + { + Source: invalidDomain, + Destination: validDomain, + Data: transfer.TransferMessageData{ + DepositNonce: 2, + ResourceId: invalidResource, + }, + }, + { + Source: invalidDomain, + Destination: validDomain, + Data: transfer.TransferMessageData{ + DepositNonce: failedNonce, + ResourceId: validResource, + }, + }, + } + s.mockDepositProcessor.EXPECT().ProcessDeposits(big.NewInt(100)).Return(deposits, nil) + s.mockPropStorer.EXPECT().PropStatus(invalidDomain, validDomain, executedNonce).Return(store.ExecutedProp, nil) + s.mockPropStorer.EXPECT().PropStatus(invalidDomain, validDomain, failedNonce).Return(store.FailedProp, nil) + + message := &message.Message{ + Source: 1, + Destination: 3, + Data: retry.RetryMessageData{ + SourceDomainID: invalidDomain, + DestinationDomainID: validDomain, + BlockHeight: big.NewInt(100), + ResourceID: validResource, + }, + Type: transfer.TransferMessageType, + } + + prop, err := s.messageHandler.HandleMessage(message) + + s.Nil(prop) + s.Nil(err) + msgs := <-s.msgChan + s.Equal(msgs[0].Data.(transfer.TransferMessageData).DepositNonce, failedNonce) + s.Equal(msgs[0].Destination, validDomain) +} diff --git a/chains/btc/executor/mock/message-handler.go b/chains/btc/executor/mock/message-handler.go new file mode 100644 index 00000000..d32d2355 --- /dev/null +++ b/chains/btc/executor/mock/message-handler.go @@ -0,0 +1,159 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: ./chains/btc/executor/message-handler.go + +// Package mock_executor is a generated GoMock package. +package mock_executor + +import ( + big "math/big" + reflect "reflect" + + store "github.com/ChainSafe/sygma-relayer/store" + btcjson "github.com/btcsuite/btcd/btcjson" + chainhash "github.com/btcsuite/btcd/chaincfg/chainhash" + gomock "github.com/golang/mock/gomock" + message "github.com/sygmaprotocol/sygma-core/relayer/message" +) + +// MockBlockFetcher is a mock of BlockFetcher interface. +type MockBlockFetcher struct { + ctrl *gomock.Controller + recorder *MockBlockFetcherMockRecorder +} + +// MockBlockFetcherMockRecorder is the mock recorder for MockBlockFetcher. +type MockBlockFetcherMockRecorder struct { + mock *MockBlockFetcher +} + +// NewMockBlockFetcher creates a new mock instance. +func NewMockBlockFetcher(ctrl *gomock.Controller) *MockBlockFetcher { + mock := &MockBlockFetcher{ctrl: ctrl} + mock.recorder = &MockBlockFetcherMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockBlockFetcher) EXPECT() *MockBlockFetcherMockRecorder { + return m.recorder +} + +// GetBestBlockHash mocks base method. +func (m *MockBlockFetcher) GetBestBlockHash() (*chainhash.Hash, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetBestBlockHash") + ret0, _ := ret[0].(*chainhash.Hash) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetBestBlockHash indicates an expected call of GetBestBlockHash. +func (mr *MockBlockFetcherMockRecorder) GetBestBlockHash() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBestBlockHash", reflect.TypeOf((*MockBlockFetcher)(nil).GetBestBlockHash)) +} + +// GetBlockVerboseTx mocks base method. +func (m *MockBlockFetcher) GetBlockVerboseTx(arg0 *chainhash.Hash) (*btcjson.GetBlockVerboseTxResult, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetBlockVerboseTx", arg0) + ret0, _ := ret[0].(*btcjson.GetBlockVerboseTxResult) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetBlockVerboseTx indicates an expected call of GetBlockVerboseTx. +func (mr *MockBlockFetcherMockRecorder) GetBlockVerboseTx(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBlockVerboseTx", reflect.TypeOf((*MockBlockFetcher)(nil).GetBlockVerboseTx), arg0) +} + +// MockPropStorer is a mock of PropStorer interface. +type MockPropStorer struct { + ctrl *gomock.Controller + recorder *MockPropStorerMockRecorder +} + +// MockPropStorerMockRecorder is the mock recorder for MockPropStorer. +type MockPropStorerMockRecorder struct { + mock *MockPropStorer +} + +// NewMockPropStorer creates a new mock instance. +func NewMockPropStorer(ctrl *gomock.Controller) *MockPropStorer { + mock := &MockPropStorer{ctrl: ctrl} + mock.recorder = &MockPropStorerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockPropStorer) EXPECT() *MockPropStorerMockRecorder { + return m.recorder +} + +// PropStatus mocks base method. +func (m *MockPropStorer) PropStatus(source, destination uint8, depositNonce uint64) (store.PropStatus, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PropStatus", source, destination, depositNonce) + ret0, _ := ret[0].(store.PropStatus) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// PropStatus indicates an expected call of PropStatus. +func (mr *MockPropStorerMockRecorder) PropStatus(source, destination, depositNonce interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PropStatus", reflect.TypeOf((*MockPropStorer)(nil).PropStatus), source, destination, depositNonce) +} + +// StorePropStatus mocks base method. +func (m *MockPropStorer) StorePropStatus(source, destination uint8, depositNonce uint64, status store.PropStatus) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "StorePropStatus", source, destination, depositNonce, status) + ret0, _ := ret[0].(error) + return ret0 +} + +// StorePropStatus indicates an expected call of StorePropStatus. +func (mr *MockPropStorerMockRecorder) StorePropStatus(source, destination, depositNonce, status interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StorePropStatus", reflect.TypeOf((*MockPropStorer)(nil).StorePropStatus), source, destination, depositNonce, status) +} + +// MockDepositProcessor is a mock of DepositProcessor interface. +type MockDepositProcessor struct { + ctrl *gomock.Controller + recorder *MockDepositProcessorMockRecorder +} + +// MockDepositProcessorMockRecorder is the mock recorder for MockDepositProcessor. +type MockDepositProcessorMockRecorder struct { + mock *MockDepositProcessor +} + +// NewMockDepositProcessor creates a new mock instance. +func NewMockDepositProcessor(ctrl *gomock.Controller) *MockDepositProcessor { + mock := &MockDepositProcessor{ctrl: ctrl} + mock.recorder = &MockDepositProcessorMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockDepositProcessor) EXPECT() *MockDepositProcessorMockRecorder { + return m.recorder +} + +// ProcessDeposits mocks base method. +func (m *MockDepositProcessor) ProcessDeposits(blockNumber *big.Int) (map[uint8][]*message.Message, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ProcessDeposits", blockNumber) + ret0, _ := ret[0].(map[uint8][]*message.Message) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ProcessDeposits indicates an expected call of ProcessDeposits. +func (mr *MockDepositProcessorMockRecorder) ProcessDeposits(blockNumber interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ProcessDeposits", reflect.TypeOf((*MockDepositProcessor)(nil).ProcessDeposits), blockNumber) +} diff --git a/chains/btc/listener/event-handlers.go b/chains/btc/listener/event-handlers.go index b2e44ae5..42d85858 100644 --- a/chains/btc/listener/event-handlers.go +++ b/chains/btc/listener/event-handlers.go @@ -46,10 +46,17 @@ type FungibleTransferEventHandler struct { log zerolog.Logger conn Connection msgChan chan []*message.Message - resource config.Resource + resources map[[32]byte]config.Resource } -func NewFungibleTransferEventHandler(logC zerolog.Context, domainID uint8, depositHandler DepositHandler, msgChan chan []*message.Message, conn Connection, resource config.Resource, feeAddress btcutil.Address) *FungibleTransferEventHandler { +func NewFungibleTransferEventHandler( + logC zerolog.Context, + domainID uint8, + depositHandler DepositHandler, + msgChan chan []*message.Message, + conn Connection, + resources map[[32]byte]config.Resource, + feeAddress btcutil.Address) *FungibleTransferEventHandler { return &FungibleTransferEventHandler{ depositHandler: depositHandler, domainID: domainID, @@ -57,16 +64,29 @@ func NewFungibleTransferEventHandler(logC zerolog.Context, domainID uint8, depos log: logC.Logger(), conn: conn, msgChan: msgChan, - resource: resource, + resources: resources, } } func (eh *FungibleTransferEventHandler) HandleEvents(blockNumber *big.Int) error { + domainDeposits, err := eh.ProcessDeposits(blockNumber) + if err != nil { + return err + } + + for _, deposits := range domainDeposits { + go func(d []*message.Message) { + eh.msgChan <- d + }(deposits) + } + return nil +} + +func (eh *FungibleTransferEventHandler) ProcessDeposits(blockNumber *big.Int) (map[uint8][]*message.Message, error) { domainDeposits := make(map[uint8][]*message.Message) evts, err := eh.FetchEvents(blockNumber) if err != nil { - eh.log.Error().Err(err).Msg("Error fetching events") - return err + return nil, err } for _, evt := range evts { err := func(evt btcjson.TxRawResult) error { @@ -76,39 +96,36 @@ func (eh *FungibleTransferEventHandler) HandleEvents(blockNumber *big.Int) error } }() - d, isDeposit, err := DecodeDepositEvent(evt, eh.resource, eh.feeAddress) - if err != nil { - return err - } + for _, resource := range eh.resources { + d, isDeposit, err := DecodeDepositEvent(evt, resource, eh.feeAddress) + if err != nil { + return err + } - if !isDeposit { - return nil - } - nonce, err := eh.CalculateNonce(blockNumber, evt.Hash) - if err != nil { - return err - } + if !isDeposit { + continue + } + nonce, err := eh.CalculateNonce(blockNumber, evt.Hash) + if err != nil { + return err + } - m, err := eh.depositHandler.HandleDeposit(eh.domainID, nonce, d.ResourceID, d.Amount, d.Data, blockNumber, time.Unix(evt.Blocktime, 0)) - if err != nil { - return err - } + m, err := eh.depositHandler.HandleDeposit(eh.domainID, nonce, d.ResourceID, d.Amount, d.Data, blockNumber, time.Unix(evt.Blocktime, 0)) + if err != nil { + return err + } - log.Debug().Str("messageID", m.ID).Msgf("Resolved message %+v in block: %s", m, blockNumber.String()) - domainDeposits[m.Destination] = append(domainDeposits[m.Destination], m) + log.Debug().Str("messageID", m.ID).Msgf("Resolved message %+v in block: %s", m, blockNumber.String()) + domainDeposits[m.Destination] = append(domainDeposits[m.Destination], m) + return nil + } return nil }(evt) if err != nil { - log.Error().Err(err).Msgf("%v", err) + log.Error().Err(err).Msgf("Failed processing Bitcoin deposit %v", evt) } } - - for _, deposits := range domainDeposits { - go func(d []*message.Message) { - eh.msgChan <- d - }(deposits) - } - return nil + return domainDeposits, nil } func (eh *FungibleTransferEventHandler) FetchEvents(startBlock *big.Int) ([]btcjson.TxRawResult, error) { diff --git a/chains/btc/listener/event-handlers_test.go b/chains/btc/listener/event-handlers_test.go index 31f52159..c6db4604 100644 --- a/chains/btc/listener/event-handlers_test.go +++ b/chains/btc/listener/event-handlers_test.go @@ -31,7 +31,7 @@ type DepositHandlerTestSuite struct { fungibleTransferEventHandler *listener.FungibleTransferEventHandler mockDepositHandler *mock_listener.MockDepositHandler domainID uint8 - resource config.Resource + resources map[[32]byte]config.Resource msgChan chan []*message.Message mockConn *mock_listener.MockConnection feeAddress btcutil.Address @@ -44,14 +44,17 @@ func TestRunDepositHandlerTestSuite(t *testing.T) { func (s *DepositHandlerTestSuite) SetupTest() { ctrl := gomock.NewController(s.T()) s.domainID = 1 - address, _ := btcutil.DecodeAddress("tb1pdf5c3q35ssem2l25n435fa69qr7dzwkc6gsqehuflr3euh905l2slafjvv", &chaincfg.TestNet3Params) + address1, _ := btcutil.DecodeAddress("tb1pdf5c3q35ssem2l25n435fa69qr7dzwkc6gsqehuflr3euh905l2slafjvv", &chaincfg.TestNet3Params) + address2, _ := btcutil.DecodeAddress("tb1pdf5c3q35ssem2l25n435fa69qr7dzwkc6gsqehuflr3euh905l2slafjvc", &chaincfg.TestNet3Params) s.feeAddress, _ = btcutil.DecodeAddress("tb1qln69zuhdunc9stwfh6t7adexxrcr04ppy6thgm", &chaincfg.TestNet3Params) - s.resource = config.Resource{Address: address, ResourceID: [32]byte{}, FeeAmount: big.NewInt(10000)} + s.resources = make(map[[32]byte]config.Resource) + s.resources[[32]byte{1}] = config.Resource{Address: address1, ResourceID: [32]byte{1}, FeeAmount: big.NewInt(10000)} + s.resources[[32]byte{2}] = config.Resource{Address: address2, ResourceID: [32]byte{2}, FeeAmount: big.NewInt(10001)} s.mockDepositHandler = mock_listener.NewMockDepositHandler(ctrl) s.msgChan = make(chan []*message.Message, 2) s.mockConn = mock_listener.NewMockConnection(ctrl) - s.fungibleTransferEventHandler = listener.NewFungibleTransferEventHandler(zerolog.Context{}, s.domainID, s.mockDepositHandler, s.msgChan, s.mockConn, s.resource, s.feeAddress) + s.fungibleTransferEventHandler = listener.NewFungibleTransferEventHandler(zerolog.Context{}, s.domainID, s.mockDepositHandler, s.msgChan, s.mockConn, s.resources, s.feeAddress) } func (s *DepositHandlerTestSuite) Test_FetchDepositFails_GetBlockHashError() { @@ -82,7 +85,7 @@ func (s *DepositHandlerTestSuite) Test_HandleDepositFails_ExecutionContinue() { blockNumber := big.NewInt(100) data2 := map[string]any{ "deposit_nonce": uint64(8228687738678474667), - "resource_id": [32]byte{0}, + "resource_id": [32]byte{1}, "amount": big.NewInt(19000), "deposit_data": "0xe9f23A8289764280697a03aC06795eA92a170e42_1", } diff --git a/chains/evm/calls/consts/retry.go b/chains/evm/calls/consts/retry.go new file mode 100644 index 00000000..6e801348 --- /dev/null +++ b/chains/evm/calls/consts/retry.go @@ -0,0 +1,117 @@ +package consts + +const RetryABI = ` +[ + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previousOwner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnershipTransferred", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint8", + "name": "sourceDomainID", + "type": "uint8" + }, + { + "indexed": false, + "internalType": "uint8", + "name": "destinationDomainID", + "type": "uint8" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "blockHeight", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "bytes32", + "name": "resourceID", + "type": "bytes32" + } + ], + "name": "Retry", + "type": "event" + }, + { + "inputs": [], + "name": "owner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "renounceOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint8", + "name": "sourceDomainID", + "type": "uint8" + }, + { + "internalType": "uint8", + "name": "destinationDomainID", + "type": "uint8" + }, + { + "internalType": "uint256", + "name": "blockHeight", + "type": "uint256" + }, + { + "internalType": "bytes32", + "name": "resourceID", + "type": "bytes32" + } + ], + "name": "retry", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +] +` diff --git a/chains/evm/calls/events/events.go b/chains/evm/calls/events/events.go index c18b5e75..ca2d006e 100644 --- a/chains/evm/calls/events/events.go +++ b/chains/evm/calls/events/events.go @@ -4,6 +4,7 @@ package events import ( + "math/big" "time" "github.com/ethereum/go-ethereum/common" @@ -23,7 +24,7 @@ const ( KeyRefreshSig EventSig = "KeyRefresh(string)" ProposalExecutionSig EventSig = "ProposalExecution(uint8,uint64,bytes32,bytes)" FeeChangedSig EventSig = "FeeChanged(uint256)" - RetrySig EventSig = "Retry(string)" + RetrySig EventSig = "Retry(uint8,uint8,uint256,bytes32)" FeeHandlerChanged EventSig = "FeeHandlerChanged(address)" ) @@ -33,10 +34,17 @@ type Refresh struct { Hash string } -type RetryEvent struct { +type RetryV1Event struct { TxHash string } +type RetryV2Event struct { + SourceDomainID uint8 + DestinationDomainID uint8 + BlockHeight *big.Int + ResourceID [32]byte +} + type Deposit struct { // ID of chain deposit will be bridged to DestinationDomainID uint8 diff --git a/chains/evm/calls/events/listener.go b/chains/evm/calls/events/listener.go index 4da66460..d0f0c384 100644 --- a/chains/evm/calls/events/listener.go +++ b/chains/evm/calls/events/listener.go @@ -26,15 +26,18 @@ type ChainClient interface { } type Listener struct { - client ChainClient - abi abi.ABI + client ChainClient + abi abi.ABI + retryAbi abi.ABI } func NewListener(client ChainClient) *Listener { + retryAbi, _ := abi.JSON(strings.NewReader(consts.RetryABI)) abi, _ := abi.JSON(strings.NewReader(consts.BridgeABI)) return &Listener{ - client: client, - abi: abi, + client: client, + abi: abi, + retryAbi: retryAbi, } } @@ -78,7 +81,7 @@ func (l *Listener) parseDeposit(ctx context.Context, dl ethTypes.Log) (*Deposit, return &d, nil } -func (l *Listener) FetchRetryDepositEvents(event RetryEvent, bridgeAddress common.Address, blockConfirmations *big.Int) ([]Deposit, error) { +func (l *Listener) FetchRetryDepositEvents(event RetryV1Event, bridgeAddress common.Address, blockConfirmations *big.Int) ([]Deposit, error) { depositEvents := make([]Deposit, 0) retryDepositTxHash := common.HexToHash(event.TxHash) receipt, err := l.client.WaitAndReturnTxReceipt(retryDepositTxHash) @@ -115,15 +118,15 @@ func (l *Listener) FetchRetryDepositEvents(event RetryEvent, bridgeAddress commo return depositEvents, nil } -func (l *Listener) FetchRetryEvents(ctx context.Context, contractAddress common.Address, startBlock *big.Int, endBlock *big.Int) ([]RetryEvent, error) { +func (l *Listener) FetchRetryV1Events(ctx context.Context, contractAddress common.Address, startBlock *big.Int, endBlock *big.Int) ([]RetryV1Event, error) { logs, err := l.client.FetchEventLogs(ctx, contractAddress, string(RetrySig), startBlock, endBlock) if err != nil { return nil, err } - var retryEvents []RetryEvent + var retryEvents []RetryV1Event for _, dl := range logs { - var event RetryEvent + var event RetryV1Event err = l.abi.UnpackIntoInterface(&event, "Retry", dl.Data) if err != nil { log.Error().Msgf( @@ -137,6 +140,28 @@ func (l *Listener) FetchRetryEvents(ctx context.Context, contractAddress common. return retryEvents, nil } +func (l *Listener) FetchRetryV2Events(ctx context.Context, contractAddress common.Address, startBlock *big.Int, endBlock *big.Int) ([]RetryV2Event, error) { + logs, err := l.client.FetchEventLogs(ctx, contractAddress, string(RetrySig), startBlock, endBlock) + if err != nil { + return nil, err + } + + var retryEvents []RetryV2Event + for _, dl := range logs { + var event RetryV2Event + err = l.retryAbi.UnpackIntoInterface(&event, "Retry", dl.Data) + if err != nil { + log.Error().Msgf( + "failed unpacking retry event with txhash %s, because of: %+v", dl.TxHash.Hex(), err, + ) + continue + } + retryEvents = append(retryEvents, event) + } + + return retryEvents, nil +} + func (l *Listener) FetchKeygenEvents(ctx context.Context, contractAddress common.Address, startBlock *big.Int, endBlock *big.Int) ([]ethTypes.Log, error) { logs, err := l.client.FetchEventLogs(ctx, contractAddress, string(StartKeygenSig), startBlock, endBlock) if err != nil { diff --git a/chains/evm/calls/events/listener_test.go b/chains/evm/calls/events/listener_test.go index db4f861f..c6686f58 100644 --- a/chains/evm/calls/events/listener_test.go +++ b/chains/evm/calls/events/listener_test.go @@ -36,7 +36,7 @@ func (s *ListenerTestSuite) SetupTest() { func (s *ListenerTestSuite) Test_FetchRetryDepositEvents_FetchingTxFails() { s.mockClient.EXPECT().WaitAndReturnTxReceipt(common.HexToHash("0xf25ed4a14bf7ad20354b46fe38d7d4525f2ea3042db9a9954ef8d73c558b500c")).Return(nil, fmt.Errorf("error")) - _, err := s.listener.FetchRetryDepositEvents(events.RetryEvent{TxHash: "0xf25ed4a14bf7ad20354b46fe38d7d4525f2ea3042db9a9954ef8d73c558b500c"}, common.Address{}, big.NewInt(5)) + _, err := s.listener.FetchRetryDepositEvents(events.RetryV1Event{TxHash: "0xf25ed4a14bf7ad20354b46fe38d7d4525f2ea3042db9a9954ef8d73c558b500c"}, common.Address{}, big.NewInt(5)) s.NotNil(err) } @@ -48,7 +48,7 @@ func (s *ListenerTestSuite) Test_FetchRetryDepositEvents_EventTooNew() { s.mockClient.EXPECT().LatestBlock().Return(big.NewInt(10), nil) _, err := s.listener.FetchRetryDepositEvents( - events.RetryEvent{TxHash: "0xf25ed4a14bf7ad20354b46fe38d7d4525f2ea3042db9a9954ef8d73c558b500c"}, + events.RetryV1Event{TxHash: "0xf25ed4a14bf7ad20354b46fe38d7d4525f2ea3042db9a9954ef8d73c558b500c"}, common.HexToAddress("0x5798e01f4b1d8f6a5d91167414f3a915d021bc4a"), big.NewInt(5), ) @@ -63,7 +63,7 @@ func (s *ListenerTestSuite) Test_FetchRetryDepositEvents_NoDepositEvent() { s.mockClient.EXPECT().LatestBlock().Return(big.NewInt(20), nil) deposits, err := s.listener.FetchRetryDepositEvents( - events.RetryEvent{TxHash: "0xf25ed4a14bf7ad20354b46fe38d7d4525f2ea3042db9a9954ef8d73c558b500c"}, + events.RetryV1Event{TxHash: "0xf25ed4a14bf7ad20354b46fe38d7d4525f2ea3042db9a9954ef8d73c558b500c"}, common.HexToAddress("0x5798e01f4b1d8f6a5d91167414f3a915d021bc4a"), big.NewInt(5), ) @@ -89,7 +89,7 @@ func (s *ListenerTestSuite) Test_FetchRetryDepositEvents_NoMatchingEvent() { s.mockClient.EXPECT().LatestBlock().Return(big.NewInt(20), nil) deposits, err := s.listener.FetchRetryDepositEvents( - events.RetryEvent{TxHash: "0xf25ed4a14bf7ad20354b46fe38d7d4525f2ea3042db9a9954ef8d73c558b500c"}, + events.RetryV1Event{TxHash: "0xf25ed4a14bf7ad20354b46fe38d7d4525f2ea3042db9a9954ef8d73c558b500c"}, common.HexToAddress("0x5798e01f4b1d8f6a5d91167414f3a915d021bc4a"), big.NewInt(5), ) @@ -127,7 +127,7 @@ func (s *ListenerTestSuite) Test_FetchRetryDepositEvents_ValidEvent() { s.mockClient.EXPECT().BlockByNumber(gomock.Any(), big.NewInt(14)).Return(nil, fmt.Errorf("error")) deposits, err := s.listener.FetchRetryDepositEvents( - events.RetryEvent{TxHash: "0xf25ed4a14bf7ad20354b46fe38d7d4525f2ea3042db9a9954ef8d73c558b500c"}, + events.RetryV1Event{TxHash: "0xf25ed4a14bf7ad20354b46fe38d7d4525f2ea3042db9a9954ef8d73c558b500c"}, common.HexToAddress("0x5798e01f4b1d8f6a5d91167414f3a915d021bc4a"), big.NewInt(5), ) diff --git a/chains/evm/config.go b/chains/evm/config.go index a6803b0d..139e178b 100644 --- a/chains/evm/config.go +++ b/chains/evm/config.go @@ -24,6 +24,7 @@ type HandlerConfig struct { type EVMConfig struct { GeneralChainConfig chain.GeneralChainConfig Bridge string + Retry string FrostKeygen string Handlers []HandlerConfig MaxGasPrice *big.Int @@ -40,7 +41,7 @@ type EVMConfig struct { func (c *EVMConfig) String() string { privateKey, _ := crypto.HexToECDSA(c.GeneralChainConfig.Key) kp := secp256k1.NewKeypair(*privateKey) - return fmt.Sprintf(`Name: '%s', Id: '%d', Type: '%s', BlockstorePath: '%s', FreshStart: '%t', LatestBlock: '%t', Key address: '%s', Bridge: '%s', Handlers: %+v, MaxGasPrice: '%s', GasMultiplier: '%s', GasLimit: '%s', TransferGas: '%d', StartBlock: '%s', BlockConfirmations: '%s', BlockInterval: '%s', BlockRetryInterval: '%s'`, + return fmt.Sprintf(`Name: '%s', Id: '%d', Type: '%s', BlockstorePath: '%s', FreshStart: '%t', LatestBlock: '%t', Key address: '%s', Bridge: '%s', Retry: '%s', Handlers: %+v, MaxGasPrice: '%s', GasMultiplier: '%s', GasLimit: '%s', TransferGas: '%d', StartBlock: '%s', BlockConfirmations: '%s', BlockInterval: '%s', BlockRetryInterval: '%s'`, c.GeneralChainConfig.Name, *c.GeneralChainConfig.Id, c.GeneralChainConfig.Type, @@ -49,6 +50,7 @@ func (c *EVMConfig) String() string { c.GeneralChainConfig.LatestBlock, kp.Address(), c.Bridge, + c.Retry, c.Handlers, c.MaxGasPrice, c.GasMultiplier, @@ -64,6 +66,7 @@ func (c *EVMConfig) String() string { type RawEVMConfig struct { chain.GeneralChainConfig `mapstructure:",squash"` Bridge string `mapstructure:"bridge"` + Retry string `mapstructure:"retry"` FrostKeygen string `mapstructure:"frostKeygen"` Handlers []HandlerConfig `mapstrcture:"handlers"` MaxGasPrice int64 `mapstructure:"maxGasPrice" default:"500000000000"` @@ -114,6 +117,7 @@ func NewEVMConfig(chainConfig map[string]interface{}) (*EVMConfig, error) { GeneralChainConfig: c.GeneralChainConfig, Handlers: c.Handlers, Bridge: c.Bridge, + Retry: c.Retry, FrostKeygen: c.FrostKeygen, BlockRetryInterval: time.Duration(c.BlockRetryInterval) * time.Second, GasLimit: big.NewInt(c.GasLimit), diff --git a/chains/evm/config_test.go b/chains/evm/config_test.go index 0453ea36..292c4a0b 100644 --- a/chains/evm/config_test.go +++ b/chains/evm/config_test.go @@ -102,6 +102,7 @@ func (s *NewEVMConfigTestSuite) Test_ValidConfigWithCustomTxParams() { "name": "evm1", "from": "address", "bridge": "bridgeAddress", + "retry": "retryAddress", "frostKeygen": "frostKeygen", "handlers": []evm.HandlerConfig{ { @@ -136,6 +137,7 @@ func (s *NewEVMConfigTestSuite) Test_ValidConfigWithCustomTxParams() { Id: id, }, Bridge: "bridgeAddress", + Retry: "retryAddress", FrostKeygen: "frostKeygen", Handlers: []evm.HandlerConfig{ { diff --git a/chains/evm/executor/message-handler.go b/chains/evm/executor/message-handler.go index 7c3962ce..496c6768 100644 --- a/chains/evm/executor/message-handler.go +++ b/chains/evm/executor/message-handler.go @@ -15,7 +15,9 @@ import ( "github.com/sygmaprotocol/sygma-core/relayer/proposal" "github.com/ChainSafe/sygma-relayer/chains/evm/listener/depositHandlers" + "github.com/ChainSafe/sygma-relayer/store" + "github.com/ChainSafe/sygma-relayer/relayer/retry" "github.com/ChainSafe/sygma-relayer/relayer/transfer" ) @@ -215,3 +217,69 @@ func GenericMessageHandler(msg *transfer.TransferMessage) (*proposal.Proposal, e Data: data.Bytes(), }, msg.ID, transfer.TransferProposalType), nil } + +type BlockFetcher interface { + LatestBlock() (*big.Int, error) +} + +type PropStorer interface { + StorePropStatus(source, destination uint8, depositNonce uint64, status store.PropStatus) error + PropStatus(source, destination uint8, depositNonce uint64) (store.PropStatus, error) +} + +type DepositProcessor interface { + ProcessDeposits(startBlock *big.Int, endBlock *big.Int) (map[uint8][]*message.Message, error) +} + +type RetryMessageHandler struct { + depositProcessor DepositProcessor + blockConfirmations *big.Int + blockFetcher BlockFetcher + propStorer PropStorer + msgChan chan []*message.Message +} + +func NewRetryMessageHandler( + depositProcessor DepositProcessor, + blockFetcher BlockFetcher, + propStorer PropStorer, + blockConfirmations *big.Int, + msgChan chan []*message.Message) *RetryMessageHandler { + return &RetryMessageHandler{ + depositProcessor: depositProcessor, + blockFetcher: blockFetcher, + propStorer: propStorer, + blockConfirmations: blockConfirmations, + msgChan: msgChan, + } +} + +func (h *RetryMessageHandler) HandleMessage(msg *message.Message) (*proposal.Proposal, error) { + retryData := msg.Data.(retry.RetryMessageData) + latestBlock, err := h.blockFetcher.LatestBlock() + if err != nil { + return nil, err + } + if latestBlock.Cmp(new(big.Int).Add(retryData.BlockHeight, h.blockConfirmations)) != 1 { + return nil, fmt.Errorf( + "latest block %s higher than receipt block number + block confirmations %s", + latestBlock, + new(big.Int).Add(retryData.BlockHeight, h.blockConfirmations), + ) + } + + domainDeposits, err := h.depositProcessor.ProcessDeposits(retryData.BlockHeight, retryData.BlockHeight) + if err != nil { + return nil, err + } + filteredDeposits, err := retry.FilterDeposits(h.propStorer, domainDeposits, retryData.ResourceID, retryData.DestinationDomainID) + if err != nil { + return nil, err + } + if len(filteredDeposits) == 0 { + return nil, nil + } + + h.msgChan <- filteredDeposits + return nil, nil +} diff --git a/chains/evm/executor/message-handler_test.go b/chains/evm/executor/message-handler_test.go index 02b30d98..db012ecd 100644 --- a/chains/evm/executor/message-handler_test.go +++ b/chains/evm/executor/message-handler_test.go @@ -9,12 +9,16 @@ import ( "math/big" "testing" + mock_executor "github.com/ChainSafe/sygma-relayer/chains/evm/executor/mock" + "github.com/ChainSafe/sygma-relayer/store" + "github.com/golang/mock/gomock" "github.com/sygmaprotocol/sygma-core/relayer/message" "github.com/sygmaprotocol/sygma-core/relayer/proposal" "github.com/ChainSafe/sygma-relayer/chains/evm/calls/events" "github.com/ChainSafe/sygma-relayer/chains/evm/executor" "github.com/ChainSafe/sygma-relayer/e2e/evm" + "github.com/ChainSafe/sygma-relayer/relayer/retry" "github.com/ChainSafe/sygma-relayer/relayer/transfer" "github.com/ethereum/go-ethereum/common" "github.com/stretchr/testify/suite" @@ -676,3 +680,147 @@ func (s *Erc1155HandlerTestSuite) Test_HandleErc1155Message_InvalidTransferData( s.Nil(prop) s.EqualError(err, errIncorrectTransferData.Error()) } + +type RetryMessageHandlerTestSuite struct { + suite.Suite + + messageHandler *executor.RetryMessageHandler + mockBlockFetcher *mock_executor.MockBlockFetcher + mockDepositProcessor *mock_executor.MockDepositProcessor + mockPropStorer *mock_executor.MockPropStorer + msgChan chan []*message.Message +} + +func TestRunRetryMessageHandlerTestSuite(t *testing.T) { + suite.Run(t, new(RetryMessageHandlerTestSuite)) +} + +func (s *RetryMessageHandlerTestSuite) SetupTest() { + ctrl := gomock.NewController(s.T()) + s.mockBlockFetcher = mock_executor.NewMockBlockFetcher(ctrl) + s.mockDepositProcessor = mock_executor.NewMockDepositProcessor(ctrl) + s.mockPropStorer = mock_executor.NewMockPropStorer(ctrl) + s.msgChan = make(chan []*message.Message, 1) + s.messageHandler = executor.NewRetryMessageHandler( + s.mockDepositProcessor, + s.mockBlockFetcher, + s.mockPropStorer, + big.NewInt(5), + s.msgChan) +} + +func (s *RetryMessageHandlerTestSuite) Test_HandleMessage_RetryTooNew() { + s.mockBlockFetcher.EXPECT().LatestBlock().Return(big.NewInt(105), nil) + + message := &message.Message{ + Source: 1, + Destination: 3, + Data: retry.RetryMessageData{ + SourceDomainID: 3, + DestinationDomainID: 4, + BlockHeight: big.NewInt(100), + ResourceID: [32]byte{}, + }, + Type: transfer.TransferMessageType, + } + + prop, err := s.messageHandler.HandleMessage(message) + + s.Nil(prop) + s.NotNil(err) +} + +func (s *RetryMessageHandlerTestSuite) Test_HandleMessage_NoDeposits() { + s.mockBlockFetcher.EXPECT().LatestBlock().Return(big.NewInt(106), nil) + s.mockDepositProcessor.EXPECT().ProcessDeposits(big.NewInt(100), big.NewInt(100)).Return(make(map[uint8][]*message.Message), nil) + + message := &message.Message{ + Source: 1, + Destination: 3, + Data: retry.RetryMessageData{ + SourceDomainID: 3, + DestinationDomainID: 4, + BlockHeight: big.NewInt(100), + ResourceID: [32]byte{}, + }, + Type: transfer.TransferMessageType, + } + + prop, err := s.messageHandler.HandleMessage(message) + + s.Nil(prop) + s.Nil(err) + s.Equal(len(s.msgChan), 0) +} + +func (s *RetryMessageHandlerTestSuite) Test_HandleMessage_ValidDeposits() { + s.mockBlockFetcher.EXPECT().LatestBlock().Return(big.NewInt(106), nil) + + validResource := evm.SliceTo32Bytes(common.LeftPadBytes([]byte{3}, 31)) + invalidResource := evm.SliceTo32Bytes(common.LeftPadBytes([]byte{4}, 31)) + invalidDomain := uint8(3) + validDomain := uint8(4) + + executedNonce := uint64(1) + failedNonce := uint64(3) + + deposits := make(map[uint8][]*message.Message) + deposits[invalidDomain] = []*message.Message{ + { + Destination: invalidDomain, + Data: transfer.TransferMessageData{ + DepositNonce: 1, + ResourceId: validResource, + }, + }, + } + deposits[validDomain] = []*message.Message{ + { + Source: invalidDomain, + Destination: validDomain, + Data: transfer.TransferMessageData{ + DepositNonce: executedNonce, + ResourceId: validResource, + }, + }, + { + Source: invalidDomain, + Destination: validDomain, + Data: transfer.TransferMessageData{ + DepositNonce: 2, + ResourceId: invalidResource, + }, + }, + { + Source: invalidDomain, + Destination: validDomain, + Data: transfer.TransferMessageData{ + DepositNonce: failedNonce, + ResourceId: validResource, + }, + }, + } + s.mockDepositProcessor.EXPECT().ProcessDeposits(big.NewInt(100), big.NewInt(100)).Return(deposits, nil) + s.mockPropStorer.EXPECT().PropStatus(invalidDomain, validDomain, executedNonce).Return(store.ExecutedProp, nil) + s.mockPropStorer.EXPECT().PropStatus(invalidDomain, validDomain, failedNonce).Return(store.FailedProp, nil) + + message := &message.Message{ + Source: 1, + Destination: 3, + Data: retry.RetryMessageData{ + SourceDomainID: invalidDomain, + DestinationDomainID: validDomain, + BlockHeight: big.NewInt(100), + ResourceID: validResource, + }, + Type: transfer.TransferMessageType, + } + + prop, err := s.messageHandler.HandleMessage(message) + + s.Nil(prop) + s.Nil(err) + msgs := <-s.msgChan + s.Equal(msgs[0].Data.(transfer.TransferMessageData).DepositNonce, failedNonce) + s.Equal(msgs[0].Destination, validDomain) +} diff --git a/chains/evm/executor/mock/message-handler.go b/chains/evm/executor/mock/message-handler.go new file mode 100644 index 00000000..d2eb1412 --- /dev/null +++ b/chains/evm/executor/mock/message-handler.go @@ -0,0 +1,142 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: ./chains/evm/executor/message-handler.go + +// Package mock_executor is a generated GoMock package. +package mock_executor + +import ( + big "math/big" + reflect "reflect" + + store "github.com/ChainSafe/sygma-relayer/store" + gomock "github.com/golang/mock/gomock" + message "github.com/sygmaprotocol/sygma-core/relayer/message" +) + +// MockBlockFetcher is a mock of BlockFetcher interface. +type MockBlockFetcher struct { + ctrl *gomock.Controller + recorder *MockBlockFetcherMockRecorder +} + +// MockBlockFetcherMockRecorder is the mock recorder for MockBlockFetcher. +type MockBlockFetcherMockRecorder struct { + mock *MockBlockFetcher +} + +// NewMockBlockFetcher creates a new mock instance. +func NewMockBlockFetcher(ctrl *gomock.Controller) *MockBlockFetcher { + mock := &MockBlockFetcher{ctrl: ctrl} + mock.recorder = &MockBlockFetcherMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockBlockFetcher) EXPECT() *MockBlockFetcherMockRecorder { + return m.recorder +} + +// LatestBlock mocks base method. +func (m *MockBlockFetcher) LatestBlock() (*big.Int, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "LatestBlock") + ret0, _ := ret[0].(*big.Int) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// LatestBlock indicates an expected call of LatestBlock. +func (mr *MockBlockFetcherMockRecorder) LatestBlock() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LatestBlock", reflect.TypeOf((*MockBlockFetcher)(nil).LatestBlock)) +} + +// MockPropStorer is a mock of PropStorer interface. +type MockPropStorer struct { + ctrl *gomock.Controller + recorder *MockPropStorerMockRecorder +} + +// MockPropStorerMockRecorder is the mock recorder for MockPropStorer. +type MockPropStorerMockRecorder struct { + mock *MockPropStorer +} + +// NewMockPropStorer creates a new mock instance. +func NewMockPropStorer(ctrl *gomock.Controller) *MockPropStorer { + mock := &MockPropStorer{ctrl: ctrl} + mock.recorder = &MockPropStorerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockPropStorer) EXPECT() *MockPropStorerMockRecorder { + return m.recorder +} + +// PropStatus mocks base method. +func (m *MockPropStorer) PropStatus(source, destination uint8, depositNonce uint64) (store.PropStatus, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PropStatus", source, destination, depositNonce) + ret0, _ := ret[0].(store.PropStatus) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// PropStatus indicates an expected call of PropStatus. +func (mr *MockPropStorerMockRecorder) PropStatus(source, destination, depositNonce interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PropStatus", reflect.TypeOf((*MockPropStorer)(nil).PropStatus), source, destination, depositNonce) +} + +// StorePropStatus mocks base method. +func (m *MockPropStorer) StorePropStatus(source, destination uint8, depositNonce uint64, status store.PropStatus) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "StorePropStatus", source, destination, depositNonce, status) + ret0, _ := ret[0].(error) + return ret0 +} + +// StorePropStatus indicates an expected call of StorePropStatus. +func (mr *MockPropStorerMockRecorder) StorePropStatus(source, destination, depositNonce, status interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StorePropStatus", reflect.TypeOf((*MockPropStorer)(nil).StorePropStatus), source, destination, depositNonce, status) +} + +// MockDepositProcessor is a mock of DepositProcessor interface. +type MockDepositProcessor struct { + ctrl *gomock.Controller + recorder *MockDepositProcessorMockRecorder +} + +// MockDepositProcessorMockRecorder is the mock recorder for MockDepositProcessor. +type MockDepositProcessorMockRecorder struct { + mock *MockDepositProcessor +} + +// NewMockDepositProcessor creates a new mock instance. +func NewMockDepositProcessor(ctrl *gomock.Controller) *MockDepositProcessor { + mock := &MockDepositProcessor{ctrl: ctrl} + mock.recorder = &MockDepositProcessorMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockDepositProcessor) EXPECT() *MockDepositProcessorMockRecorder { + return m.recorder +} + +// ProcessDeposits mocks base method. +func (m *MockDepositProcessor) ProcessDeposits(startBlock, endBlock *big.Int) (map[uint8][]*message.Message, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ProcessDeposits", startBlock, endBlock) + ret0, _ := ret[0].(map[uint8][]*message.Message) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ProcessDeposits indicates an expected call of ProcessDeposits. +func (mr *MockDepositProcessorMockRecorder) ProcessDeposits(startBlock, endBlock interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ProcessDeposits", reflect.TypeOf((*MockDepositProcessor)(nil).ProcessDeposits), startBlock, endBlock) +} diff --git a/chains/evm/listener/eventHandlers/deposit.go b/chains/evm/listener/eventHandlers/deposit.go new file mode 100644 index 00000000..67dabecd --- /dev/null +++ b/chains/evm/listener/eventHandlers/deposit.go @@ -0,0 +1,91 @@ +package eventHandlers + +import ( + "context" + "fmt" + "math/big" + "time" + + "github.com/ChainSafe/sygma-relayer/chains/evm/calls/events" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/rs/zerolog/log" + "github.com/sygmaprotocol/sygma-core/relayer/message" +) + +type EventListener interface { + FetchKeygenEvents(ctx context.Context, address common.Address, startBlock *big.Int, endBlock *big.Int) ([]types.Log, error) + FetchFrostKeygenEvents(ctx context.Context, address common.Address, startBlock *big.Int, endBlock *big.Int) ([]types.Log, error) + FetchRefreshEvents(ctx context.Context, address common.Address, startBlock *big.Int, endBlock *big.Int) ([]*events.Refresh, error) + FetchDeposits(ctx context.Context, address common.Address, startBlock *big.Int, endBlock *big.Int) ([]*events.Deposit, error) + FetchRetryV1Events(ctx context.Context, contractAddress common.Address, startBlock *big.Int, endBlock *big.Int) ([]events.RetryV1Event, error) + FetchRetryV2Events(ctx context.Context, contractAddress common.Address, startBlock *big.Int, endBlock *big.Int) ([]events.RetryV2Event, error) + FetchRetryDepositEvents(event events.RetryV1Event, bridgeAddress common.Address, blockConfirmations *big.Int) ([]events.Deposit, error) +} + +type DepositHandler interface { + HandleDeposit(sourceID, destID uint8, nonce uint64, resourceID [32]byte, calldata, handlerResponse []byte, messageID string, timestamp time.Time) (*message.Message, error) +} + +type DepositEventHandler struct { + eventListener EventListener + depositHandler DepositHandler + bridgeAddress common.Address + domainID uint8 + msgChan chan []*message.Message +} + +func NewDepositEventHandler(eventListener EventListener, depositHandler DepositHandler, bridgeAddress common.Address, domainID uint8, msgChan chan []*message.Message) *DepositEventHandler { + return &DepositEventHandler{ + eventListener: eventListener, + depositHandler: depositHandler, + bridgeAddress: bridgeAddress, + domainID: domainID, + msgChan: msgChan, + } +} + +func (eh *DepositEventHandler) HandleEvents(startBlock *big.Int, endBlock *big.Int) error { + domainDeposits, err := eh.ProcessDeposits(startBlock, endBlock) + if err != nil { + return err + } + + for _, deposits := range domainDeposits { + go func(d []*message.Message) { + eh.msgChan <- d + }(deposits) + } + + return nil +} + +func (eh *DepositEventHandler) ProcessDeposits(startBlock *big.Int, endBlock *big.Int) (map[uint8][]*message.Message, error) { + deposits, err := eh.eventListener.FetchDeposits(context.Background(), eh.bridgeAddress, startBlock, endBlock) + if err != nil { + return nil, fmt.Errorf("unable to fetch deposit events because of: %+v", err) + } + + domainDeposits := make(map[uint8][]*message.Message) + for _, d := range deposits { + func(d *events.Deposit) { + defer func() { + if r := recover(); r != nil { + log.Error().Err(err).Msgf("panic occured while handling deposit %+v", d) + } + }() + + messageID := fmt.Sprintf("%d-%d-%d-%d", eh.domainID, d.DestinationDomainID, startBlock, endBlock) + m, err := eh.depositHandler.HandleDeposit(eh.domainID, d.DestinationDomainID, d.DepositNonce, d.ResourceID, d.Data, d.HandlerResponse, messageID, d.Timestamp) + if err != nil { + log.Error().Err(err).Str("start block", startBlock.String()).Str("end block", endBlock.String()).Uint8("domainID", eh.domainID).Msgf("%v", err) + return + } + + log.Info().Str("messageID", m.ID).Msgf("Resolved message %+v in block range: %s-%s", m, startBlock.String(), endBlock.String()) + domainDeposits[m.Destination] = append(domainDeposits[m.Destination], m) + }(d) + } + + return domainDeposits, nil +} diff --git a/chains/evm/listener/eventHandlers/deposit_test.go b/chains/evm/listener/eventHandlers/deposit_test.go new file mode 100644 index 00000000..bd1946fd --- /dev/null +++ b/chains/evm/listener/eventHandlers/deposit_test.go @@ -0,0 +1,211 @@ +// The Licensed Work is (c) 2022 Sygma +// SPDX-License-Identifier: LGPL-3.0-only + +package eventHandlers_test + +import ( + "fmt" + "math/big" + "testing" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/suite" + + "github.com/ChainSafe/sygma-relayer/chains/evm/calls/events" + "github.com/ChainSafe/sygma-relayer/chains/evm/listener/eventHandlers" + mock_listener "github.com/ChainSafe/sygma-relayer/chains/evm/listener/eventHandlers/mock" + "github.com/ChainSafe/sygma-relayer/relayer/transfer" + "github.com/sygmaprotocol/sygma-core/relayer/message" +) + +type DepositHandlerTestSuite struct { + suite.Suite + depositEventHandler *eventHandlers.DepositEventHandler + mockDepositHandler *mock_listener.MockDepositHandler + mockEventListener *mock_listener.MockEventListener + domainID uint8 + msgChan chan []*message.Message +} + +func TestRunDepositHandlerTestSuite(t *testing.T) { + suite.Run(t, new(DepositHandlerTestSuite)) +} + +func (s *DepositHandlerTestSuite) SetupTest() { + ctrl := gomock.NewController(s.T()) + s.domainID = 1 + s.mockEventListener = mock_listener.NewMockEventListener(ctrl) + s.mockDepositHandler = mock_listener.NewMockDepositHandler(ctrl) + s.msgChan = make(chan []*message.Message, 2) + s.depositEventHandler = eventHandlers.NewDepositEventHandler(s.mockEventListener, s.mockDepositHandler, common.Address{}, s.domainID, s.msgChan) +} + +func (s *DepositHandlerTestSuite) Test_FetchDepositFails() { + s.mockEventListener.EXPECT().FetchDeposits(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return([]*events.Deposit{}, fmt.Errorf("error")) + + err := s.depositEventHandler.HandleEvents(big.NewInt(0), big.NewInt(5)) + + s.NotNil(err) + s.Equal(len(s.msgChan), 0) +} + +func (s *DepositHandlerTestSuite) Test_HandleDepositFails_ExecutionContinue() { + timestamp := time.Now() + d1 := &events.Deposit{ + DepositNonce: 1, + DestinationDomainID: 2, + ResourceID: [32]byte{}, + HandlerResponse: []byte{}, + Data: []byte{}, + Timestamp: timestamp, + } + d2 := &events.Deposit{ + DepositNonce: 2, + DestinationDomainID: 2, + ResourceID: [32]byte{}, + HandlerResponse: []byte{}, + Data: []byte{}, + Timestamp: timestamp, + } + deposits := []*events.Deposit{d1, d2} + s.mockEventListener.EXPECT().FetchDeposits(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(deposits, nil) + msgID := fmt.Sprintf("%d-%d-%d-%d", 1, 2, 0, 5) + s.mockDepositHandler.EXPECT().HandleDeposit( + s.domainID, + d1.DestinationDomainID, + d1.DepositNonce, + d1.ResourceID, + d1.Data, + d1.HandlerResponse, + msgID, + d1.Timestamp, + ).Return(&message.Message{}, fmt.Errorf("error")) + s.mockDepositHandler.EXPECT().HandleDeposit( + s.domainID, + d2.DestinationDomainID, + d2.DepositNonce, + d2.ResourceID, + d2.Data, + d2.HandlerResponse, + msgID, + d2.Timestamp, + ).Return( + &message.Message{ + Data: transfer.TransferMessageData{ + DepositNonce: 2, + }, + }, + nil, + ) + + err := s.depositEventHandler.HandleEvents(big.NewInt(0), big.NewInt(5)) + msgs := <-s.msgChan + + s.Nil(err) + s.Equal(msgs, []*message.Message{{Data: transfer.TransferMessageData{DepositNonce: 2}}}) +} + +func (s *DepositHandlerTestSuite) Test_HandleDepositPanis_ExecutionContinues() { + d1 := &events.Deposit{ + DepositNonce: 1, + DestinationDomainID: 2, + ResourceID: [32]byte{}, + HandlerResponse: []byte{}, + Data: []byte{}, + } + d2 := &events.Deposit{ + DepositNonce: 2, + DestinationDomainID: 2, + ResourceID: [32]byte{}, + HandlerResponse: []byte{}, + Data: []byte{}, + } + deposits := []*events.Deposit{d1, d2} + s.mockEventListener.EXPECT().FetchDeposits(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(deposits, nil) + msgID := fmt.Sprintf("%d-%d-%d-%d", 1, 2, 0, 5) + s.mockDepositHandler.EXPECT().HandleDeposit( + s.domainID, + d1.DestinationDomainID, + d1.DepositNonce, + d1.ResourceID, + d1.Data, + d1.HandlerResponse, + msgID, + gomock.Any(), + ).Do(func(sourceID, destID, nonce, resourceID, calldata, handlerResponse, msgID, timestamp interface{}) { + panic("error") + }) + s.mockDepositHandler.EXPECT().HandleDeposit( + s.domainID, + d2.DestinationDomainID, + d2.DepositNonce, + d2.ResourceID, + d2.Data, + d2.HandlerResponse, + msgID, + gomock.Any(), + ).Return( + &message.Message{Data: transfer.TransferMessageData{DepositNonce: 2}}, + nil, + ) + + err := s.depositEventHandler.HandleEvents(big.NewInt(0), big.NewInt(5)) + msgs := <-s.msgChan + + s.Nil(err) + s.Equal(msgs, []*message.Message{{Data: transfer.TransferMessageData{DepositNonce: 2}}}) +} + +func (s *DepositHandlerTestSuite) Test_SuccessfulHandleDeposit() { + d1 := &events.Deposit{ + DepositNonce: 1, + DestinationDomainID: 2, + ResourceID: [32]byte{}, + HandlerResponse: []byte{}, + Data: []byte{}, + } + d2 := &events.Deposit{ + DepositNonce: 2, + DestinationDomainID: 2, + ResourceID: [32]byte{}, + HandlerResponse: []byte{}, + Data: []byte{}, + } + deposits := []*events.Deposit{d1, d2} + s.mockEventListener.EXPECT().FetchDeposits(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(deposits, nil) + msgID := fmt.Sprintf("%d-%d-%d-%d", 1, 2, 0, 5) + s.mockDepositHandler.EXPECT().HandleDeposit( + s.domainID, + d1.DestinationDomainID, + d1.DepositNonce, + d1.ResourceID, + d1.Data, + d1.HandlerResponse, + msgID, + gomock.Any(), + ).Return( + &message.Message{Data: transfer.TransferMessageData{DepositNonce: 1}}, + nil, + ) + s.mockDepositHandler.EXPECT().HandleDeposit( + s.domainID, + d2.DestinationDomainID, + d2.DepositNonce, + d2.ResourceID, + d2.Data, + d2.HandlerResponse, + msgID, + gomock.Any(), + ).Return( + &message.Message{Data: transfer.TransferMessageData{DepositNonce: 2}}, + nil, + ) + + err := s.depositEventHandler.HandleEvents(big.NewInt(0), big.NewInt(5)) + msgs := <-s.msgChan + + s.Nil(err) + s.Equal(msgs, []*message.Message{{Data: transfer.TransferMessageData{DepositNonce: 1}}, {Data: transfer.TransferMessageData{DepositNonce: 2}}}) +} diff --git a/chains/evm/listener/eventHandlers/mock/listener.go b/chains/evm/listener/eventHandlers/mock/listener.go index b71c766a..b2c41951 100644 --- a/chains/evm/listener/eventHandlers/mock/listener.go +++ b/chains/evm/listener/eventHandlers/mock/listener.go @@ -1,5 +1,5 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: ./chains/evm/listener/eventHandlers/event-handler.go +// Source: ./chains/evm/listener/eventHandlers/deposit.go // Package mock_eventHandlers is a generated GoMock package. package mock_eventHandlers @@ -11,7 +11,6 @@ import ( time "time" events "github.com/ChainSafe/sygma-relayer/chains/evm/calls/events" - store "github.com/ChainSafe/sygma-relayer/store" common "github.com/ethereum/go-ethereum/common" types "github.com/ethereum/go-ethereum/core/types" gomock "github.com/golang/mock/gomock" @@ -102,7 +101,7 @@ func (mr *MockEventListenerMockRecorder) FetchRefreshEvents(ctx, address, startB } // FetchRetryDepositEvents mocks base method. -func (m *MockEventListener) FetchRetryDepositEvents(event events.RetryEvent, bridgeAddress common.Address, blockConfirmations *big.Int) ([]events.Deposit, error) { +func (m *MockEventListener) FetchRetryDepositEvents(event events.RetryV1Event, bridgeAddress common.Address, blockConfirmations *big.Int) ([]events.Deposit, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "FetchRetryDepositEvents", event, bridgeAddress, blockConfirmations) ret0, _ := ret[0].([]events.Deposit) @@ -116,71 +115,34 @@ func (mr *MockEventListenerMockRecorder) FetchRetryDepositEvents(event, bridgeAd return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FetchRetryDepositEvents", reflect.TypeOf((*MockEventListener)(nil).FetchRetryDepositEvents), event, bridgeAddress, blockConfirmations) } -// FetchRetryEvents mocks base method. -func (m *MockEventListener) FetchRetryEvents(ctx context.Context, contractAddress common.Address, startBlock, endBlock *big.Int) ([]events.RetryEvent, error) { +// FetchRetryV1Events mocks base method. +func (m *MockEventListener) FetchRetryV1Events(ctx context.Context, contractAddress common.Address, startBlock, endBlock *big.Int) ([]events.RetryV1Event, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "FetchRetryEvents", ctx, contractAddress, startBlock, endBlock) - ret0, _ := ret[0].([]events.RetryEvent) + ret := m.ctrl.Call(m, "FetchRetryV1Events", ctx, contractAddress, startBlock, endBlock) + ret0, _ := ret[0].([]events.RetryV1Event) ret1, _ := ret[1].(error) return ret0, ret1 } -// FetchRetryEvents indicates an expected call of FetchRetryEvents. -func (mr *MockEventListenerMockRecorder) FetchRetryEvents(ctx, contractAddress, startBlock, endBlock interface{}) *gomock.Call { +// FetchRetryV1Events indicates an expected call of FetchRetryV1Events. +func (mr *MockEventListenerMockRecorder) FetchRetryV1Events(ctx, contractAddress, startBlock, endBlock interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FetchRetryEvents", reflect.TypeOf((*MockEventListener)(nil).FetchRetryEvents), ctx, contractAddress, startBlock, endBlock) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FetchRetryV1Events", reflect.TypeOf((*MockEventListener)(nil).FetchRetryV1Events), ctx, contractAddress, startBlock, endBlock) } -// MockPropStorer is a mock of PropStorer interface. -type MockPropStorer struct { - ctrl *gomock.Controller - recorder *MockPropStorerMockRecorder -} - -// MockPropStorerMockRecorder is the mock recorder for MockPropStorer. -type MockPropStorerMockRecorder struct { - mock *MockPropStorer -} - -// NewMockPropStorer creates a new mock instance. -func NewMockPropStorer(ctrl *gomock.Controller) *MockPropStorer { - mock := &MockPropStorer{ctrl: ctrl} - mock.recorder = &MockPropStorerMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockPropStorer) EXPECT() *MockPropStorerMockRecorder { - return m.recorder -} - -// PropStatus mocks base method. -func (m *MockPropStorer) PropStatus(source, destination uint8, depositNonce uint64) (store.PropStatus, error) { +// FetchRetryV2Events mocks base method. +func (m *MockEventListener) FetchRetryV2Events(ctx context.Context, contractAddress common.Address, startBlock, endBlock *big.Int) ([]events.RetryV2Event, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "PropStatus", source, destination, depositNonce) - ret0, _ := ret[0].(store.PropStatus) + ret := m.ctrl.Call(m, "FetchRetryV2Events", ctx, contractAddress, startBlock, endBlock) + ret0, _ := ret[0].([]events.RetryV2Event) ret1, _ := ret[1].(error) return ret0, ret1 } -// PropStatus indicates an expected call of PropStatus. -func (mr *MockPropStorerMockRecorder) PropStatus(source, destination, depositNonce interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PropStatus", reflect.TypeOf((*MockPropStorer)(nil).PropStatus), source, destination, depositNonce) -} - -// StorePropStatus mocks base method. -func (m *MockPropStorer) StorePropStatus(source, destination uint8, depositNonce uint64, status store.PropStatus) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "StorePropStatus", source, destination, depositNonce, status) - ret0, _ := ret[0].(error) - return ret0 -} - -// StorePropStatus indicates an expected call of StorePropStatus. -func (mr *MockPropStorerMockRecorder) StorePropStatus(source, destination, depositNonce, status interface{}) *gomock.Call { +// FetchRetryV2Events indicates an expected call of FetchRetryV2Events. +func (mr *MockEventListenerMockRecorder) FetchRetryV2Events(ctx, contractAddress, startBlock, endBlock interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StorePropStatus", reflect.TypeOf((*MockPropStorer)(nil).StorePropStatus), source, destination, depositNonce, status) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FetchRetryV2Events", reflect.TypeOf((*MockEventListener)(nil).FetchRetryV2Events), ctx, contractAddress, startBlock, endBlock) } // MockDepositHandler is a mock of DepositHandler interface. diff --git a/chains/evm/listener/eventHandlers/mock/retry.go b/chains/evm/listener/eventHandlers/mock/retry.go new file mode 100644 index 00000000..8773760d --- /dev/null +++ b/chains/evm/listener/eventHandlers/mock/retry.go @@ -0,0 +1,64 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: ./chains/evm/listener/eventHandlers/retry.go + +// Package mock_eventHandlers is a generated GoMock package. +package mock_eventHandlers + +import ( + reflect "reflect" + + store "github.com/ChainSafe/sygma-relayer/store" + gomock "github.com/golang/mock/gomock" +) + +// MockPropStorer is a mock of PropStorer interface. +type MockPropStorer struct { + ctrl *gomock.Controller + recorder *MockPropStorerMockRecorder +} + +// MockPropStorerMockRecorder is the mock recorder for MockPropStorer. +type MockPropStorerMockRecorder struct { + mock *MockPropStorer +} + +// NewMockPropStorer creates a new mock instance. +func NewMockPropStorer(ctrl *gomock.Controller) *MockPropStorer { + mock := &MockPropStorer{ctrl: ctrl} + mock.recorder = &MockPropStorerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockPropStorer) EXPECT() *MockPropStorerMockRecorder { + return m.recorder +} + +// PropStatus mocks base method. +func (m *MockPropStorer) PropStatus(source, destination uint8, depositNonce uint64) (store.PropStatus, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PropStatus", source, destination, depositNonce) + ret0, _ := ret[0].(store.PropStatus) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// PropStatus indicates an expected call of PropStatus. +func (mr *MockPropStorerMockRecorder) PropStatus(source, destination, depositNonce interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PropStatus", reflect.TypeOf((*MockPropStorer)(nil).PropStatus), source, destination, depositNonce) +} + +// StorePropStatus mocks base method. +func (m *MockPropStorer) StorePropStatus(source, destination uint8, depositNonce uint64, status store.PropStatus) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "StorePropStatus", source, destination, depositNonce, status) + ret0, _ := ret[0].(error) + return ret0 +} + +// StorePropStatus indicates an expected call of StorePropStatus. +func (mr *MockPropStorerMockRecorder) StorePropStatus(source, destination, depositNonce, status interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StorePropStatus", reflect.TypeOf((*MockPropStorer)(nil).StorePropStatus), source, destination, depositNonce, status) +} diff --git a/chains/evm/listener/eventHandlers/retry.go b/chains/evm/listener/eventHandlers/retry.go new file mode 100644 index 00000000..11b7ba3c --- /dev/null +++ b/chains/evm/listener/eventHandlers/retry.go @@ -0,0 +1,201 @@ +package eventHandlers + +import ( + "context" + "fmt" + "math/big" + "strings" + "time" + + "github.com/ChainSafe/sygma-relayer/chains/evm/calls/consts" + "github.com/ChainSafe/sygma-relayer/chains/evm/calls/events" + "github.com/ChainSafe/sygma-relayer/relayer/retry" + "github.com/ChainSafe/sygma-relayer/relayer/transfer" + "github.com/ChainSafe/sygma-relayer/store" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + "github.com/rs/zerolog" + "github.com/sygmaprotocol/sygma-core/relayer/message" +) + +type RetryV2EventHandler struct { + log zerolog.Logger + eventListener EventListener + retryAddress common.Address + domainID uint8 + msgChan chan []*message.Message +} + +func NewRetryV2EventHandler( + logC zerolog.Context, + eventListener EventListener, + retryAddress common.Address, + domainID uint8, + msgChan chan []*message.Message, +) *RetryV2EventHandler { + return &RetryV2EventHandler{ + log: logC.Logger(), + eventListener: eventListener, + retryAddress: retryAddress, + domainID: domainID, + msgChan: msgChan, + } +} + +func (eh *RetryV2EventHandler) HandleEvents( + startBlock *big.Int, + endBlock *big.Int, +) error { + retryEvents, err := eh.eventListener.FetchRetryV2Events(context.Background(), eh.retryAddress, startBlock, endBlock) + if err != nil { + return fmt.Errorf("unable to fetch retry v2 events because of: %+v", err) + } + + for _, e := range retryEvents { + messageID := fmt.Sprintf("retry-%d-%d", e.SourceDomainID, e.DestinationDomainID) + msg := message.NewMessage( + eh.domainID, + e.SourceDomainID, + retry.RetryMessageData{ + SourceDomainID: e.SourceDomainID, + DestinationDomainID: e.DestinationDomainID, + BlockHeight: e.BlockHeight, + ResourceID: e.ResourceID, + }, + messageID, + retry.RetryMessageType, + time.Now(), + ) + + eh.log.Info().Str("messageID", messageID).Msgf( + "Resolved retry message %+v in block range: %s-%s", msg, startBlock.String(), endBlock.String(), + ) + go func() { eh.msgChan <- []*message.Message{msg} }() + } + return nil +} + +type PropStorer interface { + StorePropStatus(source, destination uint8, depositNonce uint64, status store.PropStatus) error + PropStatus(source, destination uint8, depositNonce uint64) (store.PropStatus, error) +} + +type RetryV1EventHandler struct { + log zerolog.Logger + eventListener EventListener + depositHandler DepositHandler + propStorer PropStorer + bridgeAddress common.Address + bridgeABI abi.ABI + domainID uint8 + blockConfirmations *big.Int + msgChan chan []*message.Message +} + +func NewRetryV1EventHandler( + logC zerolog.Context, + eventListener EventListener, + depositHandler DepositHandler, + propStorer PropStorer, + bridgeAddress common.Address, + domainID uint8, + blockConfirmations *big.Int, + msgChan chan []*message.Message, +) *RetryV1EventHandler { + bridgeABI, _ := abi.JSON(strings.NewReader(consts.BridgeABI)) + return &RetryV1EventHandler{ + log: logC.Logger(), + eventListener: eventListener, + depositHandler: depositHandler, + propStorer: propStorer, + bridgeAddress: bridgeAddress, + bridgeABI: bridgeABI, + domainID: domainID, + blockConfirmations: blockConfirmations, + msgChan: msgChan, + } +} + +func (eh *RetryV1EventHandler) HandleEvents( + startBlock *big.Int, + endBlock *big.Int, +) error { + retryEvents, err := eh.eventListener.FetchRetryV1Events(context.Background(), eh.bridgeAddress, startBlock, endBlock) + if err != nil { + return fmt.Errorf("unable to fetch retry events because of: %+v", err) + } + + retriesByDomain := make(map[uint8][]*message.Message) + for _, event := range retryEvents { + func(event events.RetryV1Event) { + defer func() { + if r := recover(); r != nil { + eh.log.Error().Err(err).Msgf("panic occured while handling retry event %+v", event) + } + }() + + deposits, err := eh.eventListener.FetchRetryDepositEvents(event, eh.bridgeAddress, eh.blockConfirmations) + if err != nil { + eh.log.Error().Err(err).Msgf("Unable to fetch deposit events from event %+v", event) + return + } + + for _, d := range deposits { + messageID := fmt.Sprintf("retry-%d-%d-%d-%d", eh.domainID, d.DestinationDomainID, startBlock, endBlock) + msg, err := eh.depositHandler.HandleDeposit( + eh.domainID, d.DestinationDomainID, d.DepositNonce, + d.ResourceID, d.Data, d.HandlerResponse, messageID, d.Timestamp, + ) + if err != nil { + eh.log.Err(err).Str("messageID", msg.ID).Msgf("Failed handling deposit %+v", d) + continue + } + isExecuted, err := eh.isExecuted(msg) + if err != nil { + eh.log.Err(err).Str("messageID", msg.ID).Msgf("Failed checking if deposit executed %+v", d) + continue + } + if isExecuted { + eh.log.Debug().Str("messageID", msg.ID).Msgf("Deposit marked as executed %+v", d) + continue + } + + eh.log.Info().Str("messageID", msg.ID).Msgf( + "Resolved retry message %+v in block range: %s-%s", msg, startBlock.String(), endBlock.String(), + ) + retriesByDomain[msg.Destination] = append(retriesByDomain[msg.Destination], msg) + } + }(event) + } + + for _, retries := range retriesByDomain { + eh.msgChan <- retries + } + + return nil +} + +func (eh *RetryV1EventHandler) isExecuted(msg *message.Message) (bool, error) { + var err error + propStatus, err := eh.propStorer.PropStatus( + msg.Source, + msg.Destination, + msg.Data.(transfer.TransferMessageData).DepositNonce) + if err != nil { + return true, err + } + + if propStatus == store.ExecutedProp { + return true, nil + } + + // change the status to failed if proposal is stuck to be able to retry it + if propStatus == store.PendingProp { + err = eh.propStorer.StorePropStatus( + msg.Source, + msg.Destination, + msg.Data.(transfer.TransferMessageData).DepositNonce, + store.FailedProp) + } + return false, err +} diff --git a/chains/evm/listener/eventHandlers/event-handler_test.go b/chains/evm/listener/eventHandlers/retry_test.go similarity index 54% rename from chains/evm/listener/eventHandlers/event-handler_test.go rename to chains/evm/listener/eventHandlers/retry_test.go index 4647cb59..2956b7df 100644 --- a/chains/evm/listener/eventHandlers/event-handler_test.go +++ b/chains/evm/listener/eventHandlers/retry_test.go @@ -17,14 +17,75 @@ import ( "github.com/ChainSafe/sygma-relayer/chains/evm/calls/events" "github.com/ChainSafe/sygma-relayer/chains/evm/listener/eventHandlers" mock_listener "github.com/ChainSafe/sygma-relayer/chains/evm/listener/eventHandlers/mock" + "github.com/ChainSafe/sygma-relayer/relayer/retry" "github.com/ChainSafe/sygma-relayer/relayer/transfer" "github.com/ChainSafe/sygma-relayer/store" "github.com/sygmaprotocol/sygma-core/relayer/message" ) -type RetryEventHandlerTestSuite struct { +type RetryV2EventHandlerTestSuite struct { suite.Suite - retryEventHandler *eventHandlers.RetryEventHandler + retryEventHandler *eventHandlers.RetryV2EventHandler + mockEventListener *mock_listener.MockEventListener + domainID uint8 + msgChan chan []*message.Message +} + +func TestRunRetryEventHandlerTestSuite(t *testing.T) { + suite.Run(t, new(RetryV2EventHandlerTestSuite)) +} + +func (s *RetryV2EventHandlerTestSuite) SetupTest() { + ctrl := gomock.NewController(s.T()) + s.domainID = 1 + s.mockEventListener = mock_listener.NewMockEventListener(ctrl) + s.msgChan = make(chan []*message.Message, 1) + s.retryEventHandler = eventHandlers.NewRetryV2EventHandler( + log.With(), + s.mockEventListener, + common.Address{}, + s.domainID, + s.msgChan) +} + +func (s *RetryV2EventHandlerTestSuite) Test_FetchRetryEventsFails() { + s.mockEventListener.EXPECT().FetchRetryV2Events(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return([]events.RetryV2Event{}, fmt.Errorf("error")) + + err := s.retryEventHandler.HandleEvents(big.NewInt(0), big.NewInt(5)) + + s.NotNil(err) + s.Equal(len(s.msgChan), 0) +} + +func (s *RetryV2EventHandlerTestSuite) Test_FetchRetryEvents_ValidRetry() { + s.mockEventListener.EXPECT().FetchRetryV2Events( + gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), + ).Return([]events.RetryV2Event{ + {SourceDomainID: 2, DestinationDomainID: 3, ResourceID: [32]byte{1}, BlockHeight: big.NewInt(100)}, + {SourceDomainID: 3, DestinationDomainID: 4, ResourceID: [32]byte{2}, BlockHeight: big.NewInt(101)}, + }, nil) + + err := s.retryEventHandler.HandleEvents(big.NewInt(0), big.NewInt(5)) + msgs2 := <-s.msgChan + msgs1 := <-s.msgChan + + s.Nil(err) + s.Equal(msgs1[0].Data, retry.RetryMessageData{ + SourceDomainID: 2, DestinationDomainID: 3, ResourceID: [32]byte{1}, BlockHeight: big.NewInt(100), + }) + s.Equal(msgs1[0].Destination, uint8(2)) + s.Equal(msgs1[0].Source, s.domainID) + + s.Equal(msgs2[0].Data, retry.RetryMessageData{ + SourceDomainID: 3, DestinationDomainID: 4, ResourceID: [32]byte{2}, BlockHeight: big.NewInt(101), + }) + s.Equal(msgs2[0].Destination, uint8(3)) + s.Equal(msgs2[0].Source, s.domainID) +} + +type RetryV1EventHandlerTestSuite struct { + suite.Suite + retryEventHandler *eventHandlers.RetryV1EventHandler mockDepositHandler *mock_listener.MockDepositHandler mockPropStorer *mock_listener.MockPropStorer mockEventListener *mock_listener.MockEventListener @@ -32,18 +93,18 @@ type RetryEventHandlerTestSuite struct { msgChan chan []*message.Message } -func TestRunRetryEventHandlerTestSuite(t *testing.T) { - suite.Run(t, new(RetryEventHandlerTestSuite)) +func TestRunRetryV1EventHandlerTestSuite(t *testing.T) { + suite.Run(t, new(RetryV1EventHandlerTestSuite)) } -func (s *RetryEventHandlerTestSuite) SetupTest() { +func (s *RetryV1EventHandlerTestSuite) SetupTest() { ctrl := gomock.NewController(s.T()) s.domainID = 1 s.mockEventListener = mock_listener.NewMockEventListener(ctrl) s.mockDepositHandler = mock_listener.NewMockDepositHandler(ctrl) s.mockPropStorer = mock_listener.NewMockPropStorer(ctrl) s.msgChan = make(chan []*message.Message, 1) - s.retryEventHandler = eventHandlers.NewRetryEventHandler( + s.retryEventHandler = eventHandlers.NewRetryV1EventHandler( log.With(), s.mockEventListener, s.mockDepositHandler, @@ -54,8 +115,8 @@ func (s *RetryEventHandlerTestSuite) SetupTest() { s.msgChan) } -func (s *RetryEventHandlerTestSuite) Test_FetchDepositFails() { - s.mockEventListener.EXPECT().FetchRetryEvents(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return([]events.RetryEvent{}, fmt.Errorf("error")) +func (s *RetryV1EventHandlerTestSuite) Test_FetchDepositFails() { + s.mockEventListener.EXPECT().FetchRetryV1Events(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return([]events.RetryV1Event{}, fmt.Errorf("error")) err := s.retryEventHandler.HandleEvents(big.NewInt(0), big.NewInt(5)) @@ -63,7 +124,7 @@ func (s *RetryEventHandlerTestSuite) Test_FetchDepositFails() { s.Equal(len(s.msgChan), 0) } -func (s *RetryEventHandlerTestSuite) Test_FetchDepositFails_ExecutionContinues() { +func (s *RetryV1EventHandlerTestSuite) Test_FetchDepositFails_ExecutionContinues() { d := events.Deposit{ DepositNonce: 2, DestinationDomainID: 2, @@ -72,11 +133,11 @@ func (s *RetryEventHandlerTestSuite) Test_FetchDepositFails_ExecutionContinues() Data: []byte{}, } msgID := fmt.Sprintf("retry-%d-%d-%d-%d", 1, 2, 0, 5) - s.mockEventListener.EXPECT().FetchRetryEvents( + s.mockEventListener.EXPECT().FetchRetryV1Events( gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), - ).Return([]events.RetryEvent{{TxHash: "event1"}, {TxHash: "event2"}}, nil) - s.mockEventListener.EXPECT().FetchRetryDepositEvents(events.RetryEvent{TxHash: "event1"}, gomock.Any(), big.NewInt(5)).Return([]events.Deposit{}, fmt.Errorf("error")) - s.mockEventListener.EXPECT().FetchRetryDepositEvents(events.RetryEvent{TxHash: "event2"}, gomock.Any(), big.NewInt(5)).Return([]events.Deposit{d}, nil) + ).Return([]events.RetryV1Event{{TxHash: "event1"}, {TxHash: "event2"}}, nil) + s.mockEventListener.EXPECT().FetchRetryDepositEvents(events.RetryV1Event{TxHash: "event1"}, gomock.Any(), big.NewInt(5)).Return([]events.Deposit{}, fmt.Errorf("error")) + s.mockEventListener.EXPECT().FetchRetryDepositEvents(events.RetryV1Event{TxHash: "event2"}, gomock.Any(), big.NewInt(5)).Return([]events.Deposit{d}, nil) s.mockDepositHandler.EXPECT().HandleDeposit( s.domainID, d.DestinationDomainID, @@ -102,7 +163,7 @@ func (s *RetryEventHandlerTestSuite) Test_FetchDepositFails_ExecutionContinues() }}}) } -func (s *RetryEventHandlerTestSuite) Test_HandleDepositFails_ExecutionContinues() { +func (s *RetryV1EventHandlerTestSuite) Test_HandleDepositFails_ExecutionContinues() { d1 := events.Deposit{ DepositNonce: 1, DestinationDomainID: 2, @@ -118,11 +179,11 @@ func (s *RetryEventHandlerTestSuite) Test_HandleDepositFails_ExecutionContinues( Data: []byte{}, } msgID := fmt.Sprintf("retry-%d-%d-%d-%d", 1, 2, 0, 5) - s.mockEventListener.EXPECT().FetchRetryEvents( + s.mockEventListener.EXPECT().FetchRetryV1Events( gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), - ).Return([]events.RetryEvent{{TxHash: "event1"}, {TxHash: "event2"}}, nil) - s.mockEventListener.EXPECT().FetchRetryDepositEvents(events.RetryEvent{TxHash: "event1"}, gomock.Any(), big.NewInt(5)).Return([]events.Deposit{d1}, nil) - s.mockEventListener.EXPECT().FetchRetryDepositEvents(events.RetryEvent{TxHash: "event2"}, gomock.Any(), big.NewInt(5)).Return([]events.Deposit{d2}, nil) + ).Return([]events.RetryV1Event{{TxHash: "event1"}, {TxHash: "event2"}}, nil) + s.mockEventListener.EXPECT().FetchRetryDepositEvents(events.RetryV1Event{TxHash: "event1"}, gomock.Any(), big.NewInt(5)).Return([]events.Deposit{d1}, nil) + s.mockEventListener.EXPECT().FetchRetryDepositEvents(events.RetryV1Event{TxHash: "event2"}, gomock.Any(), big.NewInt(5)).Return([]events.Deposit{d2}, nil) s.mockDepositHandler.EXPECT().HandleDeposit( s.domainID, d1.DestinationDomainID, @@ -156,7 +217,7 @@ func (s *RetryEventHandlerTestSuite) Test_HandleDepositFails_ExecutionContinues( s.Equal(msgs, []*message.Message{{Data: transfer.TransferMessageData{DepositNonce: 2}}}) } -func (s *RetryEventHandlerTestSuite) Test_HandlingRetryPanics_ExecutionContinue() { +func (s *RetryV1EventHandlerTestSuite) Test_HandlingRetryPanics_ExecutionContinue() { d1 := events.Deposit{ DepositNonce: 1, DestinationDomainID: 2, @@ -171,11 +232,11 @@ func (s *RetryEventHandlerTestSuite) Test_HandlingRetryPanics_ExecutionContinue( HandlerResponse: []byte{}, Data: []byte{}, } - s.mockEventListener.EXPECT().FetchRetryEvents( + s.mockEventListener.EXPECT().FetchRetryV1Events( gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), - ).Return([]events.RetryEvent{{TxHash: "event1"}, {TxHash: "event2"}}, nil) - s.mockEventListener.EXPECT().FetchRetryDepositEvents(events.RetryEvent{TxHash: "event1"}, gomock.Any(), big.NewInt(5)).Return([]events.Deposit{d1}, nil) - s.mockEventListener.EXPECT().FetchRetryDepositEvents(events.RetryEvent{TxHash: "event2"}, gomock.Any(), big.NewInt(5)).Return([]events.Deposit{d2}, nil) + ).Return([]events.RetryV1Event{{TxHash: "event1"}, {TxHash: "event2"}}, nil) + s.mockEventListener.EXPECT().FetchRetryDepositEvents(events.RetryV1Event{TxHash: "event1"}, gomock.Any(), big.NewInt(5)).Return([]events.Deposit{d1}, nil) + s.mockEventListener.EXPECT().FetchRetryDepositEvents(events.RetryV1Event{TxHash: "event2"}, gomock.Any(), big.NewInt(5)).Return([]events.Deposit{d2}, nil) msgID := fmt.Sprintf("retry-%d-%d-%d-%d", 1, 2, 0, 5) s.mockDepositHandler.EXPECT().HandleDeposit( s.domainID, @@ -212,7 +273,7 @@ func (s *RetryEventHandlerTestSuite) Test_HandlingRetryPanics_ExecutionContinue( }}}) } -func (s *RetryEventHandlerTestSuite) Test_MultipleDeposits() { +func (s *RetryV1EventHandlerTestSuite) Test_MultipleDeposits() { d1 := events.Deposit{ DepositNonce: 1, DestinationDomainID: 2, @@ -227,10 +288,10 @@ func (s *RetryEventHandlerTestSuite) Test_MultipleDeposits() { HandlerResponse: []byte{}, Data: []byte{}, } - s.mockEventListener.EXPECT().FetchRetryEvents( + s.mockEventListener.EXPECT().FetchRetryV1Events( gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), - ).Return([]events.RetryEvent{{TxHash: "event1"}}, nil) - s.mockEventListener.EXPECT().FetchRetryDepositEvents(events.RetryEvent{TxHash: "event1"}, gomock.Any(), big.NewInt(5)).Return([]events.Deposit{d1, d2}, nil) + ).Return([]events.RetryV1Event{{TxHash: "event1"}}, nil) + s.mockEventListener.EXPECT().FetchRetryDepositEvents(events.RetryV1Event{TxHash: "event1"}, gomock.Any(), big.NewInt(5)).Return([]events.Deposit{d1, d2}, nil) msgID := fmt.Sprintf("retry-%d-%d-%d-%d", 1, 2, 0, 5) s.mockDepositHandler.EXPECT().HandleDeposit( s.domainID, @@ -269,7 +330,7 @@ func (s *RetryEventHandlerTestSuite) Test_MultipleDeposits() { }}}) } -func (s *RetryEventHandlerTestSuite) Test_MultipleDeposits_ExecutedIgnored() { +func (s *RetryV1EventHandlerTestSuite) Test_MultipleDeposits_ExecutedIgnored() { d1 := events.Deposit{ DepositNonce: 1, DestinationDomainID: 2, @@ -284,10 +345,10 @@ func (s *RetryEventHandlerTestSuite) Test_MultipleDeposits_ExecutedIgnored() { HandlerResponse: []byte{}, Data: []byte{}, } - s.mockEventListener.EXPECT().FetchRetryEvents( + s.mockEventListener.EXPECT().FetchRetryV1Events( gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), - ).Return([]events.RetryEvent{{TxHash: "event1"}}, nil) - s.mockEventListener.EXPECT().FetchRetryDepositEvents(events.RetryEvent{TxHash: "event1"}, gomock.Any(), big.NewInt(5)).Return([]events.Deposit{d1, d2}, nil) + ).Return([]events.RetryV1Event{{TxHash: "event1"}}, nil) + s.mockEventListener.EXPECT().FetchRetryDepositEvents(events.RetryV1Event{TxHash: "event1"}, gomock.Any(), big.NewInt(5)).Return([]events.Deposit{d1, d2}, nil) msgID := fmt.Sprintf("retry-%d-%d-%d-%d", 1, 2, 0, 5) s.mockDepositHandler.EXPECT().HandleDeposit( s.domainID, @@ -329,190 +390,3 @@ func (s *RetryEventHandlerTestSuite) Test_MultipleDeposits_ExecutedIgnored() { }, }) } - -type DepositHandlerTestSuite struct { - suite.Suite - depositEventHandler *eventHandlers.DepositEventHandler - mockDepositHandler *mock_listener.MockDepositHandler - mockEventListener *mock_listener.MockEventListener - domainID uint8 - msgChan chan []*message.Message -} - -func TestRunDepositHandlerTestSuite(t *testing.T) { - suite.Run(t, new(DepositHandlerTestSuite)) -} - -func (s *DepositHandlerTestSuite) SetupTest() { - ctrl := gomock.NewController(s.T()) - s.domainID = 1 - s.mockEventListener = mock_listener.NewMockEventListener(ctrl) - s.mockDepositHandler = mock_listener.NewMockDepositHandler(ctrl) - s.msgChan = make(chan []*message.Message, 2) - s.depositEventHandler = eventHandlers.NewDepositEventHandler(s.mockEventListener, s.mockDepositHandler, common.Address{}, s.domainID, s.msgChan) -} - -func (s *DepositHandlerTestSuite) Test_FetchDepositFails() { - s.mockEventListener.EXPECT().FetchDeposits(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return([]*events.Deposit{}, fmt.Errorf("error")) - - err := s.depositEventHandler.HandleEvents(big.NewInt(0), big.NewInt(5)) - - s.NotNil(err) - s.Equal(len(s.msgChan), 0) -} - -func (s *DepositHandlerTestSuite) Test_HandleDepositFails_ExecutionContinue() { - d1 := &events.Deposit{ - DepositNonce: 1, - DestinationDomainID: 2, - ResourceID: [32]byte{}, - HandlerResponse: []byte{}, - Data: []byte{}, - } - d2 := &events.Deposit{ - DepositNonce: 2, - DestinationDomainID: 2, - ResourceID: [32]byte{}, - HandlerResponse: []byte{}, - Data: []byte{}, - } - deposits := []*events.Deposit{d1, d2} - s.mockEventListener.EXPECT().FetchDeposits(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(deposits, nil) - msgID := fmt.Sprintf("%d-%d-%d-%d", 1, 2, 0, 5) - s.mockDepositHandler.EXPECT().HandleDeposit( - s.domainID, - d1.DestinationDomainID, - d1.DepositNonce, - d1.ResourceID, - d1.Data, - d1.HandlerResponse, - msgID, - gomock.Any(), - ).Return(&message.Message{}, fmt.Errorf("error")) - s.mockDepositHandler.EXPECT().HandleDeposit( - s.domainID, - d2.DestinationDomainID, - d2.DepositNonce, - d2.ResourceID, - d2.Data, - d2.HandlerResponse, - msgID, - gomock.Any(), - ).Return( - &message.Message{ - Data: transfer.TransferMessageData{ - DepositNonce: 2, - }, - }, - nil, - ) - - err := s.depositEventHandler.HandleEvents(big.NewInt(0), big.NewInt(5)) - msgs := <-s.msgChan - - s.Nil(err) - s.Equal(msgs, []*message.Message{{Data: transfer.TransferMessageData{DepositNonce: 2}}}) -} - -func (s *DepositHandlerTestSuite) Test_HandleDepositPanis_ExecutionContinues() { - d1 := &events.Deposit{ - DepositNonce: 1, - DestinationDomainID: 2, - ResourceID: [32]byte{}, - HandlerResponse: []byte{}, - Data: []byte{}, - } - d2 := &events.Deposit{ - DepositNonce: 2, - DestinationDomainID: 2, - ResourceID: [32]byte{}, - HandlerResponse: []byte{}, - Data: []byte{}, - } - deposits := []*events.Deposit{d1, d2} - s.mockEventListener.EXPECT().FetchDeposits(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(deposits, nil) - msgID := fmt.Sprintf("%d-%d-%d-%d", 1, 2, 0, 5) - s.mockDepositHandler.EXPECT().HandleDeposit( - s.domainID, - d1.DestinationDomainID, - d1.DepositNonce, - d1.ResourceID, - d1.Data, - d1.HandlerResponse, - msgID, - gomock.Any(), - ).Do(func(sourceID, destID, nonce, resourceID, calldata, handlerResponse, msgID, timestamp interface{}) { - panic("error") - }) - s.mockDepositHandler.EXPECT().HandleDeposit( - s.domainID, - d2.DestinationDomainID, - d2.DepositNonce, - d2.ResourceID, - d2.Data, - d2.HandlerResponse, - msgID, - gomock.Any(), - ).Return( - &message.Message{Data: transfer.TransferMessageData{DepositNonce: 2}}, - nil, - ) - - err := s.depositEventHandler.HandleEvents(big.NewInt(0), big.NewInt(5)) - msgs := <-s.msgChan - - s.Nil(err) - s.Equal(msgs, []*message.Message{{Data: transfer.TransferMessageData{DepositNonce: 2}}}) -} - -func (s *DepositHandlerTestSuite) Test_SuccessfulHandleDeposit() { - d1 := &events.Deposit{ - DepositNonce: 1, - DestinationDomainID: 2, - ResourceID: [32]byte{}, - HandlerResponse: []byte{}, - Data: []byte{}, - } - d2 := &events.Deposit{ - DepositNonce: 2, - DestinationDomainID: 2, - ResourceID: [32]byte{}, - HandlerResponse: []byte{}, - Data: []byte{}, - } - deposits := []*events.Deposit{d1, d2} - s.mockEventListener.EXPECT().FetchDeposits(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(deposits, nil) - msgID := fmt.Sprintf("%d-%d-%d-%d", 1, 2, 0, 5) - s.mockDepositHandler.EXPECT().HandleDeposit( - s.domainID, - d1.DestinationDomainID, - d1.DepositNonce, - d1.ResourceID, - d1.Data, - d1.HandlerResponse, - msgID, - gomock.Any(), - ).Return( - &message.Message{Data: transfer.TransferMessageData{DepositNonce: 1}}, - nil, - ) - s.mockDepositHandler.EXPECT().HandleDeposit( - s.domainID, - d2.DestinationDomainID, - d2.DepositNonce, - d2.ResourceID, - d2.Data, - d2.HandlerResponse, - msgID, - gomock.Any(), - ).Return( - &message.Message{Data: transfer.TransferMessageData{DepositNonce: 2}}, - nil, - ) - - err := s.depositEventHandler.HandleEvents(big.NewInt(0), big.NewInt(5)) - msgs := <-s.msgChan - - s.Nil(err) - s.Equal(msgs, []*message.Message{{Data: transfer.TransferMessageData{DepositNonce: 1}}, {Data: transfer.TransferMessageData{DepositNonce: 2}}}) -} diff --git a/chains/evm/listener/eventHandlers/event-handler.go b/chains/evm/listener/eventHandlers/tss.go similarity index 50% rename from chains/evm/listener/eventHandlers/event-handler.go rename to chains/evm/listener/eventHandlers/tss.go index 0f9f0b9f..26ba7e0e 100644 --- a/chains/evm/listener/eventHandlers/event-handler.go +++ b/chains/evm/listener/eventHandlers/tss.go @@ -7,19 +7,10 @@ import ( "context" "fmt" "math/big" - "strings" - "time" "github.com/rs/zerolog" "github.com/rs/zerolog/log" - "github.com/ChainSafe/sygma-relayer/chains/evm/calls/consts" - "github.com/ChainSafe/sygma-relayer/relayer/transfer" - "github.com/ChainSafe/sygma-relayer/store" - "github.com/ethereum/go-ethereum/accounts/abi" - "github.com/sygmaprotocol/sygma-core/relayer/message" - - "github.com/ChainSafe/sygma-relayer/chains/evm/calls/events" "github.com/ChainSafe/sygma-relayer/comm" "github.com/ChainSafe/sygma-relayer/comm/p2p" "github.com/ChainSafe/sygma-relayer/topology" @@ -29,145 +20,9 @@ import ( frostKeygen "github.com/ChainSafe/sygma-relayer/tss/frost/keygen" frostResharing "github.com/ChainSafe/sygma-relayer/tss/frost/resharing" "github.com/ethereum/go-ethereum/common" - ethTypes "github.com/ethereum/go-ethereum/core/types" "github.com/libp2p/go-libp2p/core/host" ) -type EventListener interface { - FetchKeygenEvents(ctx context.Context, address common.Address, startBlock *big.Int, endBlock *big.Int) ([]ethTypes.Log, error) - FetchFrostKeygenEvents(ctx context.Context, address common.Address, startBlock *big.Int, endBlock *big.Int) ([]ethTypes.Log, error) - FetchRefreshEvents(ctx context.Context, address common.Address, startBlock *big.Int, endBlock *big.Int) ([]*events.Refresh, error) - FetchRetryEvents(ctx context.Context, contractAddress common.Address, startBlock *big.Int, endBlock *big.Int) ([]events.RetryEvent, error) - FetchRetryDepositEvents(event events.RetryEvent, bridgeAddress common.Address, blockConfirmations *big.Int) ([]events.Deposit, error) - FetchDeposits(ctx context.Context, address common.Address, startBlock *big.Int, endBlock *big.Int) ([]*events.Deposit, error) -} - -type PropStorer interface { - StorePropStatus(source, destination uint8, depositNonce uint64, status store.PropStatus) error - PropStatus(source, destination uint8, depositNonce uint64) (store.PropStatus, error) -} - -type RetryEventHandler struct { - log zerolog.Logger - eventListener EventListener - depositHandler DepositHandler - propStorer PropStorer - bridgeAddress common.Address - bridgeABI abi.ABI - domainID uint8 - blockConfirmations *big.Int - msgChan chan []*message.Message -} - -func NewRetryEventHandler( - logC zerolog.Context, - eventListener EventListener, - depositHandler DepositHandler, - propStorer PropStorer, - bridgeAddress common.Address, - domainID uint8, - blockConfirmations *big.Int, - msgChan chan []*message.Message, -) *RetryEventHandler { - bridgeABI, _ := abi.JSON(strings.NewReader(consts.BridgeABI)) - return &RetryEventHandler{ - log: logC.Logger(), - eventListener: eventListener, - depositHandler: depositHandler, - propStorer: propStorer, - bridgeAddress: bridgeAddress, - bridgeABI: bridgeABI, - domainID: domainID, - blockConfirmations: blockConfirmations, - msgChan: msgChan, - } -} - -func (eh *RetryEventHandler) HandleEvents( - startBlock *big.Int, - endBlock *big.Int, -) error { - retryEvents, err := eh.eventListener.FetchRetryEvents(context.Background(), eh.bridgeAddress, startBlock, endBlock) - if err != nil { - return fmt.Errorf("unable to fetch retry events because of: %+v", err) - } - - retriesByDomain := make(map[uint8][]*message.Message) - for _, event := range retryEvents { - func(event events.RetryEvent) { - defer func() { - if r := recover(); r != nil { - eh.log.Error().Err(err).Msgf("panic occured while handling retry event %+v", event) - } - }() - - deposits, err := eh.eventListener.FetchRetryDepositEvents(event, eh.bridgeAddress, eh.blockConfirmations) - if err != nil { - eh.log.Error().Err(err).Msgf("Unable to fetch deposit events from event %+v", event) - return - } - - for _, d := range deposits { - messageID := fmt.Sprintf("retry-%d-%d-%d-%d", eh.domainID, d.DestinationDomainID, startBlock, endBlock) - msg, err := eh.depositHandler.HandleDeposit( - eh.domainID, d.DestinationDomainID, d.DepositNonce, - d.ResourceID, d.Data, d.HandlerResponse, messageID, - d.Timestamp, - ) - if err != nil { - eh.log.Err(err).Str("messageID", msg.ID).Msgf("Failed handling deposit %+v", d) - continue - } - isExecuted, err := eh.isExecuted(msg) - if err != nil { - eh.log.Err(err).Str("messageID", msg.ID).Msgf("Failed checking if deposit executed %+v", d) - continue - } - if isExecuted { - eh.log.Debug().Str("messageID", msg.ID).Msgf("Deposit marked as executed %+v", d) - continue - } - - eh.log.Info().Str("messageID", msg.ID).Msgf( - "Resolved retry message %+v in block range: %s-%s", msg, startBlock.String(), endBlock.String(), - ) - retriesByDomain[msg.Destination] = append(retriesByDomain[msg.Destination], msg) - } - }(event) - } - - for _, retries := range retriesByDomain { - eh.msgChan <- retries - } - - return nil -} - -func (eh *RetryEventHandler) isExecuted(msg *message.Message) (bool, error) { - var err error - propStatus, err := eh.propStorer.PropStatus( - msg.Source, - msg.Destination, - msg.Data.(transfer.TransferMessageData).DepositNonce) - if err != nil { - return true, err - } - - if propStatus == store.ExecutedProp { - return true, nil - } - - // change the status to failed if proposal is stuck to be able to retry it - if propStatus == store.PendingProp { - err = eh.propStorer.StorePropStatus( - msg.Source, - msg.Destination, - msg.Data.(transfer.TransferMessageData).DepositNonce, - store.FailedProp) - } - return false, err -} - type KeygenEventHandler struct { log zerolog.Logger eventListener EventListener @@ -397,61 +252,3 @@ func (eh *RefreshEventHandler) HandleEvents( func (eh *RefreshEventHandler) sessionID(block *big.Int) string { return fmt.Sprintf("resharing-%s", block.String()) } - -type DepositHandler interface { - HandleDeposit(sourceID, destID uint8, nonce uint64, resourceID [32]byte, calldata, handlerResponse []byte, messageID string, timestamp time.Time) (*message.Message, error) -} - -type DepositEventHandler struct { - eventListener EventListener - depositHandler DepositHandler - bridgeAddress common.Address - domainID uint8 - msgChan chan []*message.Message -} - -func NewDepositEventHandler(eventListener EventListener, depositHandler DepositHandler, bridgeAddress common.Address, domainID uint8, msgChan chan []*message.Message) *DepositEventHandler { - return &DepositEventHandler{ - eventListener: eventListener, - depositHandler: depositHandler, - bridgeAddress: bridgeAddress, - domainID: domainID, - msgChan: msgChan, - } -} - -func (eh *DepositEventHandler) HandleEvents(startBlock *big.Int, endBlock *big.Int) error { - deposits, err := eh.eventListener.FetchDeposits(context.Background(), eh.bridgeAddress, startBlock, endBlock) - if err != nil { - return fmt.Errorf("unable to fetch deposit events because of: %+v", err) - } - - domainDeposits := make(map[uint8][]*message.Message) - for _, d := range deposits { - func(d *events.Deposit) { - defer func() { - if r := recover(); r != nil { - log.Error().Err(err).Msgf("panic occured while handling deposit %+v", d) - } - }() - - messageID := fmt.Sprintf("%d-%d-%d-%d", eh.domainID, d.DestinationDomainID, startBlock, endBlock) - m, err := eh.depositHandler.HandleDeposit(eh.domainID, d.DestinationDomainID, d.DepositNonce, d.ResourceID, d.Data, d.HandlerResponse, messageID, d.Timestamp) - if err != nil { - log.Error().Err(err).Str("start block", startBlock.String()).Str("end block", endBlock.String()).Uint8("domainID", eh.domainID).Msgf("%v", err) - return - } - - log.Debug().Str("messageID", m.ID).Msgf("Resolved message %+v in block range: %s-%s", m, startBlock.String(), endBlock.String()) - domainDeposits[m.Destination] = append(domainDeposits[m.Destination], m) - }(d) - } - - for _, deposits := range domainDeposits { - go func(d []*message.Message) { - eh.msgChan <- d - }(deposits) - } - - return nil -} diff --git a/chains/substrate/executor/message-handler.go b/chains/substrate/executor/message-handler.go index 82b4ea21..75613fdf 100644 --- a/chains/substrate/executor/message-handler.go +++ b/chains/substrate/executor/message-handler.go @@ -5,9 +5,13 @@ package executor import ( "errors" + "fmt" "math/big" + "github.com/ChainSafe/sygma-relayer/relayer/retry" "github.com/ChainSafe/sygma-relayer/relayer/transfer" + "github.com/ChainSafe/sygma-relayer/store" + "github.com/centrifuge/go-substrate-rpc-client/v4/types" "github.com/ethereum/go-ethereum/common" "github.com/sygmaprotocol/sygma-core/relayer/message" "github.com/sygmaprotocol/sygma-core/relayer/proposal" @@ -55,3 +59,72 @@ func fungibleTransferMessageHandler(m *transfer.TransferMessage) (*proposal.Prop Data: data, }, m.ID, transfer.TransferProposalType), nil } + +type PropStorer interface { + StorePropStatus(source, destination uint8, depositNonce uint64, status store.PropStatus) error + PropStatus(source, destination uint8, depositNonce uint64) (store.PropStatus, error) +} + +type BlockFetcher interface { + GetFinalizedHead() (types.Hash, error) + GetBlock(blockHash types.Hash) (*types.SignedBlock, error) +} + +type DepositProcessor interface { + ProcessDeposits(startBlock *big.Int, endBlock *big.Int) (map[uint8][]*message.Message, error) +} + +type RetryMessageHandler struct { + depositProcessor DepositProcessor + blockFetcher BlockFetcher + propStorer PropStorer + msgChan chan []*message.Message +} + +func NewRetryMessageHandler( + depositProcessor DepositProcessor, + blockFetcher BlockFetcher, + propStorer PropStorer, + msgChan chan []*message.Message) *RetryMessageHandler { + return &RetryMessageHandler{ + depositProcessor: depositProcessor, + blockFetcher: blockFetcher, + propStorer: propStorer, + msgChan: msgChan, + } +} + +func (h *RetryMessageHandler) HandleMessage(msg *message.Message) (*proposal.Proposal, error) { + retryData := msg.Data.(retry.RetryMessageData) + hash, err := h.blockFetcher.GetFinalizedHead() + if err != nil { + return nil, err + } + finalized, err := h.blockFetcher.GetBlock(hash) + if err != nil { + return nil, err + } + latestBlock := big.NewInt(int64(finalized.Block.Header.Number)) + if latestBlock.Cmp(retryData.BlockHeight) != 1 { + return nil, fmt.Errorf( + "latest block %s higher than receipt block number %s", + latestBlock, + retryData.BlockHeight, + ) + } + + domainDeposits, err := h.depositProcessor.ProcessDeposits(retryData.BlockHeight, retryData.BlockHeight) + if err != nil { + return nil, err + } + filteredDeposits, err := retry.FilterDeposits(h.propStorer, domainDeposits, retryData.ResourceID, retryData.DestinationDomainID) + if err != nil { + return nil, err + } + if len(filteredDeposits) == 0 { + return nil, nil + } + + h.msgChan <- filteredDeposits + return nil, nil +} diff --git a/chains/substrate/executor/message-handler_test.go b/chains/substrate/executor/message-handler_test.go index 02a6b644..9314911a 100644 --- a/chains/substrate/executor/message-handler_test.go +++ b/chains/substrate/executor/message-handler_test.go @@ -6,12 +6,19 @@ package executor_test import ( "encoding/hex" "errors" + "math/big" "testing" "unsafe" "github.com/ChainSafe/sygma-relayer/chains/substrate/executor" + mock_executor "github.com/ChainSafe/sygma-relayer/chains/substrate/executor/mock" + "github.com/ChainSafe/sygma-relayer/relayer/retry" "github.com/ChainSafe/sygma-relayer/relayer/transfer" + "github.com/ChainSafe/sygma-relayer/store" + "github.com/ethereum/go-ethereum/common" + "github.com/golang/mock/gomock" + "github.com/ChainSafe/sygma-relayer/e2e/evm" "github.com/ChainSafe/sygma-relayer/e2e/substrate" "github.com/centrifuge/go-substrate-rpc-client/v4/types" "github.com/sygmaprotocol/sygma-core/relayer/message" @@ -190,3 +197,167 @@ func (s *FungibleTransferHandlerTestSuite) TestSuccesfullyRegisterFungibleTransf s.Nil(prop2) s.NotNil(err2) } + +type RetryMessageHandlerTestSuite struct { + suite.Suite + + messageHandler *executor.RetryMessageHandler + mockBlockFetcher *mock_executor.MockBlockFetcher + mockDepositProcessor *mock_executor.MockDepositProcessor + mockPropStorer *mock_executor.MockPropStorer + msgChan chan []*message.Message +} + +func TestRunRetryMessageHandlerTestSuite(t *testing.T) { + suite.Run(t, new(RetryMessageHandlerTestSuite)) +} + +func (s *RetryMessageHandlerTestSuite) SetupTest() { + ctrl := gomock.NewController(s.T()) + s.mockBlockFetcher = mock_executor.NewMockBlockFetcher(ctrl) + s.mockDepositProcessor = mock_executor.NewMockDepositProcessor(ctrl) + s.mockPropStorer = mock_executor.NewMockPropStorer(ctrl) + s.msgChan = make(chan []*message.Message, 1) + s.messageHandler = executor.NewRetryMessageHandler( + s.mockDepositProcessor, + s.mockBlockFetcher, + s.mockPropStorer, + s.msgChan) +} + +func (s *RetryMessageHandlerTestSuite) Test_HandleMessage_RetryNotFinalized() { + s.mockBlockFetcher.EXPECT().GetFinalizedHead().Return(types.Hash{}, nil) + s.mockBlockFetcher.EXPECT().GetBlock(gomock.Any()).Return(&types.SignedBlock{ + Block: types.Block{ + Header: types.Header{ + Number: 99, + }, + }, + }, nil) + + message := &message.Message{ + Source: 1, + Destination: 3, + Data: retry.RetryMessageData{ + SourceDomainID: 3, + DestinationDomainID: 4, + BlockHeight: big.NewInt(100), + ResourceID: [32]byte{}, + }, + Type: transfer.TransferMessageType, + } + + prop, err := s.messageHandler.HandleMessage(message) + + s.Nil(prop) + s.NotNil(err) +} + +func (s *RetryMessageHandlerTestSuite) Test_HandleMessage_NoDeposits() { + s.mockBlockFetcher.EXPECT().GetFinalizedHead().Return(types.Hash{}, nil) + s.mockBlockFetcher.EXPECT().GetBlock(gomock.Any()).Return(&types.SignedBlock{ + Block: types.Block{ + Header: types.Header{ + Number: 101, + }, + }, + }, nil) + s.mockDepositProcessor.EXPECT().ProcessDeposits(big.NewInt(100), big.NewInt(100)).Return(make(map[uint8][]*message.Message), nil) + + message := &message.Message{ + Source: 1, + Destination: 3, + Data: retry.RetryMessageData{ + SourceDomainID: 3, + DestinationDomainID: 4, + BlockHeight: big.NewInt(100), + ResourceID: [32]byte{}, + }, + Type: transfer.TransferMessageType, + } + + prop, err := s.messageHandler.HandleMessage(message) + + s.Nil(prop) + s.Nil(err) + s.Equal(len(s.msgChan), 0) +} + +func (s *RetryMessageHandlerTestSuite) Test_HandleMessage_ValidDeposits() { + s.mockBlockFetcher.EXPECT().GetFinalizedHead().Return(types.Hash{}, nil) + s.mockBlockFetcher.EXPECT().GetBlock(gomock.Any()).Return(&types.SignedBlock{ + Block: types.Block{ + Header: types.Header{ + Number: 101, + }, + }, + }, nil) + + validResource := evm.SliceTo32Bytes(common.LeftPadBytes([]byte{3}, 31)) + invalidResource := evm.SliceTo32Bytes(common.LeftPadBytes([]byte{4}, 31)) + invalidDomain := uint8(3) + validDomain := uint8(4) + + executedNonce := uint64(1) + failedNonce := uint64(3) + + deposits := make(map[uint8][]*message.Message) + deposits[invalidDomain] = []*message.Message{ + { + Destination: invalidDomain, + Data: transfer.TransferMessageData{ + DepositNonce: 1, + ResourceId: validResource, + }, + }, + } + deposits[validDomain] = []*message.Message{ + { + Source: invalidDomain, + Destination: validDomain, + Data: transfer.TransferMessageData{ + DepositNonce: executedNonce, + ResourceId: validResource, + }, + }, + { + Source: invalidDomain, + Destination: validDomain, + Data: transfer.TransferMessageData{ + DepositNonce: 2, + ResourceId: invalidResource, + }, + }, + { + Source: invalidDomain, + Destination: validDomain, + Data: transfer.TransferMessageData{ + DepositNonce: failedNonce, + ResourceId: validResource, + }, + }, + } + s.mockDepositProcessor.EXPECT().ProcessDeposits(big.NewInt(100), big.NewInt(100)).Return(deposits, nil) + s.mockPropStorer.EXPECT().PropStatus(invalidDomain, validDomain, executedNonce).Return(store.ExecutedProp, nil) + s.mockPropStorer.EXPECT().PropStatus(invalidDomain, validDomain, failedNonce).Return(store.FailedProp, nil) + + message := &message.Message{ + Source: 1, + Destination: 3, + Data: retry.RetryMessageData{ + SourceDomainID: invalidDomain, + DestinationDomainID: validDomain, + BlockHeight: big.NewInt(100), + ResourceID: validResource, + }, + Type: transfer.TransferMessageType, + } + + prop, err := s.messageHandler.HandleMessage(message) + + s.Nil(prop) + s.Nil(err) + msgs := <-s.msgChan + s.Equal(msgs[0].Data.(transfer.TransferMessageData).DepositNonce, failedNonce) + s.Equal(msgs[0].Destination, validDomain) +} diff --git a/chains/substrate/executor/mock/message-handler.go b/chains/substrate/executor/mock/message-handler.go new file mode 100644 index 00000000..d476efc7 --- /dev/null +++ b/chains/substrate/executor/mock/message-handler.go @@ -0,0 +1,158 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: ./chains/substrate/executor/message-handler.go + +// Package mock_executor is a generated GoMock package. +package mock_executor + +import ( + big "math/big" + reflect "reflect" + + store "github.com/ChainSafe/sygma-relayer/store" + types "github.com/centrifuge/go-substrate-rpc-client/v4/types" + gomock "github.com/golang/mock/gomock" + message "github.com/sygmaprotocol/sygma-core/relayer/message" +) + +// MockPropStorer is a mock of PropStorer interface. +type MockPropStorer struct { + ctrl *gomock.Controller + recorder *MockPropStorerMockRecorder +} + +// MockPropStorerMockRecorder is the mock recorder for MockPropStorer. +type MockPropStorerMockRecorder struct { + mock *MockPropStorer +} + +// NewMockPropStorer creates a new mock instance. +func NewMockPropStorer(ctrl *gomock.Controller) *MockPropStorer { + mock := &MockPropStorer{ctrl: ctrl} + mock.recorder = &MockPropStorerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockPropStorer) EXPECT() *MockPropStorerMockRecorder { + return m.recorder +} + +// PropStatus mocks base method. +func (m *MockPropStorer) PropStatus(source, destination uint8, depositNonce uint64) (store.PropStatus, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PropStatus", source, destination, depositNonce) + ret0, _ := ret[0].(store.PropStatus) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// PropStatus indicates an expected call of PropStatus. +func (mr *MockPropStorerMockRecorder) PropStatus(source, destination, depositNonce interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PropStatus", reflect.TypeOf((*MockPropStorer)(nil).PropStatus), source, destination, depositNonce) +} + +// StorePropStatus mocks base method. +func (m *MockPropStorer) StorePropStatus(source, destination uint8, depositNonce uint64, status store.PropStatus) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "StorePropStatus", source, destination, depositNonce, status) + ret0, _ := ret[0].(error) + return ret0 +} + +// StorePropStatus indicates an expected call of StorePropStatus. +func (mr *MockPropStorerMockRecorder) StorePropStatus(source, destination, depositNonce, status interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StorePropStatus", reflect.TypeOf((*MockPropStorer)(nil).StorePropStatus), source, destination, depositNonce, status) +} + +// MockBlockFetcher is a mock of BlockFetcher interface. +type MockBlockFetcher struct { + ctrl *gomock.Controller + recorder *MockBlockFetcherMockRecorder +} + +// MockBlockFetcherMockRecorder is the mock recorder for MockBlockFetcher. +type MockBlockFetcherMockRecorder struct { + mock *MockBlockFetcher +} + +// NewMockBlockFetcher creates a new mock instance. +func NewMockBlockFetcher(ctrl *gomock.Controller) *MockBlockFetcher { + mock := &MockBlockFetcher{ctrl: ctrl} + mock.recorder = &MockBlockFetcherMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockBlockFetcher) EXPECT() *MockBlockFetcherMockRecorder { + return m.recorder +} + +// GetBlock mocks base method. +func (m *MockBlockFetcher) GetBlock(blockHash types.Hash) (*types.SignedBlock, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetBlock", blockHash) + ret0, _ := ret[0].(*types.SignedBlock) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetBlock indicates an expected call of GetBlock. +func (mr *MockBlockFetcherMockRecorder) GetBlock(blockHash interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBlock", reflect.TypeOf((*MockBlockFetcher)(nil).GetBlock), blockHash) +} + +// GetFinalizedHead mocks base method. +func (m *MockBlockFetcher) GetFinalizedHead() (types.Hash, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetFinalizedHead") + ret0, _ := ret[0].(types.Hash) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetFinalizedHead indicates an expected call of GetFinalizedHead. +func (mr *MockBlockFetcherMockRecorder) GetFinalizedHead() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFinalizedHead", reflect.TypeOf((*MockBlockFetcher)(nil).GetFinalizedHead)) +} + +// MockDepositProcessor is a mock of DepositProcessor interface. +type MockDepositProcessor struct { + ctrl *gomock.Controller + recorder *MockDepositProcessorMockRecorder +} + +// MockDepositProcessorMockRecorder is the mock recorder for MockDepositProcessor. +type MockDepositProcessorMockRecorder struct { + mock *MockDepositProcessor +} + +// NewMockDepositProcessor creates a new mock instance. +func NewMockDepositProcessor(ctrl *gomock.Controller) *MockDepositProcessor { + mock := &MockDepositProcessor{ctrl: ctrl} + mock.recorder = &MockDepositProcessorMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockDepositProcessor) EXPECT() *MockDepositProcessorMockRecorder { + return m.recorder +} + +// ProcessDeposits mocks base method. +func (m *MockDepositProcessor) ProcessDeposits(startBlock, endBlock *big.Int) (map[uint8][]*message.Message, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ProcessDeposits", startBlock, endBlock) + ret0, _ := ret[0].(map[uint8][]*message.Message) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ProcessDeposits indicates an expected call of ProcessDeposits. +func (mr *MockDepositProcessorMockRecorder) ProcessDeposits(startBlock, endBlock interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ProcessDeposits", reflect.TypeOf((*MockDepositProcessor)(nil).ProcessDeposits), startBlock, endBlock) +} diff --git a/chains/substrate/listener/event-handlers.go b/chains/substrate/listener/event-handlers.go index b79a61bf..c91f1a32 100644 --- a/chains/substrate/listener/event-handlers.go +++ b/chains/substrate/listener/event-handlers.go @@ -88,10 +88,24 @@ func NewFungibleTransferEventHandler(logC zerolog.Context, domainID uint8, depos } func (eh *FungibleTransferEventHandler) HandleEvents(startBlock *big.Int, endBlock *big.Int) error { + domainDeposits, err := eh.ProcessDeposits(startBlock, endBlock) + if err != nil { + return err + } + + for _, deposits := range domainDeposits { + go func(d []*message.Message) { + eh.msgChan <- d + }(deposits) + } + return nil +} + +func (eh *FungibleTransferEventHandler) ProcessDeposits(startBlock *big.Int, endBlock *big.Int) (map[uint8][]*message.Message, error) { evts, err := eh.conn.FetchEvents(startBlock, endBlock) if err != nil { log.Error().Err(err).Msg("Error fetching events") - return err + return nil, err } domainDeposits := make(map[uint8][]*message.Message) @@ -123,13 +137,7 @@ func (eh *FungibleTransferEventHandler) HandleEvents(startBlock *big.Int, endBlo }(*evt) } } - - for _, deposits := range domainDeposits { - go func(d []*message.Message) { - eh.msgChan <- d - }(deposits) - } - return nil + return domainDeposits, nil } type RetryEventHandler struct { diff --git a/e2e/btc/btc_test.go b/e2e/btc/btc_test.go index 52f9b8d5..945bc01f 100644 --- a/e2e/btc/btc_test.go +++ b/e2e/btc/btc_test.go @@ -51,7 +51,7 @@ type TestClient interface { func Test_EVMBtc(t *testing.T) { evmConfig := evm.DEFAULT_CONFIG - evmConfig.Erc20LockReleaseAddr = common.HexToAddress("0xd3Eb00fCE476aEFdC76A02F3531b7A0C6D5238B3") + evmConfig.Erc20LockReleaseAddr = common.HexToAddress("0xb61bd8740F60e0Bfc1b5C3fA2Bb9810e4AEf8938") evmConfig.Erc20LockReleaseResourceID = evm.SliceTo32Bytes(common.LeftPadBytes([]byte{0x10}, 31)) ethClient, err := evmClient.NewEVMClient(evm.ETHEndpoint1, evm.AdminAccount) diff --git a/e2e/evm/contracts/retry/retry.go b/e2e/evm/contracts/retry/retry.go new file mode 100644 index 00000000..2e03b069 --- /dev/null +++ b/e2e/evm/contracts/retry/retry.go @@ -0,0 +1,39 @@ +// The Licensed Work is (c) 2022 Sygma +// SPDX-License-Identifier: LGPL-3.0-only + +package retry + +import ( + "math/big" + "strings" + + "github.com/ChainSafe/sygma-relayer/chains/evm/calls/consts" + "github.com/sygmaprotocol/sygma-core/chains/evm/client" + "github.com/sygmaprotocol/sygma-core/chains/evm/contracts" + "github.com/sygmaprotocol/sygma-core/chains/evm/transactor" + + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" +) + +type RetryContract struct { + contracts.Contract +} + +func NewRetryContract( + client client.Client, + address common.Address, + transactor transactor.Transactor, +) *RetryContract { + a, _ := abi.JSON(strings.NewReader(consts.RetryABI)) + return &RetryContract{contracts.NewContract(address, a, nil, client, transactor)} +} + +func (c *RetryContract) Retry( + source uint8, + destination uint8, + block *big.Int, + resourceID [32]byte, + opts transactor.TransactOptions) (*common.Hash, error) { + return c.ExecuteTransaction("retry", opts, source, destination, block, resourceID) +} diff --git a/e2e/evm/evm_test.go b/e2e/evm/evm_test.go index 0f937792..a473a71e 100644 --- a/e2e/evm/evm_test.go +++ b/e2e/evm/evm_test.go @@ -12,7 +12,6 @@ import ( "time" substrateTypes "github.com/centrifuge/go-substrate-rpc-client/v4/types" - "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" @@ -20,10 +19,10 @@ import ( "github.com/rs/zerolog/log" "github.com/stretchr/testify/suite" - "github.com/ChainSafe/sygma-relayer/e2e/evm/contracts/centrifuge" "github.com/ChainSafe/sygma-relayer/e2e/evm/contracts/erc1155" "github.com/ChainSafe/sygma-relayer/e2e/evm/contracts/erc20" "github.com/ChainSafe/sygma-relayer/e2e/evm/contracts/erc721" + "github.com/ChainSafe/sygma-relayer/e2e/evm/contracts/retry" "github.com/ChainSafe/sygma-relayer/e2e/evm/keystore" "github.com/sygmaprotocol/sygma-core/chains/evm/client" @@ -137,7 +136,7 @@ func (s *IntegrationTestSuite) SetupSuite() { } erc20LRContract2 := erc20.NewERC20Contract(s.client2, s.config2.Erc20LockReleaseAddr, transactor2) - _, err = erc20LRContract2.MintTokens(s.config2.Erc20LockReleaseHandlerAddr, amountToMint.Mul(amountToMint, big.NewInt(10000)), transactor.TransactOptions{}) + _, err = erc20LRContract2.MintTokens(s.config2.Erc20LockReleaseHandlerAddr, amountToMint, transactor.TransactOptions{}) if err != nil { panic(err) } @@ -244,15 +243,12 @@ func (s *IntegrationTestSuite) Test_Erc721Deposit() { func (s *IntegrationTestSuite) Test_PermissionlessGenericDeposit() { transactor1 := signAndSend.NewSignAndSendTransactor(s.fabric1, s.gasPricer1, s.client1) - transactor2 := signAndSend.NewSignAndSendTransactor(s.fabric2, s.gasPricer2, s.client2) - bridgeContract1 := bridge.NewBridgeContract(s.client1, s.config1.BridgeAddr, transactor1) - assetStoreContract2 := centrifuge.NewAssetStoreContract(s.client2, s.config2.AssetStoreAddr, transactor2) byteArrayToHash, _ := substrateTypes.NewI64(int64(rand.Int())).MarshalJSON() hash := substrateTypes.NewHash(byteArrayToHash) functionSig := string(crypto.Keccak256([]byte("storeWithDepositor(address,bytes32,address)"))[:4]) - contractAddress := assetStoreContract2.ContractAddress() + contractAddress := s.client2.From() maxFee := big.NewInt(600000) depositor := s.client1.From() var metadata []byte @@ -265,15 +261,11 @@ func (s *IntegrationTestSuite) Test_PermissionlessGenericDeposit() { err = evm.WaitForProposalExecuted(s.client2, s.config2.BridgeAddr) s.Nil(err) - - exists, err := assetStoreContract2.IsCentrifugeAssetStored(hash) - s.Nil(err) - s.Equal(true, exists) } func (s *IntegrationTestSuite) Test_RetryDeposit() { dstAddr := keystore.TestKeyRing.EthereumKeys[keystore.BobKey].CommonAddress() - amountToMint := big.NewInt(0).Mul(big.NewInt(250), big.NewInt(0).Exp(big.NewInt(10), big.NewInt(18), nil)) + amountToMint := big.NewInt(0).Mul(big.NewInt(350), big.NewInt(0).Exp(big.NewInt(10), big.NewInt(18), nil)) transactor1 := signAndSend.NewSignAndSendTransactor(s.fabric1, s.gasPricer1, s.client1) bridgeContract1 := bridge.NewBridgeContract(s.client1, s.config1.BridgeAddr, transactor1) @@ -281,6 +273,8 @@ func (s *IntegrationTestSuite) Test_RetryDeposit() { transactor2 := signAndSend.NewSignAndSendTransactor(s.fabric2, s.gasPricer2, s.client2) erc20Contract2 := erc20.NewERC20Contract(s.client2, s.config2.Erc20LockReleaseAddr, transactor2) + retryContract := retry.NewRetryContract(s.client2, s.config2.RetryAddr, transactor2) + destBalanceBefore, err := erc20Contract2.GetBalance(dstAddr) s.Nil(err) @@ -291,21 +285,22 @@ func (s *IntegrationTestSuite) Test_RetryDeposit() { log.Debug().Msgf("deposit hash %s", depositTxHash.Hex()) - _, _, err = s.client1.TransactionByHash(context.Background(), *depositTxHash) + receipt, err := s.client1.TransactionReceipt(context.Background(), *depositTxHash) s.Nil(err) time.Sleep(time.Second * 15) - _, err = erc20Contract2.MintTokens(s.config2.Erc20HandlerAddr, amountToMint, transactor.TransactOptions{}) + _, err = erc20Contract2.MintTokens(s.config2.Erc20LockReleaseHandlerAddr, amountToMint, transactor.TransactOptions{}) if err != nil { return } - retryTxHash, err := bridgeContract1.Retry(*depositTxHash, transactor.TransactOptions{}) + retryTxHash, err := retryContract.Retry(1, 2, receipt.BlockNumber, s.config1.Erc20LockReleaseResourceID, transactor.TransactOptions{}) if err != nil { return } s.Nil(err) s.NotNil(retryTxHash) + time.Sleep(time.Second * 15) destBalanceAfter, err := erc20Contract2.GetBalance(dstAddr) @@ -341,7 +336,7 @@ func (s *IntegrationTestSuite) Test_MultipleDeposits() { }() } wg.Wait() - time.Sleep(30 * time.Second) + time.Sleep(10 * time.Second) err = evm.WaitForProposalExecuted(s.client2, s.config2.BridgeAddr) s.Nil(err) diff --git a/e2e/evm/util.go b/e2e/evm/util.go index 4dc76225..9a9765b0 100644 --- a/e2e/evm/util.go +++ b/e2e/evm/util.go @@ -49,6 +49,7 @@ type EVMClient interface { type BridgeConfig struct { BridgeAddr common.Address + RetryAddr common.Address Erc20Addr common.Address Erc20HandlerAddr common.Address @@ -58,12 +59,7 @@ type BridgeConfig struct { Erc20LockReleaseHandlerAddr common.Address Erc20LockReleaseResourceID [32]byte - GenericHandlerAddr common.Address - AssetStoreAddr common.Address - GenericResourceID [32]byte - - PermissionlessGenericHandlerAddr common.Address - PermissionlessGenericResourceID [32]byte + PermissionlessGenericResourceID [32]byte Erc721Addr common.Address Erc721HandlerAddr common.Address @@ -83,32 +79,28 @@ type BridgeConfig struct { var DEFAULT_CONFIG = BridgeConfig{ BridgeAddr: common.HexToAddress("0x6CdE2Cd82a4F8B74693Ff5e194c19CA08c2d1c68"), + RetryAddr: common.HexToAddress("0xAD825082B91980E7C8908652269c96a47D687cC5"), - Erc20Addr: common.HexToAddress("0x78E5b9cEC9aEA29071f070C8cC561F692B3511A6"), - Erc20HandlerAddr: common.HexToAddress("0x02091EefF969b33A5CE8A729DaE325879bf76f90"), + Erc20Addr: common.HexToAddress("0xA45E01c8D945D47ADa916828828B201d0815b83F"), + Erc20HandlerAddr: common.HexToAddress("0xC2D334e2f27A9dB2Ed8C4561De86C1A00EBf6760"), Erc20ResourceID: SliceTo32Bytes(common.LeftPadBytes([]byte{0}, 31)), - Erc20LockReleaseAddr: common.HexToAddress("0x1ED1d77911944622FCcDDEad8A731fd77E94173e"), - Erc20LockReleaseHandlerAddr: common.HexToAddress("0x02091EefF969b33A5CE8A729DaE325879bf76f90"), + Erc20LockReleaseAddr: common.HexToAddress("0x783BB8123b8532CC85C8D2deF2f47C55D1e46b46"), + Erc20LockReleaseHandlerAddr: common.HexToAddress("0xC2D334e2f27A9dB2Ed8C4561De86C1A00EBf6760"), Erc20LockReleaseResourceID: SliceTo32Bytes(common.LeftPadBytes([]byte{3}, 31)), - Erc721Addr: common.HexToAddress("0xa4640d1315Be1f88aC4F81546AA2C785cf247C31"), - Erc721HandlerAddr: common.HexToAddress("0xC2D334e2f27A9dB2Ed8C4561De86C1A00EBf6760"), + Erc721Addr: common.HexToAddress("0x67272Fa6DB7ADa1639736753eD99f9D0C9e1450D"), + Erc721HandlerAddr: common.HexToAddress("0xF28c11CB14C6d2B806f99EA8b138F65e74a1Ed66"), Erc721ResourceID: SliceTo32Bytes(common.LeftPadBytes([]byte{2}, 31)), - GenericHandlerAddr: common.HexToAddress("0xF956Ba663bd563f585e00D5973E06b443E5C4D65"), - GenericResourceID: SliceTo32Bytes(common.LeftPadBytes([]byte{1}, 31)), - AssetStoreAddr: common.HexToAddress("0xa2451c8553371E754F5e93A440aDcCa1c0DcF395"), - - PermissionlessGenericHandlerAddr: common.HexToAddress("0x156fA85e1df5d69B0F138dcEbAa5a14ca640FaED"), - PermissionlessGenericResourceID: SliceTo32Bytes(common.LeftPadBytes([]byte{5}, 31)), + PermissionlessGenericResourceID: SliceTo32Bytes(common.LeftPadBytes([]byte{5}, 31)), - Erc1155Addr: common.HexToAddress("0xFfd243c2C27e303e6d96aA4F7b3840Bf7209F0d7"), - Erc1155HandlerAddr: common.HexToAddress("0x9Fd58882b82EFaD2867f7eaB43539907bc07C360"), Erc1155ResourceID: SliceTo32Bytes(common.LeftPadBytes([]byte{4}, 31)), + Erc1155HandlerAddr: common.HexToAddress("0xE54Dc792c226AEF99D6086527b98b36a4ADDe56a"), + Erc1155Addr: common.HexToAddress("0x9Fd58882b82EFaD2867f7eaB43539907bc07C360"), - BasicFeeHandlerAddr: common.HexToAddress("0x1CcB4231f2ff299E1E049De76F0a1D2B415C563A"), - FeeRouterAddress: common.HexToAddress("0xF28c11CB14C6d2B806f99EA8b138F65e74a1Ed66"), + BasicFeeHandlerAddr: common.HexToAddress("0x8dA96a8C2b2d3e5ae7e668d0C94393aa8D5D3B94"), + FeeRouterAddress: common.HexToAddress("0x1CcB4231f2ff299E1E049De76F0a1D2B415C563A"), BasicFee: BasicFee, } diff --git a/example/app/app.go b/example/app/app.go index e1830926..d245bbeb 100644 --- a/example/app/app.go +++ b/example/app/app.go @@ -12,11 +12,13 @@ import ( "syscall" "time" + "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" substrateListener "github.com/ChainSafe/sygma-relayer/chains/substrate/listener" substratePallet "github.com/ChainSafe/sygma-relayer/chains/substrate/pallet" + "github.com/ChainSafe/sygma-relayer/relayer/retry" "github.com/ChainSafe/sygma-relayer/relayer/transfer" propStore "github.com/ChainSafe/sygma-relayer/store" "github.com/ChainSafe/sygma-relayer/tss" @@ -132,40 +134,32 @@ func Run() error { } msgChan := make(chan []*message.Message) - chains := make(map[uint8]relayer.RelayedChain) + domains := make(map[uint8]relayer.RelayedChain) for _, chainConfig := range configuration.ChainConfigs { switch chainConfig["type"] { case "evm": { config, err := evm.NewEVMConfig(chainConfig) - if err != nil { - panic(err) - } - + panicOnError(err) kp, err := secp256k1.NewKeypairFromString(config.GeneralChainConfig.Key) - if err != nil { - panic(err) - } + panicOnError(err) client, err := evmClient.NewEVMClient(config.GeneralChainConfig.Endpoint, kp) - if err != nil { - panic(err) - } + panicOnError(err) log.Info().Str("domain", config.String()).Msgf("Registering EVM domain") bridgeAddress := common.HexToAddress(config.Bridge) frostAddress := common.HexToAddress(config.FrostKeygen) - dummyGasPricer := gas.NewStaticGasPriceDeterminant(client, nil) - t := monitored.NewMonitoredTransactor( - *config.GeneralChainConfig.Id, transaction.NewTransaction, dummyGasPricer, sygmaMetrics, client, config.MaxGasPrice, config.GasIncreasePercentage) - go t.Monitor(ctx, time.Second*15, time.Minute*10, time.Minute) - + gasPricer := gas.NewLondonGasPriceClient(client, &gas.GasPricerOpts{ + UpperLimitFeePerGas: config.MaxGasPrice, + GasPriceFactor: config.GasMultiplier, + }) + t := monitored.NewMonitoredTransactor(*config.GeneralChainConfig.Id, transaction.NewTransaction, gasPricer, sygmaMetrics, client, config.MaxGasPrice, config.GasIncreasePercentage) + go t.Monitor(ctx, time.Minute*3, time.Minute*10, time.Minute) bridgeContract := bridge.NewBridgeContract(client, bridgeAddress, t) depositHandler := depositHandlers.NewETHDepositHandler(bridgeContract) - mh := message.NewMessageHandler() - mh.RegisterMessageHandler(transfer.TransferMessageType, &executor.TransferMessageHandler{}) for _, handler := range config.Handlers { switch handler.Type { case "erc20", "native": @@ -190,17 +184,41 @@ func Run() error { tssListener := events.NewListener(client) eventHandlers := make([]listener.EventHandler, 0) l := log.With().Str("chain", fmt.Sprintf("%v", config.GeneralChainConfig.Name)).Uint8("domainID", *config.GeneralChainConfig.Id) - eventHandlers = append(eventHandlers, hubEventHandlers.NewDepositEventHandler(depositListener, depositHandler, bridgeAddress, *config.GeneralChainConfig.Id, msgChan)) + + depositEventHandler := hubEventHandlers.NewDepositEventHandler(depositListener, depositHandler, bridgeAddress, *config.GeneralChainConfig.Id, msgChan) + eventHandlers = append(eventHandlers, depositEventHandler) eventHandlers = append(eventHandlers, hubEventHandlers.NewKeygenEventHandler(l, tssListener, coordinator, host, communication, keyshareStore, bridgeAddress, networkTopology.Threshold)) eventHandlers = append(eventHandlers, hubEventHandlers.NewFrostKeygenEventHandler(l, tssListener, coordinator, host, communication, frostKeyshareStore, frostAddress, networkTopology.Threshold)) eventHandlers = append(eventHandlers, hubEventHandlers.NewRefreshEventHandler(l, nil, nil, tssListener, coordinator, host, communication, connectionGate, keyshareStore, frostKeyshareStore, bridgeAddress)) - eventHandlers = append(eventHandlers, hubEventHandlers.NewRetryEventHandler(l, tssListener, depositHandler, propStore, bridgeAddress, *config.GeneralChainConfig.Id, config.BlockConfirmations, msgChan)) + eventHandlers = append(eventHandlers, hubEventHandlers.NewRetryV1EventHandler(l, tssListener, depositHandler, propStore, bridgeAddress, *config.GeneralChainConfig.Id, config.BlockConfirmations, msgChan)) + if config.Retry != "" { + eventHandlers = append(eventHandlers, hubEventHandlers.NewRetryV2EventHandler(l, tssListener, common.HexToAddress(config.Retry), *config.GeneralChainConfig.Id, msgChan)) + } evmListener := listener.NewEVMListener(client, eventHandlers, blockstore, sygmaMetrics, *config.GeneralChainConfig.Id, config.BlockRetryInterval, config.BlockConfirmations, config.BlockInterval) + + mh := message.NewMessageHandler() + mh.RegisterMessageHandler(retry.RetryMessageType, executor.NewRetryMessageHandler(depositEventHandler, client, propStore, config.BlockConfirmations, msgChan)) + mh.RegisterMessageHandler(transfer.TransferMessageType, &executor.TransferMessageHandler{}) executor := executor.NewExecutor(host, communication, coordinator, bridgeContract, keyshareStore, exitLock, config.GasLimit.Uint64(), config.TransferGas) - chain := coreEvm.NewEVMChain(evmListener, mh, executor, *config.GeneralChainConfig.Id, config.StartBlock) + startBlock, err := blockstore.GetStartBlock(*config.GeneralChainConfig.Id, config.StartBlock, config.GeneralChainConfig.LatestBlock, config.GeneralChainConfig.FreshStart) + if err != nil { + panic(err) + } + if startBlock == nil { + head, err := client.LatestBlock() + if err != nil { + panic(err) + } + startBlock = head + } + startBlock, err = chains.CalculateStartingBlock(startBlock, config.BlockInterval) + if err != nil { + panic(err) + } + chain := coreEvm.NewEVMChain(evmListener, mh, executor, *config.GeneralChainConfig.Id, startBlock) - chains[*config.GeneralChainConfig.Id] = chain + domains[*config.GeneralChainConfig.Id] = chain } case "substrate": { @@ -213,38 +231,53 @@ func Run() error { if err != nil { panic(err) } - keyPair, err := signature.KeyringPairFromSecret(config.GeneralChainConfig.Key, config.SubstrateNetwork) if err != nil { panic(err) } - log.Info().Str("domain", config.String()).Msgf("Registering substrate domain") - substrateClient := substrateClient.NewSubstrateClient(conn, &keyPair, config.ChainID, config.Tip) bridgePallet := substratePallet.NewPallet(substrateClient) + log.Info().Str("domain", config.String()).Msgf("Registering substrate domain") + l := log.With().Str("chain", fmt.Sprintf("%v", config.GeneralChainConfig.Name)).Uint8("domainID", *config.GeneralChainConfig.Id) depositHandler := substrateListener.NewSubstrateDepositHandler() depositHandler.RegisterDepositHandler(transfer.FungibleTransfer, substrateListener.FungibleTransferHandler) eventHandlers := make([]coreSubstrateListener.EventHandler, 0) - eventHandlers = append(eventHandlers, substrateListener.NewFungibleTransferEventHandler(l, *config.GeneralChainConfig.Id, depositHandler, msgChan, conn)) + depositEventHandler := substrateListener.NewFungibleTransferEventHandler(l, *config.GeneralChainConfig.Id, depositHandler, msgChan, conn) eventHandlers = append(eventHandlers, substrateListener.NewRetryEventHandler(l, conn, depositHandler, *config.GeneralChainConfig.Id, msgChan)) - + eventHandlers = append(eventHandlers, depositEventHandler) substrateListener := coreSubstrateListener.NewSubstrateListener(conn, eventHandlers, blockstore, sygmaMetrics, *config.GeneralChainConfig.Id, config.BlockRetryInterval, config.BlockInterval) mh := message.NewMessageHandler() mh.RegisterMessageHandler(transfer.TransferMessageType, &substrateExecutor.SubstrateMessageHandler{}) + mh.RegisterMessageHandler(retry.RetryMessageType, substrateExecutor.NewRetryMessageHandler(depositEventHandler, conn, propStore, msgChan)) sExecutor := substrateExecutor.NewExecutor(host, communication, coordinator, bridgePallet, keyshareStore, conn, exitLock) - substrateChain := coreSubstrate.NewSubstrateChain(substrateListener, mh, sExecutor, *config.GeneralChainConfig.Id, config.StartBlock) - chains[*config.GeneralChainConfig.Id] = substrateChain + + startBlock, err := blockstore.GetStartBlock(*config.GeneralChainConfig.Id, config.StartBlock, config.GeneralChainConfig.LatestBlock, config.GeneralChainConfig.FreshStart) + if err != nil { + panic(err) + } + if startBlock == nil { + head, err := substrateClient.LatestBlock() + if err != nil { + panic(err) + } + startBlock = head + } + startBlock, err = chains.CalculateStartingBlock(startBlock, config.BlockInterval) + if err != nil { + panic(err) + } + substrateChain := coreSubstrate.NewSubstrateChain(substrateListener, mh, sExecutor, *config.GeneralChainConfig.Id, startBlock) + + domains[*config.GeneralChainConfig.Id] = substrateChain } case "btc": { log.Info().Msgf("Registering btc domain") - time.Sleep(time.Second * 5) - config, err := btcConfig.NewBtcConfig(chainConfig) if err != nil { panic(err) @@ -260,17 +293,21 @@ func Run() error { } l := log.With().Str("chain", fmt.Sprintf("%v", config.GeneralChainConfig.Name)).Uint8("domainID", *config.GeneralChainConfig.Id) - depositHandler := &btcListener.BtcDepositHandler{} - eventHandlers := make([]btcListener.EventHandler, 0) resources := make(map[[32]byte]btcConfig.Resource) for _, resource := range config.Resources { resources[resource.ResourceID] = resource - eventHandlers = append(eventHandlers, btcListener.NewFungibleTransferEventHandler(l, *config.GeneralChainConfig.Id, depositHandler, msgChan, conn, resource, config.FeeAddress)) } + depositHandler := &btcListener.BtcDepositHandler{} + depositEventHandler := btcListener.NewFungibleTransferEventHandler(l, *config.GeneralChainConfig.Id, depositHandler, msgChan, conn, resources, config.FeeAddress) + eventHandlers := make([]btcListener.EventHandler, 0) + eventHandlers = append(eventHandlers, depositEventHandler) listener := btcListener.NewBtcListener(conn, eventHandlers, config, blockstore) mempool := mempool.NewMempoolAPI(config.MempoolUrl) - mh := &btcExecutor.BtcMessageHandler{} + + mh := message.NewMessageHandler() + mh.RegisterMessageHandler(transfer.TransferMessageType, &btcExecutor.FungibleMessageHandler{}) + mh.RegisterMessageHandler(retry.RetryMessageType, btcExecutor.NewRetryMessageHandler(depositEventHandler, conn, config.BlockConfirmations, propStore, msgChan)) uploader := uploader.NewIPFSUploader(configuration.RelayerConfig.UploaderConfig) executor := btcExecutor.NewExecutor( propStore, @@ -286,7 +323,7 @@ func Run() error { uploader) btcChain := btc.NewBtcChain(listener, executor, mh, *config.GeneralChainConfig.Id) - chains[*config.GeneralChainConfig.Id] = btcChain + domains[*config.GeneralChainConfig.Id] = btcChain } default: @@ -295,7 +332,7 @@ func Run() error { } go jobs.StartCommunicationHealthCheckJob(host, configuration.RelayerConfig.MpcConfig.CommHealthCheckInterval, sygmaMetrics) - r := relayer.NewRelayer(chains, sygmaMetrics) + r := relayer.NewRelayer(domains, sygmaMetrics) go r.Start(ctx, msgChan) @@ -311,3 +348,9 @@ func Run() error { return nil } + +func panicOnError(err error) { + if err != nil { + panic(err) + } +} diff --git a/example/cfg/config_evm-evm_1.json b/example/cfg/config_evm-evm_1.json index f3627161..dfe1b3c6 100644 --- a/example/cfg/config_evm-evm_1.json +++ b/example/cfg/config_evm-evm_1.json @@ -24,22 +24,23 @@ "type": "evm", "endpoint": "ws://evm1-1:8545", "bridge": "0x6CdE2Cd82a4F8B74693Ff5e194c19CA08c2d1c68", + "retry": "0xAD825082B91980E7C8908652269c96a47D687cC5", "handlers": [ { "type": "erc20", - "address": "0x02091EefF969b33A5CE8A729DaE325879bf76f90" + "address": "0xC2D334e2f27A9dB2Ed8C4561De86C1A00EBf6760" }, { "type": "erc721", - "address": "0xC2D334e2f27A9dB2Ed8C4561De86C1A00EBf6760" + "address": "0xF28c11CB14C6d2B806f99EA8b138F65e74a1Ed66" }, { "type": "erc1155", - "address": "0x9Fd58882b82EFaD2867f7eaB43539907bc07C360" + "address": "0xE54Dc792c226AEF99D6086527b98b36a4ADDe56a" }, { "type": "permissionlessGeneric", - "address": "0x156fA85e1df5d69B0F138dcEbAa5a14ca640FaED" + "address": "0x94e9070f0354b69E7FB583134A820949A390D341" } ], "gasLimit": 9000000, @@ -53,22 +54,23 @@ "type": "evm", "endpoint": "ws://evm2-1:8545", "bridge": "0x6CdE2Cd82a4F8B74693Ff5e194c19CA08c2d1c68", + "retry": "0xAD825082B91980E7C8908652269c96a47D687cC5", "handlers": [ { "type": "erc20", - "address": "0x02091EefF969b33A5CE8A729DaE325879bf76f90" + "address": "0xC2D334e2f27A9dB2Ed8C4561De86C1A00EBf6760" }, { "type": "erc721", - "address": "0xC2D334e2f27A9dB2Ed8C4561De86C1A00EBf6760" + "address": "0xF28c11CB14C6d2B806f99EA8b138F65e74a1Ed66" }, { "type": "erc1155", - "address": "0x9Fd58882b82EFaD2867f7eaB43539907bc07C360" + "address": "0xE54Dc792c226AEF99D6086527b98b36a4ADDe56a" }, { "type": "permissionlessGeneric", - "address": "0x156fA85e1df5d69B0F138dcEbAa5a14ca640FaED" + "address": "0x94e9070f0354b69E7FB583134A820949A390D341" } ], "gasLimit": 9000000, diff --git a/example/cfg/config_evm-evm_2.json b/example/cfg/config_evm-evm_2.json index f3800914..5e492bae 100644 --- a/example/cfg/config_evm-evm_2.json +++ b/example/cfg/config_evm-evm_2.json @@ -24,22 +24,23 @@ "type": "evm", "endpoint": "ws://evm1-1:8545", "bridge": "0x6CdE2Cd82a4F8B74693Ff5e194c19CA08c2d1c68", + "retry": "0xAD825082B91980E7C8908652269c96a47D687cC5", "handlers": [ { "type": "erc20", - "address": "0x02091EefF969b33A5CE8A729DaE325879bf76f90" + "address": "0xC2D334e2f27A9dB2Ed8C4561De86C1A00EBf6760" }, { "type": "erc721", - "address": "0xC2D334e2f27A9dB2Ed8C4561De86C1A00EBf6760" + "address": "0xF28c11CB14C6d2B806f99EA8b138F65e74a1Ed66" }, { "type": "erc1155", - "address": "0x9Fd58882b82EFaD2867f7eaB43539907bc07C360" + "address": "0xE54Dc792c226AEF99D6086527b98b36a4ADDe56a" }, { "type": "permissionlessGeneric", - "address": "0x156fA85e1df5d69B0F138dcEbAa5a14ca640FaED" + "address": "0x94e9070f0354b69E7FB583134A820949A390D341" } ], "gasLimit": 9000000, @@ -53,22 +54,23 @@ "type": "evm", "endpoint": "ws://evm2-1:8545", "bridge": "0x6CdE2Cd82a4F8B74693Ff5e194c19CA08c2d1c68", + "retry": "0xAD825082B91980E7C8908652269c96a47D687cC5", "handlers": [ { "type": "erc20", - "address": "0x02091EefF969b33A5CE8A729DaE325879bf76f90" + "address": "0xC2D334e2f27A9dB2Ed8C4561De86C1A00EBf6760" }, { "type": "erc721", - "address": "0xC2D334e2f27A9dB2Ed8C4561De86C1A00EBf6760" + "address": "0xF28c11CB14C6d2B806f99EA8b138F65e74a1Ed66" }, { "type": "erc1155", - "address": "0x9Fd58882b82EFaD2867f7eaB43539907bc07C360" + "address": "0xE54Dc792c226AEF99D6086527b98b36a4ADDe56a" }, { "type": "permissionlessGeneric", - "address": "0x156fA85e1df5d69B0F138dcEbAa5a14ca640FaED" + "address": "0x94e9070f0354b69E7FB583134A820949A390D341" } ], "gasLimit": 9000000, diff --git a/example/cfg/config_evm-evm_3.json b/example/cfg/config_evm-evm_3.json index 2bfe1b2d..97a0659a 100644 --- a/example/cfg/config_evm-evm_3.json +++ b/example/cfg/config_evm-evm_3.json @@ -24,22 +24,23 @@ "type": "evm", "endpoint": "ws://evm1-1:8545", "bridge": "0x6CdE2Cd82a4F8B74693Ff5e194c19CA08c2d1c68", + "retry": "0xAD825082B91980E7C8908652269c96a47D687cC5", "handlers": [ { "type": "erc20", - "address": "0x02091EefF969b33A5CE8A729DaE325879bf76f90" + "address": "0xC2D334e2f27A9dB2Ed8C4561De86C1A00EBf6760" }, { "type": "erc721", - "address": "0xC2D334e2f27A9dB2Ed8C4561De86C1A00EBf6760" + "address": "0xF28c11CB14C6d2B806f99EA8b138F65e74a1Ed66" }, { "type": "erc1155", - "address": "0x9Fd58882b82EFaD2867f7eaB43539907bc07C360" + "address": "0xE54Dc792c226AEF99D6086527b98b36a4ADDe56a" }, { "type": "permissionlessGeneric", - "address": "0x156fA85e1df5d69B0F138dcEbAa5a14ca640FaED" + "address": "0x94e9070f0354b69E7FB583134A820949A390D341" } ], "gasLimit": 9000000, @@ -53,22 +54,23 @@ "type": "evm", "endpoint": "ws://evm2-1:8545", "bridge": "0x6CdE2Cd82a4F8B74693Ff5e194c19CA08c2d1c68", + "retry": "0xAD825082B91980E7C8908652269c96a47D687cC5", "handlers": [ { "type": "erc20", - "address": "0x02091EefF969b33A5CE8A729DaE325879bf76f90" + "address": "0xC2D334e2f27A9dB2Ed8C4561De86C1A00EBf6760" }, { "type": "erc721", - "address": "0xC2D334e2f27A9dB2Ed8C4561De86C1A00EBf6760" + "address": "0xF28c11CB14C6d2B806f99EA8b138F65e74a1Ed66" }, { "type": "erc1155", - "address": "0x9Fd58882b82EFaD2867f7eaB43539907bc07C360" + "address": "0xE54Dc792c226AEF99D6086527b98b36a4ADDe56a" }, { "type": "permissionlessGeneric", - "address": "0x156fA85e1df5d69B0F138dcEbAa5a14ca640FaED" + "address": "0x94e9070f0354b69E7FB583134A820949A390D341" } ], "gasLimit": 9000000, diff --git a/example/docker-compose.yml b/example/docker-compose.yml index 9658c976..2f228bc4 100644 --- a/example/docker-compose.yml +++ b/example/docker-compose.yml @@ -29,17 +29,17 @@ services: entrypoint: /cfg/entrypoint/entrypoint.sh evm1-1: - image: ghcr.io/sygmaprotocol/sygma-solidity:evm1-v2.7.0 + image: ghcr.io/sygmaprotocol/sygma-solidity:evm1-v2.10.1 container_name: evm1-1 - command: ganache-cli --chainId 1337 -d --db data/ --blockTime 2 > /dev/null + command: --chain.chainId 1337 --db data/ --blockTime 2 --m 'black toward wish jar twin produce remember fluid always confirm bacon slush' > /dev/null logging: driver: none ports: - "8545:8545" evm2-1: - image: ghcr.io/sygmaprotocol/sygma-solidity:evm2-v2.7.0 - command: ganache-cli --chainId 1338 -d --db data/ --blockTime 2 > /dev/null + image: ghcr.io/sygmaprotocol/sygma-solidity:evm2-v2.10.1 + command: --chain.chainId 1338 --db data/ --blockTime 2 --m 'black toward wish jar twin produce remember fluid always confirm bacon slush' > /dev/null container_name: evm2-1 logging: driver: none diff --git a/relayer/retry/retry.go b/relayer/retry/retry.go new file mode 100644 index 00000000..3634e7b9 --- /dev/null +++ b/relayer/retry/retry.go @@ -0,0 +1,89 @@ +// The Licensed Work is (c) 2022 Sygma +// SPDX-License-Identifier: LGPL-3.0-only + +package retry + +import ( + "math/big" + + "github.com/ChainSafe/sygma-relayer/relayer/transfer" + "github.com/ChainSafe/sygma-relayer/store" + "github.com/rs/zerolog/log" + "github.com/sygmaprotocol/sygma-core/relayer/message" +) + +const ( + RetryMessageType message.MessageType = "RetryMessage" +) + +type RetryMessageData struct { + SourceDomainID uint8 + DestinationDomainID uint8 + BlockHeight *big.Int + ResourceID [32]byte +} + +type PropStorer interface { + StorePropStatus(source, destination uint8, depositNonce uint64, status store.PropStatus) error + PropStatus(source, destination uint8, depositNonce uint64) (store.PropStatus, error) +} + +// FilterDeposits filters deposits per domain and resource +// that are to be retried. +func FilterDeposits( + propStorer PropStorer, + domainDeposits map[uint8][]*message.Message, + resourceID [32]byte, + destination uint8) ([]*message.Message, error) { + filteredDeposits := make([]*message.Message, 0) + for domain, deposits := range domainDeposits { + if domain != destination { + continue + } + + for _, deposit := range deposits { + data := deposit.Data.(transfer.TransferMessageData) + if data.ResourceId != resourceID { + continue + } + + isExecuted, err := isExecuted(deposit, propStorer) + if err != nil { + log.Err(err).Str("messageID", deposit.ID).Msgf("Failed checking if deposit executed %+v", deposit) + continue + } + if isExecuted { + log.Debug().Str("messageID", deposit.ID).Msgf("Deposit marked as executed %+v", deposit) + continue + } + + filteredDeposits = append(filteredDeposits, deposit) + } + } + return filteredDeposits, nil +} + +func isExecuted(msg *message.Message, propStorer PropStorer) (bool, error) { + var err error + propStatus, err := propStorer.PropStatus( + msg.Source, + msg.Destination, + msg.Data.(transfer.TransferMessageData).DepositNonce) + if err != nil { + return true, err + } + + if propStatus == store.ExecutedProp { + return true, nil + } + + // change the status to failed if proposal is stuck to be able to retry it + if propStatus == store.PendingProp { + err = propStorer.StorePropStatus( + msg.Source, + msg.Destination, + msg.Data.(transfer.TransferMessageData).DepositNonce, + store.FailedProp) + } + return false, err +} diff --git a/relayer/retry/retry_test.go b/relayer/retry/retry_test.go new file mode 100644 index 00000000..eda2a0fa --- /dev/null +++ b/relayer/retry/retry_test.go @@ -0,0 +1,149 @@ +package retry_test + +import ( + "fmt" + "testing" + + mock_executor "github.com/ChainSafe/sygma-relayer/chains/btc/executor/mock" + "github.com/ChainSafe/sygma-relayer/e2e/evm" + "github.com/ChainSafe/sygma-relayer/relayer/retry" + "github.com/ChainSafe/sygma-relayer/relayer/transfer" + "github.com/ChainSafe/sygma-relayer/store" + "github.com/ethereum/go-ethereum/common" + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/suite" + "github.com/sygmaprotocol/sygma-core/relayer/message" +) + +type FilterDepositsTestSuite struct { + suite.Suite + + mockPropStorer *mock_executor.MockPropStorer +} + +func TestRunRetryMessageHandlerTestSuite(t *testing.T) { + suite.Run(t, new(FilterDepositsTestSuite)) +} + +func (s *FilterDepositsTestSuite) SetupTest() { + ctrl := gomock.NewController(s.T()) + s.mockPropStorer = mock_executor.NewMockPropStorer(ctrl) +} + +func (s *FilterDepositsTestSuite) Test_NoValidDeposits() { + validResource := evm.SliceTo32Bytes(common.LeftPadBytes([]byte{3}, 31)) + invalidResource := evm.SliceTo32Bytes(common.LeftPadBytes([]byte{4}, 31)) + invalidDomain := uint8(3) + validDomain := uint8(4) + + deposits := make(map[uint8][]*message.Message) + deposits[validDomain] = []*message.Message{ + { + Source: invalidDomain, + Destination: validDomain, + Data: transfer.TransferMessageData{ + DepositNonce: 2, + ResourceId: invalidResource, + }, + }, + } + + d, err := retry.FilterDeposits(s.mockPropStorer, deposits, validResource, validDomain) + + expectedDeposits := make([]*message.Message, 0) + s.Nil(err) + s.Equal(d, expectedDeposits) +} + +func (s *FilterDepositsTestSuite) Test_FilterDeposits() { + validResource := evm.SliceTo32Bytes(common.LeftPadBytes([]byte{3}, 31)) + invalidResource := evm.SliceTo32Bytes(common.LeftPadBytes([]byte{4}, 31)) + invalidDomain := uint8(3) + validDomain := uint8(4) + + executedNonce := uint64(1) + failedNonce := uint64(3) + pendingNonce := uint64(4) + failedExecutionCheckNonce := uint64(5) + + deposits := make(map[uint8][]*message.Message) + deposits[invalidDomain] = []*message.Message{ + { + Destination: invalidDomain, + Data: transfer.TransferMessageData{ + DepositNonce: 1, + ResourceId: validResource, + }, + }, + } + deposits[validDomain] = []*message.Message{ + { + Source: invalidDomain, + Destination: validDomain, + Data: transfer.TransferMessageData{ + DepositNonce: executedNonce, + ResourceId: validResource, + }, + }, + { + Source: invalidDomain, + Destination: validDomain, + Data: transfer.TransferMessageData{ + DepositNonce: 2, + ResourceId: invalidResource, + }, + }, + { + Source: invalidDomain, + Destination: validDomain, + Data: transfer.TransferMessageData{ + DepositNonce: failedNonce, + ResourceId: validResource, + }, + }, + { + Source: invalidDomain, + Destination: validDomain, + Data: transfer.TransferMessageData{ + DepositNonce: failedExecutionCheckNonce, + ResourceId: validResource, + }, + }, + { + Source: invalidDomain, + Destination: validDomain, + Data: transfer.TransferMessageData{ + DepositNonce: pendingNonce, + ResourceId: validResource, + }, + }, + } + s.mockPropStorer.EXPECT().PropStatus(invalidDomain, validDomain, executedNonce).Return(store.ExecutedProp, nil) + s.mockPropStorer.EXPECT().PropStatus(invalidDomain, validDomain, failedNonce).Return(store.FailedProp, nil) + s.mockPropStorer.EXPECT().PropStatus(invalidDomain, validDomain, pendingNonce).Return(store.PendingProp, nil) + s.mockPropStorer.EXPECT().PropStatus(invalidDomain, validDomain, failedExecutionCheckNonce).Return(store.PendingProp, fmt.Errorf("error")) + s.mockPropStorer.EXPECT().StorePropStatus(invalidDomain, validDomain, pendingNonce, store.FailedProp).Return(nil) + + d, err := retry.FilterDeposits(s.mockPropStorer, deposits, validResource, validDomain) + + expectedDeposits := []*message.Message{ + { + Source: invalidDomain, + Destination: validDomain, + Data: transfer.TransferMessageData{ + DepositNonce: failedNonce, + ResourceId: validResource, + }, + }, + { + Source: invalidDomain, + Destination: validDomain, + Data: transfer.TransferMessageData{ + DepositNonce: pendingNonce, + ResourceId: validResource, + }, + }, + } + s.Nil(err) + s.Equal(d, expectedDeposits) +}