diff --git a/api/errors/errors.go b/api/errors/errors.go index 67480a70..33e97fc5 100644 --- a/api/errors/errors.go +++ b/api/errors/errors.go @@ -89,6 +89,9 @@ var ErrInvalidGuardianAddress = errors.New("invalid guardian address") // ErrTxGenerationFailed signals an error generating a transaction var ErrTxGenerationFailed = errors.New("transaction generation failed") +// ErrDelegationOperationsUnavailable signals that delegation operations are temporarily unavailable +var ErrDelegationOperationsUnavailable = errors.New("delegation operations are temporarily unavailable") + // ErrInvalidSenderAddress signals a wrong format for sender address was provided var ErrInvalidSenderAddress = errors.New("invalid sender address") diff --git a/api/groups/baseTransactionGroup.go b/api/groups/baseTransactionGroup.go index fc33e52c..6a827927 100644 --- a/api/groups/baseTransactionGroup.go +++ b/api/groups/baseTransactionGroup.go @@ -1,6 +1,7 @@ package groups import ( + stdErrors "errors" "fmt" "net/http" "strconv" @@ -127,6 +128,16 @@ func (group *transactionGroup) sendMultipleTransactions(c *gin.Context) { response, err := group.facade.SendMultipleTransactions(txs) if err != nil { + if stdErrors.Is(err, errors.ErrDelegationOperationsUnavailable) { + shared.RespondWith( + c, + http.StatusServiceUnavailable, + nil, + err.Error(), + data.ReturnCodeInternalError, + ) + return + } shared.RespondWith( c, http.StatusInternalServerError, diff --git a/api/groups/baseTransactionGroup_test.go b/api/groups/baseTransactionGroup_test.go index e9c94c42..2c9e911a 100644 --- a/api/groups/baseTransactionGroup_test.go +++ b/api/groups/baseTransactionGroup_test.go @@ -400,6 +400,31 @@ func TestSendMultipleTransactions_ReturnsSuccessfully(t *testing.T) { assert.Equal(t, uint64(10), response.Data.Num) } +func TestSendMultipleTransactions_DelegationOperationsUnavailable(t *testing.T) { + t.Parallel() + + facade := &mock.FacadeStub{ + SendMultipleTransactionsHandler: func(_ []*data.Transaction) (data.MultipleTransactionsResponseData, error) { + return data.MultipleTransactionsResponseData{}, apiErrors.ErrDelegationOperationsUnavailable + }, + } + + transactionsGroup, err := groups.NewTransactionGroup(facade) + require.NoError(t, err) + ws := startProxyServer(transactionsGroup, transactionsPath) + req, err := http.NewRequest("POST", "/transaction/send-multiple", bytes.NewBufferString(`[{}]`)) + require.NoError(t, err) + + resp := httptest.NewRecorder() + ws.ServeHTTP(resp, req) + + response := GeneralResponse{} + loadResponse(resp.Body, &response) + + require.Equal(t, http.StatusServiceUnavailable, resp.Code) + require.Contains(t, response.Error, apiErrors.ErrDelegationOperationsUnavailable.Error()) +} + func TestSendUserFunds_ErrorWhenFacadeSendUserFundsError(t *testing.T) { t.Parallel() diff --git a/process/delegationOperationsGuard.go b/process/delegationOperationsGuard.go new file mode 100644 index 00000000..7961ea98 --- /dev/null +++ b/process/delegationOperationsGuard.go @@ -0,0 +1,85 @@ +package process + +import ( + "bytes" + + "github.com/multiversx/mx-chain-proxy-go/data" +) + +const ( + delegationSCAddressPrefixLength = 25 + delegationSCAddressSuffixStart = 29 + maxBlockedFunctionLength = len("mergeValidatorToDelegationWithWhitelist") +) + +var delegationManagerSCAddress = [32]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 255, 255} +var firstDelegationSCAddress = [32]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 255, 255, 255} + +var blockedDelegationFunctions = map[string]struct{}{ + "claimRewards": {}, + "delegate": {}, + "reDelegateRewards": {}, + "unDelegate": {}, + "withdraw": {}, +} + +var blockedDelegationManagerFunctions = map[string]struct{}{ + "claimMulti": {}, + "mergeValidatorToDelegationSameOwner": {}, + "mergeValidatorToDelegationWithWhitelist": {}, + "reDelegateMulti": {}, +} + +func (tp *TransactionProcessor) shouldBlockDelegationOperation(tx *data.Transaction) bool { + blockedDelegation, blockedDelegationManager := getBlockedDelegationFunctionTypes(tx.Data) + if !blockedDelegation && !blockedDelegationManager { + return false + } + + receiver, err := tp.pubKeyConverter.Decode(tx.Receiver) + if err != nil { + return false + } + + if blockedDelegationManager && bytes.Equal(receiver, delegationManagerSCAddress[:]) { + return true + } + if blockedDelegation && isDelegationSCAddress(receiver) { + return true + } + + return false +} + +func getBlockedDelegationFunctionTypes(txData []byte) (bool, bool) { + selectorEnd := len(txData) + if selectorEnd > maxBlockedFunctionLength+1 { + selectorEnd = maxBlockedFunctionLength + 1 + } + + separatorIndex := bytes.IndexByte(txData[:selectorEnd], '@') + if separatorIndex >= 0 { + selectorEnd = separatorIndex + } else if len(txData) > maxBlockedFunctionLength { + return false, false + } + + _, blockedDelegation := blockedDelegationFunctions[string(txData[:selectorEnd])] + _, blockedDelegationManager := blockedDelegationManagerFunctions[string(txData[:selectorEnd])] + + return blockedDelegation, blockedDelegationManager +} + +func isDelegationSCAddress(address []byte) bool { + if len(address) != len(firstDelegationSCAddress) { + return false + } + if !bytes.Equal(address[:delegationSCAddressPrefixLength], firstDelegationSCAddress[:delegationSCAddressPrefixLength]) { + return false + } + if !bytes.Equal(address[delegationSCAddressSuffixStart:], firstDelegationSCAddress[delegationSCAddressSuffixStart:]) { + return false + } + + return bytes.Compare(address, firstDelegationSCAddress[:]) >= 0 +} diff --git a/process/delegationOperationsGuard_test.go b/process/delegationOperationsGuard_test.go new file mode 100644 index 00000000..e85994e9 --- /dev/null +++ b/process/delegationOperationsGuard_test.go @@ -0,0 +1,67 @@ +package process + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestGetBlockedDelegationFunctionTypes(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + data []byte + blockedDelegation bool + blockedDelegationManager bool + }{ + { + name: "delegation function", + data: []byte("claimRewards"), + blockedDelegation: true, + }, + { + name: "delegation function with arguments", + data: []byte("unDelegate@01"), + blockedDelegation: true, + }, + { + name: "delegation manager function", + data: []byte("mergeValidatorToDelegationWithWhitelist@00"), + blockedDelegationManager: true, + }, + { + name: "function prefix", + data: []byte("claimRewardsExtra"), + }, + { + name: "long data", + data: bytes.Repeat([]byte{'a'}, 1<<20), + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + blockedDelegation, blockedDelegationManager := getBlockedDelegationFunctionTypes(testCase.data) + + require.Equal(t, testCase.blockedDelegation, blockedDelegation) + require.Equal(t, testCase.blockedDelegationManager, blockedDelegationManager) + }) + } +} + +func TestIsDelegationSCAddress(t *testing.T) { + t.Parallel() + + addressAfterCounterCarry := firstDelegationSCAddress + addressAfterCounterCarry[27] = 1 + addressAfterCounterCarry[28] = 0 + + require.True(t, isDelegationSCAddress(firstDelegationSCAddress[:])) + require.True(t, isDelegationSCAddress(addressAfterCounterCarry[:])) + require.False(t, isDelegationSCAddress(delegationManagerSCAddress[:])) + require.False(t, isDelegationSCAddress(make([]byte, len(firstDelegationSCAddress)))) +} diff --git a/process/transactionProcessor.go b/process/transactionProcessor.go index 6b25429c..7fade45d 100644 --- a/process/transactionProcessor.go +++ b/process/transactionProcessor.go @@ -137,6 +137,9 @@ func (tp *TransactionProcessor) SendTransaction(tx *data.Transaction) (int, stri if err != nil { return http.StatusBadRequest, "", err } + if tp.shouldBlockDelegationOperation(tx) { + return http.StatusServiceUnavailable, "", errors.ErrDelegationOperationsUnavailable + } senderBuff, err := tp.pubKeyConverter.Decode(tx.Sender) if err != nil { @@ -302,6 +305,9 @@ func (tp *TransactionProcessor) SendMultipleTransactions(txs []*data.Transaction "error", err) continue } + if tp.shouldBlockDelegationOperation(currentTx) { + return data.MultipleTransactionsResponseData{}, errors.ErrDelegationOperationsUnavailable + } txsToSend = append(txsToSend, currentTx) } if len(txsToSend) == 0 { diff --git a/process/transactionProcessor_test.go b/process/transactionProcessor_test.go index ba8d6687..b2f10b66 100644 --- a/process/transactionProcessor_test.go +++ b/process/transactionProcessor_test.go @@ -331,8 +331,183 @@ func TestTransactionProcessor_SendTransactionSendingFailsOnFirstObserverShouldSt require.Equal(t, http.StatusOK, rc) } +func TestTransactionProcessor_SendTransactionShouldBlockDelegationOperations(t *testing.T) { + t.Parallel() + + delegationAddress := []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 255, 255, 255} + delegationManagerAddress := []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 255, 255} + testCases := []struct { + name string + receiver []byte + functions []string + }{ + { + name: "delegation contract", + receiver: delegationAddress, + functions: []string{ + "claimRewards", + "delegate", + "reDelegateRewards", + "unDelegate", + "withdraw", + }, + }, + { + name: "delegation manager", + receiver: delegationManagerAddress, + functions: []string{ + "claimMulti", + "mergeValidatorToDelegationSameOwner", + "mergeValidatorToDelegationWithWhitelist", + "reDelegateMulti", + }, + }, + } + + for _, testCase := range testCases { + for _, function := range testCase.functions { + t.Run(testCase.name+"/"+function, func(t *testing.T) { + t.Parallel() + + sender, err := testPubkeyConverter.Encode(bytes.Repeat([]byte{1}, 32)) + require.NoError(t, err) + receiver, err := testPubkeyConverter.Encode(testCase.receiver) + require.NoError(t, err) + + getObserversCalled := false + tp, err := process.NewTransactionProcessor( + &mock.ProcessorStub{ + GetObserversCalled: func(_ uint32, _ data.ObserverDataAvailabilityType) ([]*data.NodeData, error) { + getObserversCalled = true + return nil, nil + }, + }, + testPubkeyConverter, + hasher, + marshalizer, + funcNewTxCostHandler, + logsMerger, + true, + ) + require.NoError(t, err) + + statusCode, txHash, err := tp.SendTransaction(&data.Transaction{ + Sender: sender, + Receiver: receiver, + Data: []byte(function + "@00"), + ChainID: "chain", + Version: 1, + }) + + require.ErrorIs(t, err, apiErrors.ErrDelegationOperationsUnavailable) + require.Equal(t, http.StatusServiceUnavailable, statusCode) + require.Empty(t, txHash) + require.False(t, getObserversCalled) + }) + } + } +} + +func TestTransactionProcessor_SendTransactionShouldAllowOtherOperations(t *testing.T) { + t.Parallel() + + delegationAddress := []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 255, 255, 255} + delegationManagerAddress := []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 255, 255} + otherSCAddress := []byte{0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} + testCases := []struct { + name string + receiver []byte + function string + }{ + {name: "provider operation", receiver: delegationAddress, function: "stakeNodes"}, + {name: "manager operation", receiver: delegationManagerAddress, function: "createNewDelegationContract"}, + {name: "same function on another contract", receiver: otherSCAddress, function: "claimRewards"}, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + sender, err := testPubkeyConverter.Encode(bytes.Repeat([]byte{1}, 32)) + require.NoError(t, err) + receiver, err := testPubkeyConverter.Encode(testCase.receiver) + require.NoError(t, err) + + tp, err := process.NewTransactionProcessor( + &mock.ProcessorStub{ + ComputeShardIdCalled: func(_ []byte) (uint32, error) { + return 0, nil + }, + GetObserversCalled: func(_ uint32, _ data.ObserverDataAvailabilityType) ([]*data.NodeData, error) { + return []*data.NodeData{{Address: "observer"}}, nil + }, + CallPostRestEndPointCalled: func(_ string, _ string, _ interface{}, response interface{}) (int, error) { + response.(*data.ResponseTransaction).Data.TxHash = "hash" + return http.StatusOK, nil + }, + }, + testPubkeyConverter, + hasher, + marshalizer, + funcNewTxCostHandler, + logsMerger, + true, + ) + require.NoError(t, err) + + statusCode, txHash, err := tp.SendTransaction(&data.Transaction{ + Sender: sender, + Receiver: receiver, + Data: []byte(testCase.function), + ChainID: "chain", + Version: 1, + }) + + require.NoError(t, err) + require.Equal(t, http.StatusOK, statusCode) + require.Equal(t, "hash", txHash) + }) + } +} + // //------- SendMultipleTransactions +func TestTransactionProcessor_SendMultipleTransactionsShouldRejectBlockedOperationAtomically(t *testing.T) { + t.Parallel() + + sender, err := testPubkeyConverter.Encode(bytes.Repeat([]byte{1}, 32)) + require.NoError(t, err) + receiver, err := testPubkeyConverter.Encode(bytes.Repeat([]byte{2}, 32)) + require.NoError(t, err) + delegationAddress, err := testPubkeyConverter.Encode([]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 255, 255, 255}) + require.NoError(t, err) + + getObserversCalled := false + tp, err := process.NewTransactionProcessor( + &mock.ProcessorStub{ + GetObserversCalled: func(_ uint32, _ data.ObserverDataAvailabilityType) ([]*data.NodeData, error) { + getObserversCalled = true + return nil, nil + }, + }, + testPubkeyConverter, + hasher, + marshalizer, + funcNewTxCostHandler, + logsMerger, + true, + ) + require.NoError(t, err) + + _, err = tp.SendMultipleTransactions([]*data.Transaction{ + {Sender: sender, Receiver: receiver, ChainID: "chain", Version: 1}, + {Sender: sender, Receiver: delegationAddress, Data: []byte("unDelegate@01"), ChainID: "chain", Version: 1}, + }) + + require.ErrorIs(t, err, apiErrors.ErrDelegationOperationsUnavailable) + require.False(t, getObserversCalled) +} + func TestTransactionProcessor_SendMultipleTransactionsShouldWork(t *testing.T) { t.Parallel()