diff --git a/app/app.go b/app/app.go index ceca1538..7e65b8e2 100644 --- a/app/app.go +++ b/app/app.go @@ -224,8 +224,8 @@ func Run() error { 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, bridgeAddress)) - eventHandlers = append(eventHandlers, hubEventHandlers.NewRetryEventHandler(l, tssListener, depositHandler, propStore, bridgeAddress, *config.GeneralChainConfig.Id, config.BlockConfirmations, msgChan)) + eventHandlers = append(eventHandlers, hubEventHandlers.NewRefreshEventHandler(l, topologyProvider, topologyStore, tssListener, coordinator, host, communication, connectionGate, keyshareStore, frostKeyshareStore, bridgeAddress)) + eventHandlers = append(eventHandlers, hubEventHandlers.NewRetryEventHandler(l, tssListener, depositHandler, bridgeAddress, *config.GeneralChainConfig.Id, config.BlockConfirmations, msgChan)) evmListener := listener.NewEVMListener(client, eventHandlers, blockstore, sygmaMetrics, *config.GeneralChainConfig.Id, config.BlockRetryInterval, config.BlockConfirmations, config.BlockInterval) executor := executor.NewExecutor(host, communication, coordinator, bridgeContract, keyshareStore, exitLock, config.GasLimit.Uint64()) @@ -325,7 +325,7 @@ func Run() error { 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)) + eventHandlers = append(eventHandlers, btcListener.NewFungibleTransferEventHandler(l, *config.GeneralChainConfig.Id, depositHandler, msgChan, conn, resource, config.FeeAddress)) } listener := btcListener.NewBtcListener(conn, eventHandlers, config, blockstore) diff --git a/chains/btc/config/config.go b/chains/btc/config/config.go index c43abc9b..6131a06c 100644 --- a/chains/btc/config/config.go +++ b/chains/btc/config/config.go @@ -19,12 +19,14 @@ import ( type RawResource struct { Address string ResourceID string + FeeAmount string Tweak string Script string } type Resource struct { Address btcutil.Address + FeeAmount *big.Int ResourceID [32]byte Tweak string Script []byte @@ -34,6 +36,7 @@ type RawBtcConfig struct { chain.GeneralChainConfig `mapstructure:",squash"` Resources []RawResource `mapstrcture:"resources"` StartBlock int64 `mapstructure:"startBlock"` + FeeAddress string `mapstructure:"feeAddress"` Username string `mapstructure:"username"` Password string `mapstructure:"password"` BlockInterval int64 `mapstructure:"blockInterval" default:"5"` @@ -48,7 +51,7 @@ func (c *RawBtcConfig) Validate() error { return err } - if c.BlockConfirmations != 0 && c.BlockConfirmations < 1 { + if c.BlockConfirmations < 1 { return fmt.Errorf("blockConfirmations has to be >=1") } @@ -65,6 +68,7 @@ func (c *RawBtcConfig) Validate() error { type BtcConfig struct { GeneralChainConfig chain.GeneralChainConfig Resources []Resource + FeeAddress btcutil.Address Username string Password string StartBlock *big.Int @@ -100,7 +104,10 @@ func NewBtcConfig(chainConfig map[string]interface{}) (*BtcConfig, error) { if err != nil { return nil, err } - + feeAddress, err := btcutil.DecodeAddress(c.FeeAddress, &networkParams) + if err != nil { + return nil, err + } resources := make([]Resource, len(c.Resources)) for i, r := range c.Resources { scriptBytes, err := hex.DecodeString(r.Script) @@ -108,6 +115,11 @@ func NewBtcConfig(chainConfig map[string]interface{}) (*BtcConfig, error) { return nil, err } + feeAmount, success := new(big.Int).SetString(r.FeeAmount, 10) + if !success { + return nil, fmt.Errorf("error: could not convert string to *big.Int") + } + address, err := btcutil.DecodeAddress(r.Address, &networkParams) if err != nil { return nil, err @@ -123,6 +135,7 @@ func NewBtcConfig(chainConfig map[string]interface{}) (*BtcConfig, error) { ResourceID: resource32Bytes, Script: scriptBytes, Tweak: r.Tweak, + FeeAmount: feeAmount, } } @@ -137,6 +150,7 @@ func NewBtcConfig(chainConfig map[string]interface{}) (*BtcConfig, error) { Password: c.Password, Network: networkParams, MempoolUrl: c.MempoolUrl, + FeeAddress: feeAddress, Resources: resources, } return config, nil diff --git a/chains/btc/config/config_test.go b/chains/btc/config/config_test.go index 2dfaf0df..78512453 100644 --- a/chains/btc/config/config_test.go +++ b/chains/btc/config/config_test.go @@ -93,18 +93,21 @@ func (s *NewBtcConfigTestSuite) Test_InvalidPassword() { func (s *NewBtcConfigTestSuite) Test_ValidConfig() { expectedResource := listener.SliceTo32Bytes(common.LeftPadBytes([]byte{3}, 31)) expectedAddress, _ := btcutil.DecodeAddress("tb1qln69zuhdunc9stwfh6t7adexxrcr04ppy6thgm", &chaincfg.TestNet3Params) + feeAddress, _ := btcutil.DecodeAddress("mkHS9ne12qx9pS9VojpwU5xtRd4T7X7ZUt", &chaincfg.TestNet3Params) expectedScript, _ := hex.DecodeString("51206a698882348433b57d549d6344f74500fcd13ad8d2200cdf89f8e39e5cafa7d5") rawConfig := map[string]interface{}{ - "id": 1, - "endpoint": "ws://domain.com", - "name": "btc1", - "username": "username", - "password": "pass123", - "network": "testnet", + "id": 1, + "endpoint": "ws://domain.com", + "name": "btc1", + "username": "username", + "password": "pass123", + "network": "testnet", + "feeAddress": "mkHS9ne12qx9pS9VojpwU5xtRd4T7X7ZUt", "resources": []interface{}{ config.RawResource{ Address: "tb1qln69zuhdunc9stwfh6t7adexxrcr04ppy6thgm", + FeeAmount: "10000000", ResourceID: "0x0000000000000000000000000000000000000000000000000000000000000300", Script: "51206a698882348433b57d549d6344f74500fcd13ad8d2200cdf89f8e39e5cafa7d5", Tweak: "tweak", @@ -130,12 +133,14 @@ func (s *NewBtcConfigTestSuite) Test_ValidConfig() { BlockInterval: big.NewInt(5), BlockRetryInterval: time.Duration(5) * time.Second, Network: chaincfg.TestNet3Params, + FeeAddress: feeAddress, Resources: []config.Resource{ { Address: expectedAddress, ResourceID: expectedResource, Script: expectedScript, Tweak: "tweak", + FeeAmount: big.NewInt(10000000), }, }, }) diff --git a/chains/btc/executor/executor.go b/chains/btc/executor/executor.go index 591da385..7a72a852 100644 --- a/chains/btc/executor/executor.go +++ b/chains/btc/executor/executor.go @@ -101,10 +101,32 @@ func (e *Executor) Execute(proposals []*proposal.Proposal) error { if len(props) == 0 { return nil } - resource, ok := e.resources[props[0].Data.ResourceId] - if !ok { - return fmt.Errorf("no address for resource") + + propsPerResource := make(map[[32]byte][]*BtcTransferProposal) + for _, prop := range props { + propsPerResource[prop.Data.ResourceId] = append(propsPerResource[prop.Data.ResourceId], prop) + } + + p := pool.New().WithErrors() + for resourceID, props := range propsPerResource { + resourceID := resourceID + props := props + + p.Go(func() error { + resource, ok := e.resources[resourceID] + if !ok { + return fmt.Errorf("no resource for ID %s", hex.EncodeToString(resourceID[:])) + } + + sessionID := fmt.Sprintf("%s-%s", sessionID, hex.EncodeToString(resourceID[:])) + return e.executeResourceProps(props, resource, sessionID) + }) } + return p.Wait() +} + +func (e *Executor) executeResourceProps(props []*BtcTransferProposal, resource config.Resource, sessionID string) error { + log.Info().Str("SessionID", sessionID).Msgf("Executing proposals for resource %s", hex.EncodeToString(resource.ResourceID[:])) tx, utxos, err := e.rawTx(props, resource) if err != nil { @@ -203,7 +225,11 @@ func (e *Executor) rawTx(proposals []*BtcTransferProposal, resource config.Resou if err != nil { return nil, nil, err } - inputAmount, utxos, err := e.inputs(tx, resource.Address, outputAmount) + feeEstimate, err := e.fee(int64(len(proposals)), int64(len(proposals))) + if err != nil { + return nil, nil, err + } + inputAmount, utxos, err := e.inputs(tx, resource.Address, outputAmount+feeEstimate) if err != nil { return nil, nil, err } @@ -345,6 +371,7 @@ func (e *Executor) isExecuted(prop *proposal.Proposal) (bool, error) { } func (e *Executor) storeProposalsStatus(props []*BtcTransferProposal, status store.PropStatus) { + e.propMutex.Lock() for _, prop := range props { err := e.propStorer.StorePropStatus( prop.Source, @@ -355,4 +382,5 @@ func (e *Executor) storeProposalsStatus(props []*BtcTransferProposal, status sto log.Err(err).Msgf("Failed storing proposal %+v status %s", prop, status) } } + e.propMutex.Unlock() } diff --git a/chains/btc/listener/event-handlers.go b/chains/btc/listener/event-handlers.go index 925c0ebc..08745d66 100644 --- a/chains/btc/listener/event-handlers.go +++ b/chains/btc/listener/event-handlers.go @@ -4,18 +4,20 @@ package listener import ( + "crypto/sha256" + "encoding/binary" "math/big" - "strconv" "github.com/ChainSafe/sygma-relayer/chains/btc/config" "github.com/btcsuite/btcd/btcjson" + "github.com/btcsuite/btcd/btcutil" "github.com/rs/zerolog" "github.com/rs/zerolog/log" "github.com/sygmaprotocol/sygma-core/relayer/message" ) type Deposit struct { - // ResourceID used to find address of handler to be used for deposit + // ID of the resource that is transfered ResourceID [32]byte // Address of sender (msg.sender: user) SenderAddress string @@ -38,16 +40,18 @@ type DepositHandler interface { type FungibleTransferEventHandler struct { depositHandler DepositHandler domainID uint8 + feeAddress btcutil.Address log zerolog.Logger conn Connection msgChan chan []*message.Message resource config.Resource } -func NewFungibleTransferEventHandler(logC zerolog.Context, domainID uint8, depositHandler DepositHandler, msgChan chan []*message.Message, conn Connection, resource config.Resource) *FungibleTransferEventHandler { +func NewFungibleTransferEventHandler(logC zerolog.Context, domainID uint8, depositHandler DepositHandler, msgChan chan []*message.Message, conn Connection, resource config.Resource, feeAddress btcutil.Address) *FungibleTransferEventHandler { return &FungibleTransferEventHandler{ depositHandler: depositHandler, domainID: domainID, + feeAddress: feeAddress, log: logC.Logger(), conn: conn, msgChan: msgChan, @@ -62,7 +66,7 @@ func (eh *FungibleTransferEventHandler) HandleEvents(blockNumber *big.Int) error eh.log.Error().Err(err).Msg("Error fetching events") return err } - for evtNumber, evt := range evts { + for _, evt := range evts { err := func(evt btcjson.TxRawResult) error { defer func() { if r := recover(); r != nil { @@ -70,7 +74,7 @@ func (eh *FungibleTransferEventHandler) HandleEvents(blockNumber *big.Int) error } }() - d, isDeposit, err := DecodeDepositEvent(evt, eh.resource) + d, isDeposit, err := DecodeDepositEvent(evt, eh.resource, eh.feeAddress) if err != nil { return err } @@ -78,7 +82,7 @@ func (eh *FungibleTransferEventHandler) HandleEvents(blockNumber *big.Int) error if !isDeposit { return nil } - nonce, err := eh.CalculateNonce(blockNumber, evtNumber) + nonce, err := eh.CalculateNonce(blockNumber, evt.Hash) if err != nil { return err } @@ -119,23 +123,23 @@ func (eh *FungibleTransferEventHandler) FetchEvents(startBlock *big.Int) ([]btcj return block.Tx, nil } -func (eh *FungibleTransferEventHandler) CalculateNonce(blockNumber *big.Int, evtNumber int) (uint64, error) { +func (eh *FungibleTransferEventHandler) CalculateNonce(blockNumber *big.Int, transactionHash string) (uint64, error) { // Convert blockNumber to string blockNumberStr := blockNumber.String() - // Convert evtNumber to *big.Int - evtNumberBigInt := big.NewInt(int64(evtNumber)) + // Concatenate blockNumberStr and transactionHash with a separator + concatenatedStr := blockNumberStr + "-" + transactionHash - // Convert evtNumberBigInt to string - evtNumberStr := evtNumberBigInt.String() + // Calculate SHA-256 hash of the concatenated string + hash := sha256.New() + hash.Write([]byte(concatenatedStr)) + hashBytes := hash.Sum(nil) - // Concatenate blockNumberStr and evtNumberStr - concatenatedStr := blockNumberStr + evtNumberStr - - // Parse the concatenated string to uint64 - result, err := strconv.ParseUint(concatenatedStr, 10, 64) - if err != nil { - return 0, err + // XOR fold the hash to get a 64-bit value + var result uint64 + for i := 0; i < 4; i++ { + part := binary.BigEndian.Uint64(hashBytes[i*8 : (i+1)*8]) + result ^= part } return result, nil diff --git a/chains/btc/listener/event-handlers_test.go b/chains/btc/listener/event-handlers_test.go index e408411e..61f1ab20 100644 --- a/chains/btc/listener/event-handlers_test.go +++ b/chains/btc/listener/event-handlers_test.go @@ -34,6 +34,7 @@ type DepositHandlerTestSuite struct { resource config.Resource msgChan chan []*message.Message mockConn *mock_listener.MockConnection + feeAddress btcutil.Address } func TestRunDepositHandlerTestSuite(t *testing.T) { @@ -43,12 +44,14 @@ func TestRunDepositHandlerTestSuite(t *testing.T) { func (s *DepositHandlerTestSuite) SetupTest() { ctrl := gomock.NewController(s.T()) s.domainID = 1 - address, _ := btcutil.DecodeAddress("tb1qln69zuhdunc9stwfh6t7adexxrcr04ppy6thgm", &chaincfg.TestNet3Params) - s.resource = config.Resource{Address: address, ResourceID: [32]byte{}} + address, _ := btcutil.DecodeAddress("tb1pdf5c3q35ssem2l25n435fa69qr7dzwkc6gsqehuflr3euh905l2slafjvv", &chaincfg.TestNet3Params) + s.feeAddress, _ = btcutil.DecodeAddress("tb1qln69zuhdunc9stwfh6t7adexxrcr04ppy6thgm", &chaincfg.TestNet3Params) + + s.resource = config.Resource{Address: address, ResourceID: [32]byte{}, FeeAmount: big.NewInt(10000)} 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.fungibleTransferEventHandler = listener.NewFungibleTransferEventHandler(zerolog.Context{}, s.domainID, s.mockDepositHandler, s.msgChan, s.mockConn, s.resource, s.feeAddress) } func (s *DepositHandlerTestSuite) Test_FetchDepositFails_GetBlockHashError() { @@ -67,26 +70,18 @@ func (s *DepositHandlerTestSuite) Test_FetchDepositFails_GetBlockVerboseTxError( s.NotNil(err) } -func (s *DepositHandlerTestSuite) Test_CalculateNonceFail_BlockNumberOverflow() { - - blockNumber := new(big.Int) - blockNumber.SetString("18446744073709551616", 10) - nonce, err := s.fungibleTransferEventHandler.CalculateNonce(blockNumber, 5) - s.Equal(nonce, uint64(0)) - s.NotNil(err) -} - func (s *DepositHandlerTestSuite) Test_CalculateNonce() { - blockNumber := big.NewInt(123) - nonce, err := s.fungibleTransferEventHandler.CalculateNonce(blockNumber, 4) - s.Equal(nonce, uint64(1234)) + blockNumber := big.NewInt(850000) + nonce, err := s.fungibleTransferEventHandler.CalculateNonce(blockNumber, "a3f1e4d8b3c5e2a1f6d3c7e4b8a9f3e2c1d4a6b7c8e3f1d2c4b5a6e7") + fmt.Println(nonce) + s.Equal(nonce, uint64(12849897320021645821)) s.Nil(err) } func (s *DepositHandlerTestSuite) Test_HandleDepositFails_ExecutionContinue() { blockNumber := big.NewInt(100) data2 := map[string]any{ - "deposit_nonce": uint64(1001), + "deposit_nonce": uint64(8228687738678474667), "resource_id": [32]byte{0}, "amount": big.NewInt(19000), "deposit_data": "0xe9f23A8289764280697a03aC06795eA92a170e42_1", @@ -129,10 +124,17 @@ func (s *DepositHandlerTestSuite) Test_HandleDepositFails_ExecutionContinue() { { ScriptPubKey: btcjson.ScriptPubKeyResult{ Type: "witness_v1_taproot", - Address: "tb1qln69zuhdunc9stwfh6t7adexxrcr04ppy6thgm", + Address: "tb1pdf5c3q35ssem2l25n435fa69qr7dzwkc6gsqehuflr3euh905l2slafjvv", }, Value: float64(0.00019), }, + { + ScriptPubKey: btcjson.ScriptPubKeyResult{ + Type: "witness_v1_taproot", + Address: "tb1qln69zuhdunc9stwfh6t7adexxrcr04ppy6thgm", + }, + Value: float64(0.0002), + }, }, } diff --git a/chains/btc/listener/util.go b/chains/btc/listener/util.go index ab4012f8..d41a5146 100644 --- a/chains/btc/listener/util.go +++ b/chains/btc/listener/util.go @@ -6,6 +6,7 @@ import ( "github.com/ChainSafe/sygma-relayer/chains/btc/config" "github.com/btcsuite/btcd/btcjson" + "github.com/btcsuite/btcd/btcutil" ) const ( @@ -13,8 +14,10 @@ const ( OP_RETURN = "nulldata" ) -func DecodeDepositEvent(evt btcjson.TxRawResult, resource config.Resource) (Deposit, bool, error) { +func DecodeDepositEvent(evt btcjson.TxRawResult, resource config.Resource, feeAddress btcutil.Address) (Deposit, bool, error) { amount := big.NewInt(0) + feeAmount := big.NewInt(0) + isBridgeDeposit := false sender := "" data := "" @@ -37,9 +40,13 @@ func DecodeDepositEvent(evt btcjson.TxRawResult, resource config.Resource) (Depo amount.Add(amount, big.NewInt(int64(vout.Value*1e8))) } } + + if feeAddress.String() == vout.ScriptPubKey.Address { + feeAmount.Add(feeAmount, big.NewInt(int64(vout.Value*1e8))) + } } - if !isBridgeDeposit { + if !isBridgeDeposit || (feeAmount.Cmp(resource.FeeAmount) == -1) { return Deposit{}, false, nil } diff --git a/chains/btc/listener/util_test.go b/chains/btc/listener/util_test.go index 25aba540..4dd12ed0 100644 --- a/chains/btc/listener/util_test.go +++ b/chains/btc/listener/util_test.go @@ -16,8 +16,9 @@ import ( type DecodeEventsSuite struct { suite.Suite - mockConn *mock_listener.MockConnection - resource config.Resource + mockConn *mock_listener.MockConnection + resource config.Resource + feeAddress btcutil.Address } func TestRunDecodeDepositEventsSuite(t *testing.T) { @@ -27,7 +28,8 @@ func TestRunDecodeDepositEventsSuite(t *testing.T) { func (s *DecodeEventsSuite) SetupTest() { ctrl := gomock.NewController(s.T()) address, _ := btcutil.DecodeAddress("tb1qln69zuhdunc9stwfh6t7adexxrcr04ppy6thgm", &chaincfg.TestNet3Params) - s.resource = config.Resource{Address: address, ResourceID: [32]byte{}} + s.resource = config.Resource{Address: address, ResourceID: [32]byte{}, FeeAmount: big.NewInt(100000000)} + s.feeAddress, _ = btcutil.DecodeAddress("tb1pdf5c3q35ssem2l25n435fa69qr7dzwkc6gsqehuflr3euh905l2slafjvv", &chaincfg.TestNet3Params) s.mockConn = mock_listener.NewMockConnection(ctrl) } @@ -56,7 +58,7 @@ func (s *DecodeEventsSuite) Test_DecodeDepositEvent_ErrorDecodingOPRETURNData() }, } - deposit, isDeposit, err := listener.DecodeDepositEvent(d1, s.resource) + deposit, isDeposit, err := listener.DecodeDepositEvent(d1, s.resource, s.feeAddress) s.Equal(isDeposit, true) s.NotNil(err) s.Equal(deposit, listener.Deposit{}) @@ -84,9 +86,16 @@ func (s *DecodeEventsSuite) Test_DecodeDepositEvent() { }, Value: float64(0.00019), }, + { + ScriptPubKey: btcjson.ScriptPubKeyResult{ + Type: "witness_v1_taproot", + Address: "tb1pdf5c3q35ssem2l25n435fa69qr7dzwkc6gsqehuflr3euh905l2slafjvv", + }, + Value: float64(1), + }, }, } - deposit, isDeposit, err := listener.DecodeDepositEvent(d1, s.resource) + deposit, isDeposit, err := listener.DecodeDepositEvent(d1, s.resource, s.feeAddress) s.Equal(isDeposit, true) s.Nil(err) s.Equal(deposit, listener.Deposit{ @@ -96,6 +105,71 @@ func (s *DecodeEventsSuite) Test_DecodeDepositEvent() { }) } +func (s *DecodeEventsSuite) Test_DecodeDepositEvent_FeeNotSent() { + d1 := btcjson.TxRawResult{ + Vin: []btcjson.Vin{ + { + Txid: "00000000000000000008bba5a6ff31fdb9bb1d4147905b5b3c47a07a07235bfc", + }, + }, + Vout: []btcjson.Vout{ + { + ScriptPubKey: btcjson.ScriptPubKeyResult{ + Type: "nulldata", + Hex: "6a2c3078653966323341383238393736343238303639376130336143303637393565413932613137306534325f31", + }, + }, + { + ScriptPubKey: btcjson.ScriptPubKeyResult{ + Type: "witness_v1_taproot", + Address: "tb1qln69zuhdunc9stwfh6t7adexxrcr04ppy6thgm", + }, + Value: float64(0.00019), + }, + }, + } + deposit, isDeposit, err := listener.DecodeDepositEvent(d1, s.resource, s.feeAddress) + s.Equal(isDeposit, false) + s.Nil(err) + s.Equal(deposit, listener.Deposit{}) +} + +func (s *DecodeEventsSuite) Test_DecodeDepositEvent_NotEnoughFeeSent() { + d1 := btcjson.TxRawResult{ + Vin: []btcjson.Vin{ + { + Txid: "00000000000000000008bba5a6ff31fdb9bb1d4147905b5b3c47a07a07235bfc", + }, + }, + Vout: []btcjson.Vout{ + { + ScriptPubKey: btcjson.ScriptPubKeyResult{ + Type: "nulldata", + Hex: "6a2c3078653966323341383238393736343238303639376130336143303637393565413932613137306534325f31", + }, + }, + { + ScriptPubKey: btcjson.ScriptPubKeyResult{ + Type: "witness_v1_taproot", + Address: "tb1qln69zuhdunc9stwfh6t7adexxrcr04ppy6thgm", + }, + Value: float64(0.00019), + }, + { + ScriptPubKey: btcjson.ScriptPubKeyResult{ + Type: "witness_v1_taproot", + Address: "tb1pdf5c3q35ssem2l25n435fa69qr7dzwkc6gsqehuflr3euh905l2slafjvv", + }, + Value: float64(0.9), + }, + }, + } + deposit, isDeposit, err := listener.DecodeDepositEvent(d1, s.resource, s.feeAddress) + s.Equal(isDeposit, false) + s.Nil(err) + s.Equal(deposit, listener.Deposit{}) +} + func (s *DecodeEventsSuite) Test_DecodeDepositEvent_NotBridgeDepositTx() { d1 := btcjson.TxRawResult{ Vin: []btcjson.Vin{ @@ -118,9 +192,16 @@ func (s *DecodeEventsSuite) Test_DecodeDepositEvent_NotBridgeDepositTx() { }, Value: float64(0.00019), }, + { + ScriptPubKey: btcjson.ScriptPubKeyResult{ + Type: "witness_v1_taproot", + Address: "tb1pdf5c3q35ssem2l25n435fa69qr7dzwkc6gsqehuflr3euh905l2slafjvv", + }, + Value: float64(1), + }, }, } - deposit, isDeposit, err := listener.DecodeDepositEvent(d1, s.resource) + deposit, isDeposit, err := listener.DecodeDepositEvent(d1, s.resource, s.feeAddress) s.Equal(isDeposit, false) s.Nil(err) s.Equal(deposit, listener.Deposit{}) diff --git a/chains/btc/mempool/mempool.go b/chains/btc/mempool/mempool.go index 6692e171..445b9c75 100644 --- a/chains/btc/mempool/mempool.go +++ b/chains/btc/mempool/mempool.go @@ -8,10 +8,18 @@ import ( "sort" ) +type Status struct { + Confirmed bool + BlockHeight uint64 `json:"block_height"` + BlockHash string `json:"block_hash"` + BlockTime uint64 `json:"block_time"` +} + type Utxo struct { - TxID string `json:"txid"` - Vout uint32 `json:"vout"` - Value uint64 `json:"value"` + TxID string `json:"txid"` + Vout uint32 `json:"vout"` + Value uint64 `json:"value"` + Status Status `json:"status"` } type Fee struct { @@ -69,7 +77,7 @@ func (a *MempoolAPI) Utxos(address string) ([]Utxo, error) { return nil, err } sort.Slice(utxos, func(i int, j int) bool { - return utxos[i].TxID < utxos[j].TxID + return utxos[i].Status.BlockTime < utxos[j].Status.BlockTime }) return utxos, nil diff --git a/chains/btc/mempool/mempool_test.go b/chains/btc/mempool/mempool_test.go index b6aecc4c..cc507edc 100644 --- a/chains/btc/mempool/mempool_test.go +++ b/chains/btc/mempool/mempool_test.go @@ -52,12 +52,24 @@ func (s *MempoolTestSuite) Test_Utxo_SuccessfulFetch() { { TxID: "28154e2008912d27978225c096c22ffe2ea65e1d55bf440ee41c21f9489c7fe1", Vout: 0, - Value: 11198, + Value: 11197, + Status: mempool.Status{ + Confirmed: true, + BlockHeight: 2812826, + BlockHash: "000000000000001a01d4058773384f2c23aed5a7e5ede252f99e290fa58324a3", + BlockTime: 1715083122, + }, }, { TxID: "28154e2008912d27978225c096c22ffe2ea65e1d55bf440ee41c21f9489c7fe9", Vout: 0, - Value: 11197, + Value: 11198, + Status: mempool.Status{ + Confirmed: true, + BlockHeight: 2812827, + BlockHash: "000000000000001a01d4058773384f2c23aed5a7e5ede252f99e290fa58324a5", + BlockTime: 1715083123, + }, }, }) } diff --git a/chains/btc/mempool/test-data/successful-utxo.json b/chains/btc/mempool/test-data/successful-utxo.json index 4ae65107..0435b6c9 100644 --- a/chains/btc/mempool/test-data/successful-utxo.json +++ b/chains/btc/mempool/test-data/successful-utxo.json @@ -1,4 +1,4 @@ [ - {"txid":"28154e2008912d27978225c096c22ffe2ea65e1d55bf440ee41c21f9489c7fe9","vout":0,"status":{"confirmed":true,"block_height":2812826,"block_hash":"000000000000001a01d4058773384f2c23aed5a7e5ede252f99e290fa58324a3","block_time":1715083122},"value":11197}, - {"txid":"28154e2008912d27978225c096c22ffe2ea65e1d55bf440ee41c21f9489c7fe1","vout":0,"status":{"confirmed":true,"block_height":2812826,"block_hash":"000000000000001a01d4058773384f2c23aed5a7e5ede252f99e290fa58324a3","block_time":1715083122},"value":11198} + {"txid":"28154e2008912d27978225c096c22ffe2ea65e1d55bf440ee41c21f9489c7fe9","vout":0,"status":{"confirmed":true,"block_height":2812827,"block_hash":"000000000000001a01d4058773384f2c23aed5a7e5ede252f99e290fa58324a5","block_time":1715083123},"value":11198}, + {"txid":"28154e2008912d27978225c096c22ffe2ea65e1d55bf440ee41c21f9489c7fe1","vout":0,"status":{"confirmed":true,"block_height":2812826,"block_hash":"000000000000001a01d4058773384f2c23aed5a7e5ede252f99e290fa58324a3","block_time":1715083122},"value":11197} ] \ No newline at end of file diff --git a/chains/evm/config.go b/chains/evm/config.go index 25c5e968..666e7129 100644 --- a/chains/evm/config.go +++ b/chains/evm/config.go @@ -81,7 +81,7 @@ func (c *RawEVMConfig) Validate() error { if c.Bridge == "" { return fmt.Errorf("required field chain.Bridge empty for chain %v", *c.Id) } - if c.BlockConfirmations != 0 && c.BlockConfirmations < 1 { + if c.BlockConfirmations < 1 { return fmt.Errorf("blockConfirmations has to be >=1") } return nil diff --git a/chains/evm/listener/eventHandlers/event-handler.go b/chains/evm/listener/eventHandlers/event-handler.go index 1f8fb302..dbde74cf 100644 --- a/chains/evm/listener/eventHandlers/event-handler.go +++ b/chains/evm/listener/eventHandlers/event-handler.go @@ -26,6 +26,7 @@ import ( "github.com/ChainSafe/sygma-relayer/tss/ecdsa/keygen" "github.com/ChainSafe/sygma-relayer/tss/ecdsa/resharing" 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" @@ -309,7 +310,8 @@ type RefreshEventHandler struct { host host.Host communication comm.Communication connectionGate *p2p.ConnectionGate - storer resharing.SaveDataStorer + ecdsaStorer resharing.SaveDataStorer + frostStorer frostResharing.FrostKeyshareStorer } func NewRefreshEventHandler( @@ -321,7 +323,8 @@ func NewRefreshEventHandler( host host.Host, communication comm.Communication, connectionGate *p2p.ConnectionGate, - storer resharing.SaveDataStorer, + ecdsaStorer resharing.SaveDataStorer, + frostStorer frostResharing.FrostKeyshareStorer, bridgeAddress common.Address, ) *RefreshEventHandler { return &RefreshEventHandler{ @@ -332,7 +335,8 @@ func NewRefreshEventHandler( coordinator: coordinator, host: host, communication: communication, - storer: storer, + ecdsaStorer: ecdsaStorer, + frostStorer: frostStorer, connectionGate: connectionGate, bridgeAddress: bridgeAddress, } @@ -375,11 +379,18 @@ func (eh *RefreshEventHandler) HandleEvents( ) resharing := resharing.NewResharing( - eh.sessionID(startBlock), topology.Threshold, eh.host, eh.communication, eh.storer, + eh.sessionID(startBlock), topology.Threshold, eh.host, eh.communication, eh.ecdsaStorer, ) err = eh.coordinator.Execute(context.Background(), resharing, make(chan interface{}, 1)) if err != nil { - log.Err(err).Msgf("Failed executing key refresh") + log.Err(err).Msgf("Failed executing ecdsa key refresh") + } + frostResharing := frostResharing.NewResharing( + eh.sessionID(startBlock), topology.Threshold, eh.host, eh.communication, eh.frostStorer, + ) + err = eh.coordinator.Execute(context.Background(), frostResharing, make(chan interface{}, 1)) + if err != nil { + log.Err(err).Msgf("Failed executing frost key refresh") } return nil } diff --git a/docs/general/Arhitecture.md b/docs/general/Arhitecture.md index ef5a10cb..9242eca6 100644 --- a/docs/general/Arhitecture.md +++ b/docs/general/Arhitecture.md @@ -5,6 +5,7 @@ ## Components - **[CLI commands](/docs/general/CLI.md)** - overview of CLI commands +- **[Deposit](/docs/general/Deposit.md)** - Deposit data overview - **[Fees](/docs/general/Fees.md)** - high-level overview of handling fees - **[Relayers](/docs/Home.md)** - relayer technical documentation - **[Topology Map](/docs/general/Topology.md)** - overview of topology map usage diff --git a/docs/general/Deposit.md b/docs/general/Deposit.md new file mode 100644 index 00000000..404dbddf --- /dev/null +++ b/docs/general/Deposit.md @@ -0,0 +1,17 @@ +# Deposit +This document describes the expected format of the deposit for different networks +## BTC Deposit +## Format + +### OP_RETURN Output + +- **Purpose**: Stores arbitrary data within the transaction. +- **Requirements**: + - There should be at most one output with a `ScriptPubKey.Type` of `OP_RETURN`. + - The `OP_RETURN` data must be formatted as `receiverEVMAddress_destinationDomainID`. + + +### Amount Calculation + +- The total deposit amount is calculated by summing the values of the outputs that match the resource address. +- Only outputs with script types of `witness_v1_taproot` are considered for the amount calculation. diff --git a/e2e/btc/btc_test.go b/e2e/btc/btc_test.go index 5cfa8f61..52f9b8d5 100644 --- a/e2e/btc/btc_test.go +++ b/e2e/btc/btc_test.go @@ -152,6 +152,9 @@ func (s *IntegrationTestSuite) Test_Erc20Deposit_Btc_to_EVM() { recipientAddress, err := btcutil.DecodeAddress("bcrt1pdf5c3q35ssem2l25n435fa69qr7dzwkc6gsqehuflr3euh905l2sjyr5ek", &chaincfg.RegressionNetParams) s.Nil(err) + feeAddress, err := btcutil.DecodeAddress("mkHS9ne12qx9pS9VojpwU5xtRd4T7X7ZUt", &chaincfg.RegressionNetParams) + s.Nil(err) + // Define the private key as a hexadecimal string privateKeyHex := "ccfa495d2ae193eeec53db12971bdedfe500603ec53f98a6138f0abe932be84f" @@ -170,6 +173,10 @@ func (s *IntegrationTestSuite) Test_Erc20Deposit_Btc_to_EVM() { recipientPkScript, err := txscript.PayToAddrScript(recipientAddress) s.Nil(err) + // Create the PkScript for the recipient address + feeAddressPkScript, err := txscript.PayToAddrScript(feeAddress) + s.Nil(err) + // Define data for the OP_RETURN output opReturnData := []byte("0x1c3A03D04c026b1f4B4208D2ce053c5686E6FB8d_01") opReturnScript, err := txscript.NullDataScript(opReturnData) @@ -183,7 +190,14 @@ func (s *IntegrationTestSuite) Test_Erc20Deposit_Btc_to_EVM() { Index: unspent[0].Vout, }, nil, nil) + hash2, _ := chainhash.NewHashFromStr(unspent[1].TxID) + txInput2 := wire.NewTxIn(&wire.OutPoint{ + Hash: *hash2, + Index: unspent[1].Vout, + }, nil, nil) + txInputs = append(txInputs, txInput) + txInputs = append(txInputs, txInput2) // Create transaction outputs txOutputs := []*wire.TxOut{ @@ -191,6 +205,10 @@ func (s *IntegrationTestSuite) Test_Erc20Deposit_Btc_to_EVM() { Value: int64(unspent[0].Amount*math.Pow(10, 8)) - 10000000, PkScript: recipientPkScript, }, + { + Value: int64(unspent[1].Amount*math.Pow(10, 8)) - 10000000, + PkScript: feeAddressPkScript, + }, { Value: 0, PkScript: opReturnScript, @@ -206,14 +224,13 @@ func (s *IntegrationTestSuite) Test_Erc20Deposit_Btc_to_EVM() { tx.AddTxOut(txOut) } - subscript, err := hex.DecodeString(unspent[0].ScriptPubKey) - s.Nil(err) - - // Sign the transaction - sigScript, err := txscript.SignatureScript(tx, 0, subscript, txscript.SigHashAll, privateKey, true) - s.Nil(err) - - tx.TxIn[0].SignatureScript = sigScript + for i, txIn := range tx.TxIn { + subscript, err := hex.DecodeString(unspent[i].ScriptPubKey) + s.Nil(err) + sigScript, err := txscript.SignatureScript(tx, i, subscript, txscript.SigHashAll, privateKey, true) + s.Nil(err) + txIn.SignatureScript = sigScript + } _, err = conn.Client.SendRawTransaction(tx, true) s.Nil(err) diff --git a/example/app/app.go b/example/app/app.go index 0e87ba09..b2c7ddd9 100644 --- a/example/app/app.go +++ b/example/app/app.go @@ -196,8 +196,8 @@ func Run() error { 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, nil, nil, tssListener, coordinator, host, communication, connectionGate, keyshareStore, bridgeAddress)) - eventHandlers = append(eventHandlers, hubEventHandlers.NewRetryEventHandler(l, tssListener, depositHandler, propStore, bridgeAddress, *config.GeneralChainConfig.Id, config.BlockConfirmations, msgChan)) + eventHandlers = append(eventHandlers, hubEventHandlers.NewRefreshEventHandler(l, nil, nil, tssListener, coordinator, host, communication, connectionGate, keyshareStore, frostKeyshareStore, bridgeAddress)) + eventHandlers = append(eventHandlers, hubEventHandlers.NewRetryEventHandler(l, tssListener, depositHandler, bridgeAddress, *config.GeneralChainConfig.Id, config.BlockConfirmations, msgChan)) evmListener := listener.NewEVMListener(client, eventHandlers, blockstore, sygmaMetrics, *config.GeneralChainConfig.Id, config.BlockRetryInterval, config.BlockConfirmations, config.BlockInterval) executor := executor.NewExecutor(host, communication, coordinator, bridgeContract, keyshareStore, exitLock, config.GasLimit.Uint64()) @@ -268,7 +268,7 @@ func Run() error { 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)) + eventHandlers = append(eventHandlers, btcListener.NewFungibleTransferEventHandler(l, *config.GeneralChainConfig.Id, depositHandler, msgChan, conn, resource, config.FeeAddress)) } listener := btcListener.NewBtcListener(conn, eventHandlers, config, blockstore) diff --git a/example/cfg/config_evm-evm_1.json b/example/cfg/config_evm-evm_1.json index f681fd53..57fead43 100644 --- a/example/cfg/config_evm-evm_1.json +++ b/example/cfg/config_evm-evm_1.json @@ -98,6 +98,7 @@ "name": "bitcoin", "type": "btc", "startBlock": 100, + "feeAddress": "mkHS9ne12qx9pS9VojpwU5xtRd4T7X7ZUt", "endpoint": "bitcoin:18443", "mempoolUrl": "http://mempool-stub:8882", "blockConfirmations": 1, @@ -109,7 +110,8 @@ "address": "bcrt1pdf5c3q35ssem2l25n435fa69qr7dzwkc6gsqehuflr3euh905l2sjyr5ek", "resourceId": "0x0000000000000000000000000000000000000000000000000000000000001000", "script": "51206a698882348433b57d549d6344f74500fcd13ad8d2200cdf89f8e39e5cafa7d5", - "tweak": "c82aa6ae534bb28aaafeb3660c31d6a52e187d8f05d48bb6bdb9b733a9b42212" + "tweak": "c82aa6ae534bb28aaafeb3660c31d6a52e187d8f05d48bb6bdb9b733a9b42212", + "feeAmount": "10000000" } ] } diff --git a/example/cfg/config_evm-evm_2.json b/example/cfg/config_evm-evm_2.json index 6ff0ebf4..153f1341 100644 --- a/example/cfg/config_evm-evm_2.json +++ b/example/cfg/config_evm-evm_2.json @@ -100,6 +100,7 @@ "name": "bitcoin", "type": "btc", "startBlock": 100, + "feeAddress": "mkHS9ne12qx9pS9VojpwU5xtRd4T7X7ZUt", "endpoint": "bitcoin:18443", "mempoolUrl": "http://mempool-stub:8882", "blockConfirmations": 1, @@ -112,7 +113,8 @@ "address": "bcrt1pdf5c3q35ssem2l25n435fa69qr7dzwkc6gsqehuflr3euh905l2sjyr5ek", "resourceId": "0x0000000000000000000000000000000000000000000000000000000000001000", "script": "51206a698882348433b57d549d6344f74500fcd13ad8d2200cdf89f8e39e5cafa7d5", - "tweak": "c82aa6ae534bb28aaafeb3660c31d6a52e187d8f05d48bb6bdb9b733a9b42212" + "tweak": "c82aa6ae534bb28aaafeb3660c31d6a52e187d8f05d48bb6bdb9b733a9b42212", + "feeAmount": "10000000" } ] } diff --git a/example/cfg/config_evm-evm_3.json b/example/cfg/config_evm-evm_3.json index 72884292..a40f31b3 100644 --- a/example/cfg/config_evm-evm_3.json +++ b/example/cfg/config_evm-evm_3.json @@ -99,6 +99,7 @@ "name": "bitcoin", "type": "btc", "startBlock": 100, + "feeAddress": "mkHS9ne12qx9pS9VojpwU5xtRd4T7X7ZUt", "endpoint": "bitcoin:18443", "mempoolUrl": "http://mempool-stub:8882", "blockConfirmations": 1, @@ -111,7 +112,8 @@ "address": "bcrt1pdf5c3q35ssem2l25n435fa69qr7dzwkc6gsqehuflr3euh905l2sjyr5ek", "resourceId": "0x0000000000000000000000000000000000000000000000000000000000001000", "script": "51206a698882348433b57d549d6344f74500fcd13ad8d2200cdf89f8e39e5cafa7d5", - "tweak": "c82aa6ae534bb28aaafeb3660c31d6a52e187d8f05d48bb6bdb9b733a9b42212" + "tweak": "c82aa6ae534bb28aaafeb3660c31d6a52e187d8f05d48bb6bdb9b733a9b42212", + "feeAmount": "10000000" } ] } diff --git a/go.mod b/go.mod index c015c8c5..c957bd21 100644 --- a/go.mod +++ b/go.mod @@ -20,7 +20,7 @@ require ( github.com/spf13/cobra v1.5.0 github.com/spf13/viper v1.9.0 github.com/stretchr/testify v1.8.4 - github.com/sygmaprotocol/sygma-core v0.0.0-20240411120252-bf0131a81565 + github.com/sygmaprotocol/sygma-core v0.0.0-20240627123251-75c4bcb419d0 github.com/taurusgroup/multi-party-sig v0.6.0-alpha-2021-09-21.0.20230619131919-9c7c6ffd7217 go.opentelemetry.io/otel v1.16.0 go.opentelemetry.io/otel/metric v1.16.0 diff --git a/go.sum b/go.sum index 3d12dec0..f236e09f 100644 --- a/go.sum +++ b/go.sum @@ -761,6 +761,8 @@ github.com/sygmaprotocol/multi-party-sig v0.0.0-20240523153754-9377ba09c35e h1:I github.com/sygmaprotocol/multi-party-sig v0.0.0-20240523153754-9377ba09c35e/go.mod h1:roZI3gaKCo15PUSB4LdJpTLTjq8TFsJiOH5kpcN1HpQ= github.com/sygmaprotocol/sygma-core v0.0.0-20240411120252-bf0131a81565 h1:oEd8KmRDSyGvQGg/A0cd4JbWNiVP8GGjK672JWQ0QAk= github.com/sygmaprotocol/sygma-core v0.0.0-20240411120252-bf0131a81565/go.mod h1:b4RZCyYr20Mp4WAAj4TkC6gU2KZ0ZWcpSGmKc6n8NKc= +github.com/sygmaprotocol/sygma-core v0.0.0-20240627123251-75c4bcb419d0 h1:VMsuJP5BCOSFQvA/zkvp4sdqOyj04E48+09EXEv4DT0= +github.com/sygmaprotocol/sygma-core v0.0.0-20240627123251-75c4bcb419d0/go.mod h1:b4RZCyYr20Mp4WAAj4TkC6gU2KZ0ZWcpSGmKc6n8NKc= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= github.com/syndtr/goleveldb v1.0.1-0.20220614013038-64ee5596c38a h1:1ur3QoCqvE5fl+nylMaIr9PVV1w343YRDtsy+Rwu7XI= github.com/syndtr/goleveldb v1.0.1-0.20220614013038-64ee5596c38a/go.mod h1:RRCYJbIwD5jmqPI9XoAFR0OcDxqUctll6zUj/+B4S48= diff --git a/tss/coordinator.go b/tss/coordinator.go index 8b510267..49852127 100644 --- a/tss/coordinator.go +++ b/tss/coordinator.go @@ -30,9 +30,9 @@ var ( type TssProcess interface { Run(ctx context.Context, coordinator bool, resultChn chan interface{}, params []byte) error Stop() - Ready(readyMap map[peer.ID]bool, excludedPeers []peer.ID) (bool, error) + Ready(readyPeers []peer.ID, excludedPeers []peer.ID) (bool, error) Retryable() bool - StartParams(readyMap map[peer.ID]bool) []byte + StartParams(readyPeers []peer.ID) []byte SessionID() string ValidCoordinators() []peer.ID } @@ -230,8 +230,8 @@ func (c *Coordinator) broadcastInitiateMsg(sessionID string) { // peers are ready, start message is broadcasted and tss process is started. func (c *Coordinator) initiate(ctx context.Context, tssProcess TssProcess, resultChn chan interface{}, excludedPeers []peer.ID) error { readyChan := make(chan *comm.WrappedMessage) - readyMap := make(map[peer.ID]bool) - readyMap[c.host.ID()] = true + readyPeers := make([]peer.ID, 0) + readyPeers = append(readyPeers, c.host.ID()) subID := c.communication.Subscribe(tssProcess.SessionID(), comm.TssReadyMsg, readyChan) defer c.communication.UnSubscribe(subID) @@ -245,9 +245,9 @@ func (c *Coordinator) initiate(ctx context.Context, tssProcess TssProcess, resul { log.Debug().Str("SessionID", tssProcess.SessionID()).Msgf("received ready message from %s", wMsg.From) if !slices.Contains(excludedPeers, wMsg.From) { - readyMap[wMsg.From] = true + readyPeers = append(readyPeers, wMsg.From) } - ready, err := tssProcess.Ready(readyMap, excludedPeers) + ready, err := tssProcess.Ready(readyPeers, excludedPeers) if err != nil { return err } @@ -255,7 +255,7 @@ func (c *Coordinator) initiate(ctx context.Context, tssProcess TssProcess, resul continue } - startParams := tssProcess.StartParams(readyMap) + startParams := tssProcess.StartParams(readyPeers) startMsgBytes, err := message.MarshalStartMessage(startParams) if err != nil { return err diff --git a/tss/ecdsa/common/utils.go b/tss/ecdsa/common/utils.go index f9fac5e6..e8cb49fa 100644 --- a/tss/ecdsa/common/utils.go +++ b/tss/ecdsa/common/utils.go @@ -66,3 +66,15 @@ func ExcludePeers(peers peer.IDSlice, excludedPeers peer.IDSlice) peer.IDSlice { } return includedPeers } + +func PeersIntersection(oldPeers peer.IDSlice, newPeers peer.IDSlice) peer.IDSlice { + includedPeers := make(peer.IDSlice, 0) + for _, peer := range oldPeers { + if !slices.Contains(newPeers, peer) { + continue + } + + includedPeers = append(includedPeers, peer) + } + return includedPeers +} diff --git a/tss/ecdsa/common/utils_test.go b/tss/ecdsa/common/utils_test.go index bf6b565a..8a7d2395 100644 --- a/tss/ecdsa/common/utils_test.go +++ b/tss/ecdsa/common/utils_test.go @@ -106,10 +106,10 @@ type ExcludedPeersTestSuite struct { } func TestRunExcludedPeersTestSuite(t *testing.T) { - suite.Run(t, new(PartiesFromPeersTestSuite)) + suite.Run(t, new(ExcludedPeersTestSuite)) } -func (s *PartiesFromPeersTestSuite) Test_ExcludePeers_Excluded() { +func (s *ExcludedPeersTestSuite) Test_ExcludePeers_Excluded() { peerID1, _ := peer.Decode("QmcW3oMdSqoEcjbyd51auqC23vhKX6BqfcZcY2HJ3sKAZR") peerID2, _ := peer.Decode("QmZHPnN3CKiTAp8VaJqszbf8m7v4mPh15M421KpVdYHF54") peerID3, _ := peer.Decode("QmYayosTHxL2xa4jyrQ2PmbhGbrkSxsGM1kzXLTT8SsLVy") @@ -120,3 +120,23 @@ func (s *PartiesFromPeersTestSuite) Test_ExcludePeers_Excluded() { s.Equal(includedPeers, peer.IDSlice{peerID1}) } + +type PeersIntersectionTestSuite struct { + suite.Suite +} + +func TestRunPeersIntersectionTestSuite(t *testing.T) { + suite.Run(t, new(PeersIntersectionTestSuite)) +} + +func (s *PeersIntersectionTestSuite) Test_PeersIntersection() { + peerID1, _ := peer.Decode("QmcW3oMdSqoEcjbyd51auqC23vhKX6BqfcZcY2HJ3sKAZR") + peerID2, _ := peer.Decode("QmZHPnN3CKiTAp8VaJqszbf8m7v4mPh15M421KpVdYHF54") + peerID3, _ := peer.Decode("QmYayosTHxL2xa4jyrQ2PmbhGbrkSxsGM1kzXLTT8SsLVy") + oldPeers := []peer.ID{peerID1, peerID2, peerID3} + newPeers := []peer.ID{peerID3, peerID2} + + includedPeers := common.PeersIntersection(oldPeers, newPeers) + + s.Equal(includedPeers, peer.IDSlice{peerID2, peerID3}) +} diff --git a/tss/ecdsa/keygen/keygen.go b/tss/ecdsa/keygen/keygen.go index 6ca3c16d..245111c5 100644 --- a/tss/ecdsa/keygen/keygen.go +++ b/tss/ecdsa/keygen/keygen.go @@ -114,12 +114,12 @@ func (k *Keygen) Stop() { // Ready returns true if all parties from the peerstore are ready. // Error is returned if excluded peers exist as we need all peers to participate // in keygen process. -func (k *Keygen) Ready(readyMap map[peer.ID]bool, excludedPeers []peer.ID) (bool, error) { +func (k *Keygen) Ready(readyPeers []peer.ID, excludedPeers []peer.ID) (bool, error) { if len(excludedPeers) > 0 { return false, errors.New("error") } - return len(readyMap) == len(k.Host.Peerstore().Peers()), nil + return len(readyPeers) == len(k.Host.Peerstore().Peers()), nil } // ValidCoordinators returns all peers in peerstore @@ -127,7 +127,7 @@ func (k *Keygen) ValidCoordinators() []peer.ID { return k.Host.Peerstore().Peers() } -func (k *Keygen) StartParams(readyMap map[peer.ID]bool) []byte { +func (k *Keygen) StartParams(readyPeers []peer.ID) []byte { return []byte{} } diff --git a/tss/ecdsa/resharing/resharing.go b/tss/ecdsa/resharing/resharing.go index 9b303767..3540fa15 100644 --- a/tss/ecdsa/resharing/resharing.go +++ b/tss/ecdsa/resharing/resharing.go @@ -143,8 +143,8 @@ func (r *Resharing) Stop() { } // Ready returns true if all parties from peerstore are ready -func (r *Resharing) Ready(readyMap map[peer.ID]bool, excludedPeers []peer.ID) (bool, error) { - return len(readyMap) == len(r.Host.Peerstore().Peers()), nil +func (r *Resharing) Ready(readyPeers []peer.ID, excludedPeers []peer.ID) (bool, error) { + return len(readyPeers) == len(r.Host.Peerstore().Peers()), nil } // ValidCoordinators returns only peers that have a valid keyshare from the previous resharing @@ -164,10 +164,11 @@ func (r *Resharing) ValidCoordinators() []peer.ID { } // StartParams returns threshold and peer subset from the old key to share with new parties. -func (r *Resharing) StartParams(readyMap map[peer.ID]bool) []byte { +func (r *Resharing) StartParams(readyPeers []peer.ID) []byte { + oldSubset := common.PeersIntersection(r.key.Peers, r.Host.Peerstore().Peers()) startParams := &startParams{ OldThreshold: r.key.Threshold, - OldSubset: r.key.Peers, + OldSubset: oldSubset, } paramBytes, _ := json.Marshal(startParams) return paramBytes @@ -200,7 +201,7 @@ func (r *Resharing) validateStartParams(params startParams) error { slices.Sort(r.key.Peers) // if relayer is already part of the active subset, check if peer subset // in starting params is same as one saved in keyshare - if len(r.key.Peers) != 0 && !slices.Equal(params.OldSubset, r.key.Peers) { + if len(r.key.Peers) != 0 && !slices.Equal(params.OldSubset, common.PeersIntersection(r.key.Peers, r.Host.Peerstore().Peers())) { return errors.New("invalid peers subset in start params") } diff --git a/tss/ecdsa/resharing/resharing_test.go b/tss/ecdsa/resharing/resharing_test.go index 5f2c1495..10a14203 100644 --- a/tss/ecdsa/resharing/resharing_test.go +++ b/tss/ecdsa/resharing/resharing_test.go @@ -75,6 +75,53 @@ func (s *ResharingTestSuite) Test_ValidResharingProcess_OldAndNewSubset() { s.Nil(err) } +func (s *ResharingTestSuite) Test_ValidResharingProcess_RemovePeer() { + communicationMap := make(map[peer.ID]*tsstest.TestCommunication) + coordinators := []*tss.Coordinator{} + processes := []tss.TssProcess{} + + hosts := []host.Host{} + for i := 0; i < s.PartyNumber-1; i++ { + host, _ := tsstest.NewHost(i) + hosts = append(hosts, host) + } + for _, host := range hosts { + for _, peer := range hosts { + host.Peerstore().AddAddr(peer.ID(), peer.Addrs()[0], peerstore.PermanentAddrTTL) + } + } + + for i, host := range hosts { + communication := tsstest.TestCommunication{ + Host: host, + Subscriptions: make(map[comm.SubscriptionID]chan *comm.WrappedMessage), + } + communicationMap[host.ID()] = &communication + storer := keyshare.NewECDSAKeyshareStore(fmt.Sprintf("../../test/keyshares/%d.keyshare", i)) + share, _ := storer.GetKeyshare() + s.MockECDSAStorer.EXPECT().LockKeyshare() + s.MockECDSAStorer.EXPECT().UnlockKeyshare() + s.MockECDSAStorer.EXPECT().GetKeyshare().Return(share, nil) + s.MockECDSAStorer.EXPECT().StoreKeyshare(gomock.Any()).Return(nil) + resharing := resharing.NewResharing("resharing2", 1, host, &communication, s.MockECDSAStorer) + electorFactory := elector.NewCoordinatorElectorFactory(host, s.BullyConfig) + coordinators = append(coordinators, tss.NewCoordinator(host, &communication, electorFactory)) + processes = append(processes, resharing) + } + tsstest.SetupCommunication(communicationMap) + + resultChn := make(chan interface{}) + pool := pool.New().WithContext(context.Background()).WithCancelOnError() + for i, coordinator := range coordinators { + pool.Go(func(ctx context.Context) error { + return coordinator.Execute(ctx, processes[i], resultChn) + }) + } + + err := pool.Wait() + s.Nil(err) +} + func (s *ResharingTestSuite) Test_InvalidResharingProcess_InvalidOldThreshold_LessThenZero() { communicationMap := make(map[peer.ID]*tsstest.TestCommunication) coordinators := []*tss.Coordinator{} diff --git a/tss/ecdsa/signing/signing.go b/tss/ecdsa/signing/signing.go index 76888b20..c4a1c49b 100644 --- a/tss/ecdsa/signing/signing.go +++ b/tss/ecdsa/signing/signing.go @@ -144,9 +144,9 @@ func (s *Signing) Stop() { } // Ready returns true if threshold+1 parties are ready to start the signing process. -func (s *Signing) Ready(readyMap map[peer.ID]bool, excludedPeers []peer.ID) (bool, error) { - readyMap = s.readyParticipants(readyMap) - return len(readyMap) == s.key.Threshold+1, nil +func (s *Signing) Ready(readyPeers []peer.ID, excludedPeers []peer.ID) (bool, error) { + readyPeers = s.readyParticipants(readyPeers) + return len(readyPeers) == s.key.Threshold+1, nil } // ValidCoordinators returns only peers that have a valid keyshare @@ -157,12 +157,10 @@ func (s *Signing) ValidCoordinators() []peer.ID { // StartParams returns peer subset for this tss process. It is calculated // by sorting hashes of peer IDs and session ID and chosing ready peers alphabetically // until threshold is satisfied. -func (s *Signing) StartParams(readyMap map[peer.ID]bool) []byte { - readyMap = s.readyParticipants(readyMap) +func (s *Signing) StartParams(readyPeers []peer.ID) []byte { + readyPeers = s.readyParticipants(readyPeers) peers := []peer.ID{} - for peer := range readyMap { - peers = append(peers, peer) - } + peers = append(peers, readyPeers...) sortedPeers := util.SortPeersForSession(peers, s.SessionID()) peerSubset := []peer.ID{} @@ -214,18 +212,15 @@ func (s *Signing) processEndMessage(ctx context.Context, endChn chan tssCommon.S } // readyParticipants returns all ready peers that contain a valid key share -func (s *Signing) readyParticipants(readyMap map[peer.ID]bool) map[peer.ID]bool { - readyParticipants := make(map[peer.ID]bool) - for peer, ready := range readyMap { - if !ready { - continue - } +func (s *Signing) readyParticipants(readyPeers []peer.ID) []peer.ID { + readyParticipants := make([]peer.ID, 0) + for _, peer := range readyPeers { if !slices.Contains(s.key.Peers, peer) { continue } - readyParticipants[peer] = true + readyParticipants = append(readyParticipants, peer) } return readyParticipants diff --git a/tss/frost/common/base.go b/tss/frost/common/base.go index 21a0a458..5ed4362c 100644 --- a/tss/frost/common/base.go +++ b/tss/frost/common/base.go @@ -108,7 +108,7 @@ func (k *BaseFrostTss) BroadcastPeers(msg *protocol.Message) ([]peer.ID, error) return k.Peers, nil } else { if string(msg.To) == "" { - return []peer.ID{}, nil + return k.Peers, nil } p, err := peer.Decode(string(msg.To)) diff --git a/tss/frost/common/util.go b/tss/frost/common/util.go index 135c3438..22de1fdb 100644 --- a/tss/frost/common/util.go +++ b/tss/frost/common/util.go @@ -4,19 +4,16 @@ package common import ( - "sort" - mapset "github.com/deckarep/golang-set/v2" "github.com/libp2p/go-libp2p/core/peer" "github.com/taurusgroup/multi-party-sig/pkg/party" ) func PartyIDSFromPeers(peers peer.IDSlice) []party.ID { - sort.Sort(peers) peerSet := mapset.NewSet[peer.ID](peers...) idSlice := make([]party.ID, len(peerSet.ToSlice())) for i, peer := range peerSet.ToSlice() { - idSlice[i] = party.ID(peer.Pretty()) + idSlice[i] = party.ID(peer.String()) } return idSlice } diff --git a/tss/frost/keygen/keygen.go b/tss/frost/keygen/keygen.go index e297b4ac..fbd9801d 100644 --- a/tss/frost/keygen/keygen.go +++ b/tss/frost/keygen/keygen.go @@ -76,7 +76,7 @@ func (k *Keygen) Run( var err error k.Handler, err = protocol.NewMultiHandler( frost.KeygenTaproot( - party.ID(k.Host.ID().Pretty()), + party.ID(k.Host.ID().String()), common.PartyIDSFromPeers(append(k.Host.Peerstore().Peers(), k.Host.ID())), k.threshold), []byte(k.SessionID())) @@ -101,12 +101,12 @@ func (k *Keygen) Stop() { // Ready returns true if all parties from the peerstore are ready. // Error is returned if excluded peers exist as we need all peers to participate // in keygen process. -func (k *Keygen) Ready(readyMap map[peer.ID]bool, excludedPeers []peer.ID) (bool, error) { +func (k *Keygen) Ready(readyPeers []peer.ID, excludedPeers []peer.ID) (bool, error) { if len(excludedPeers) > 0 { return false, errors.New("error") } - return len(readyMap) == len(k.Host.Peerstore().Peers()), nil + return len(readyPeers) == len(k.Host.Peerstore().Peers()), nil } // ValidCoordinators returns all peers in peerstore @@ -114,7 +114,7 @@ func (k *Keygen) ValidCoordinators() []peer.ID { return k.Peers } -func (k *Keygen) StartParams(readyMap map[peer.ID]bool) []byte { +func (k *Keygen) StartParams(readyPeers []peer.ID) []byte { return []byte{} } diff --git a/tss/frost/resharing/resharing.go b/tss/frost/resharing/resharing.go index 964b639a..f5dd9b3e 100644 --- a/tss/frost/resharing/resharing.go +++ b/tss/frost/resharing/resharing.go @@ -117,15 +117,15 @@ func (r *Resharing) Stop() { } // Ready returns true if all parties from peerstore are ready -func (r *Resharing) Ready(readyMap map[peer.ID]bool, excludedPeers []peer.ID) (bool, error) { - return len(readyMap) == len(r.Host.Peerstore().Peers()), nil +func (r *Resharing) Ready(readyPeers []peer.ID, excludedPeers []peer.ID) (bool, error) { + return len(readyPeers) == len(r.Host.Peerstore().Peers()), nil } func (r *Resharing) ValidCoordinators() []peer.ID { return r.key.Peers } -func (r *Resharing) StartParams(readyMap map[peer.ID]bool) []byte { +func (r *Resharing) StartParams(readyPeers []peer.ID) []byte { return r.key.Key.PublicKey } diff --git a/tss/frost/resharing/resharing_test.go b/tss/frost/resharing/resharing_test.go index 7657db2c..8925c756 100644 --- a/tss/frost/resharing/resharing_test.go +++ b/tss/frost/resharing/resharing_test.go @@ -74,3 +74,48 @@ func (s *ResharingTestSuite) Test_ValidResharingProcess_OldAndNewSubset() { err := pool.Wait() s.Nil(err) } + +func (s *ResharingTestSuite) Test_ValidResharingProcess_RemovePeer() { + communicationMap := make(map[peer.ID]*tsstest.TestCommunication) + coordinators := []*tss.Coordinator{} + processes := []tss.TssProcess{} + + hosts := []host.Host{} + for i := 0; i < s.PartyNumber-1; i++ { + host, _ := tsstest.NewHost(i) + hosts = append(hosts, host) + } + for _, host := range hosts { + for _, peer := range hosts { + host.Peerstore().AddAddr(peer.ID(), peer.Addrs()[0], peerstore.PermanentAddrTTL) + } + } + + for i, host := range hosts { + communication := tsstest.TestCommunication{ + Host: host, + Subscriptions: make(map[comm.SubscriptionID]chan *comm.WrappedMessage), + } + communicationMap[host.ID()] = &communication + storer := keyshare.NewFrostKeyshareStore(fmt.Sprintf("../../test/keyshares/%d-frost.keyshare", i)) + share, err := storer.GetKeyshare() + s.MockFrostStorer.EXPECT().LockKeyshare() + s.MockFrostStorer.EXPECT().UnlockKeyshare() + s.MockFrostStorer.EXPECT().GetKeyshare().Return(share, err) + s.MockFrostStorer.EXPECT().StoreKeyshare(gomock.Any()).Return(nil) + resharing := resharing.NewResharing("resharing2", 1, host, &communication, s.MockFrostStorer) + electorFactory := elector.NewCoordinatorElectorFactory(host, s.BullyConfig) + coordinators = append(coordinators, tss.NewCoordinator(host, &communication, electorFactory)) + processes = append(processes, resharing) + } + tsstest.SetupCommunication(communicationMap) + + resultChn := make(chan interface{}) + pool := pool.New().WithContext(context.Background()).WithCancelOnError() + for i, coordinator := range coordinators { + pool.Go(func(ctx context.Context) error { return coordinator.Execute(ctx, processes[i], resultChn) }) + } + + err := pool.Wait() + s.Nil(err) +} diff --git a/tss/frost/signing/signing.go b/tss/frost/signing/signing.go index b1d3544c..4d3ee992 100644 --- a/tss/frost/signing/signing.go +++ b/tss/frost/signing/signing.go @@ -147,9 +147,9 @@ func (s *Signing) Stop() { } // Ready returns true if threshold+1 parties are ready to start the signing process. -func (s *Signing) Ready(readyMap map[peer.ID]bool, excludedPeers []peer.ID) (bool, error) { - readyMap = s.readyParticipants(readyMap) - return len(readyMap) == s.key.Threshold+1, nil +func (s *Signing) Ready(readyPeers []peer.ID, excludedPeers []peer.ID) (bool, error) { + readyPeers = s.readyParticipants(readyPeers) + return len(readyPeers) == s.key.Threshold+1, nil } // ValidCoordinators returns only peers that have a valid keyshare @@ -160,12 +160,10 @@ func (s *Signing) ValidCoordinators() []peer.ID { // StartParams returns peer subset for this tss process. It is calculated // by sorting hashes of peer IDs and session ID and chosing ready peers alphabetically // until threshold is satisfied. -func (s *Signing) StartParams(readyMap map[peer.ID]bool) []byte { - readyMap = s.readyParticipants(readyMap) +func (s *Signing) StartParams(readyPeers []peer.ID) []byte { + readyPeers = s.readyParticipants(readyPeers) peers := []peer.ID{} - for peer := range readyMap { - peers = append(peers, peer) - } + peers = append(peers, readyPeers...) sortedPeers := util.SortPeersForSession(peers, s.SessionID()) peerSubset := []peer.ID{} @@ -221,18 +219,15 @@ func (s *Signing) processEndMessage(ctx context.Context) error { } // readyParticipants returns all ready peers that contain a valid key share -func (s *Signing) readyParticipants(readyMap map[peer.ID]bool) map[peer.ID]bool { - readyParticipants := make(map[peer.ID]bool) - for peer, ready := range readyMap { - if !ready { - continue - } +func (s *Signing) readyParticipants(readyPeers []peer.ID) []peer.ID { + readyParticipants := make([]peer.ID, 0) + for _, peer := range readyPeers { if !slices.Contains(s.key.Peers, peer) { continue } - readyParticipants[peer] = true + readyParticipants = append(readyParticipants, peer) } return readyParticipants diff --git a/tss/mock/coordinator.go b/tss/mock/coordinator.go index 66a60e17..8268dd2b 100644 --- a/tss/mock/coordinator.go +++ b/tss/mock/coordinator.go @@ -36,18 +36,18 @@ func (m *MockTssProcess) EXPECT() *MockTssProcessMockRecorder { } // Ready mocks base method. -func (m *MockTssProcess) Ready(readyMap map[peer.ID]bool, excludedPeers []peer.ID) (bool, error) { +func (m *MockTssProcess) Ready(readyPeers, excludedPeers []peer.ID) (bool, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Ready", readyMap, excludedPeers) + ret := m.ctrl.Call(m, "Ready", readyPeers, excludedPeers) ret0, _ := ret[0].(bool) ret1, _ := ret[1].(error) return ret0, ret1 } // Ready indicates an expected call of Ready. -func (mr *MockTssProcessMockRecorder) Ready(readyMap, excludedPeers interface{}) *gomock.Call { +func (mr *MockTssProcessMockRecorder) Ready(readyPeers, excludedPeers interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Ready", reflect.TypeOf((*MockTssProcess)(nil).Ready), readyMap, excludedPeers) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Ready", reflect.TypeOf((*MockTssProcess)(nil).Ready), readyPeers, excludedPeers) } // Retryable mocks base method. @@ -93,17 +93,17 @@ func (mr *MockTssProcessMockRecorder) SessionID() *gomock.Call { } // StartParams mocks base method. -func (m *MockTssProcess) StartParams(readyMap map[peer.ID]bool) []byte { +func (m *MockTssProcess) StartParams(readyPeers []peer.ID) []byte { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "StartParams", readyMap) + ret := m.ctrl.Call(m, "StartParams", readyPeers) ret0, _ := ret[0].([]byte) return ret0 } // StartParams indicates an expected call of StartParams. -func (mr *MockTssProcessMockRecorder) StartParams(readyMap interface{}) *gomock.Call { +func (mr *MockTssProcessMockRecorder) StartParams(readyPeers interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StartParams", reflect.TypeOf((*MockTssProcess)(nil).StartParams), readyMap) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StartParams", reflect.TypeOf((*MockTssProcess)(nil).StartParams), readyPeers) } // Stop mocks base method.