Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle intra-shard relayed transactions with signal error #81

Merged
merged 13 commits into from
Sep 4, 2023
Merged
5 changes: 5 additions & 0 deletions server/factory/components/observerFacade.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,8 @@ type ObserverFacade struct {
facade.TransactionProcessor
facade.BlockProcessor
}

// ComputeShardId computes the shard ID for a given public key
func (facade *ObserverFacade) ComputeShardId(pubKey []byte) uint32 {
AdoAdoAdo marked this conversation as resolved.
Show resolved Hide resolved
return facade.GetShardCoordinator().ComputeId(pubKey)
}
1 change: 1 addition & 0 deletions server/factory/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ type NetworkProvider interface {
GetAccountNativeBalance(address string, options resources.AccountQueryOptions) (*resources.AccountOnBlock, error)
GetAccountESDTBalance(address string, tokenIdentifier string, options resources.AccountQueryOptions) (*resources.AccountESDTBalance, error)
IsAddressObserved(address string) (bool, error)
ComputeShardIdOfPubKey(pubkey []byte) uint32
ConvertPubKeyToAddress(pubkey []byte) string
ConvertAddressToPubKey(address string) ([]byte, error)
SendTransaction(tx *data.Transaction) (string, error)
Expand Down
2 changes: 1 addition & 1 deletion server/provider/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (

type observerFacade interface {
CallGetRestEndPoint(baseUrl string, path string, value interface{}) (int, error)
ComputeShardId(pubKey []byte) (uint32, error)
ComputeShardId(pubKey []byte) uint32
SendTransaction(tx *data.Transaction) (int, string, error)
ComputeTransactionHash(tx *data.Transaction) (string, error)
GetTransactionByHashAndSenderAddress(hash string, sender string, withEvents bool) (*transaction.ApiTransactionResult, int, error)
Expand Down
12 changes: 7 additions & 5 deletions server/provider/networkProvider.go
Original file line number Diff line number Diff line change
Expand Up @@ -305,11 +305,7 @@ func (provider *networkProvider) IsAddressObserved(address string) (bool, error)
return false, err
}

shard, err := provider.observerFacade.ComputeShardId(pubKey)
if err != nil {
return false, err
}

shard := provider.observerFacade.ComputeShardId(pubKey)
isObservedActualShard := shard == provider.observedActualShard
isObservedProjectedShard := pubKey[len(pubKey)-1] == byte(provider.observedProjectedShard)

Expand All @@ -320,6 +316,12 @@ func (provider *networkProvider) IsAddressObserved(address string) (bool, error)
return isObservedActualShard, nil
}

// ComputeShardIdOfPubKey computes the shard ID of a public key
func (provider *networkProvider) ComputeShardIdOfPubKey(pubKey []byte) uint32 {
shard := provider.observerFacade.ComputeShardId(pubKey)
return shard
}

// ConvertPubKeyToAddress converts a public key to an address
func (provider *networkProvider) ConvertPubKeyToAddress(pubkey []byte) string {
return provider.pubKeyConverter.Encode(pubkey)
Expand Down
11 changes: 11 additions & 0 deletions server/provider/networkProvider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,17 @@ func TestNetworkProvider_DoGetBlockByNonce(t *testing.T) {
})
}

func Test_ComputeShardIdOfPubKey(t *testing.T) {
args := createDefaultArgsNewNetworkProvider()
provider, err := NewNetworkProvider(args)
require.Nil(t, err)
require.NotNil(t, provider)

require.Equal(t, uint32(0), provider.ComputeShardIdOfPubKey(testscommon.TestPubKeyBob))
require.Equal(t, uint32(1), provider.ComputeShardIdOfPubKey(testscommon.TestPubKeyAlice))
require.Equal(t, uint32(2), provider.ComputeShardIdOfPubKey(testscommon.TestPubKeyCarol))
}

func Test_ComputeTransactionFeeForMoveBalance(t *testing.T) {
args := createDefaultArgsNewNetworkProvider()
provider, err := NewNetworkProvider(args)
Expand Down
8 changes: 8 additions & 0 deletions server/services/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,14 @@ func isZeroAmount(amount string) bool {
return false
}

func isZeroBigIntOrNil(value *big.Int) bool {
if value == nil {
return true
}

return value.Sign() == 0
}

func getMagnitudeOfAmount(amount string) string {
return strings.Trim(amount, "-")
}
Expand Down
7 changes: 7 additions & 0 deletions server/services/common_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,13 @@ func Test_IsZeroAmount(t *testing.T) {
require.False(t, isZeroAmount("-1"))
}

func Test_IsZeroBigInt(t *testing.T) {
require.True(t, isZeroBigIntOrNil(big.NewInt(0)))
require.True(t, isZeroBigIntOrNil(nil))
require.False(t, isZeroBigIntOrNil(big.NewInt(42)))
require.False(t, isZeroBigIntOrNil(big.NewInt(-42)))
}

func Test_GetMagnitudeOfAmount(t *testing.T) {
require.Equal(t, "100", getMagnitudeOfAmount("100"))
require.Equal(t, "100", getMagnitudeOfAmount("-100"))
Expand Down
9 changes: 6 additions & 3 deletions server/services/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,25 @@ package services
import (
"encoding/hex"
"strings"

"github.com/multiversx/mx-chain-core-go/core"
)

var (
transactionVersion = 1
transactionProcessingTypeRelayed = "RelayedTx"
transactionProcessingTypeBuiltInFunctionCall = "BuiltInFunctionCall"
transactionProcessingTypeMoveBalance = "MoveBalance"
builtInFunctionClaimDeveloperRewards = "ClaimDeveloperRewards"
builtInFunctionClaimDeveloperRewards = core.BuiltInFunctionClaimDeveloperRewards
refundGasMessage = "refundedGas"
sendingValueToNonPayableContractDataPrefix = "@" + hex.EncodeToString([]byte("sending value to non payable contract"))
argumentsSeparator = "@"
sendingValueToNonPayableContractDataPrefix = argumentsSeparator + hex.EncodeToString([]byte("sending value to non payable contract"))
emptyHash = strings.Repeat("0", 64)
nodeVersionForOfflineRosetta = "N / A"
)

var (
transactionEventSignalError = "signalError"
transactionEventSignalError = core.SignalErrorOperation
transactionEventTransferValueOnly = "transferValueOnly"
transactionEventTopicInvalidMetaTransaction = "meta transaction is invalid"
transactionEventTopicInvalidMetaTransactionNotEnoughGas = "meta transaction is invalid: not enough gas"
Expand Down
1 change: 1 addition & 0 deletions server/services/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -201,3 +201,4 @@ func (factory *errFactory) getPrototypeByCode(code errCode) errPrototype {

var errEventNotFound = errors.New("transaction event not found")
var errCannotRecognizeEvent = errors.New("cannot recognize transaction event")
var errCannotParseRelayedV1 = errors.New("cannot parse relayed V1 transaction")
1 change: 1 addition & 0 deletions server/services/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ type NetworkProvider interface {
GetAccountNativeBalance(address string, options resources.AccountQueryOptions) (*resources.AccountOnBlock, error)
GetAccountESDTBalance(address string, tokenIdentifier string, options resources.AccountQueryOptions) (*resources.AccountESDTBalance, error)
IsAddressObserved(address string) (bool, error)
ComputeShardIdOfPubKey(pubkey []byte) uint32
ConvertPubKeyToAddress(pubkey []byte) string
ConvertAddressToPubKey(address string) ([]byte, error)
SendTransaction(tx *data.Transaction) (string, error)
Expand Down
45 changes: 45 additions & 0 deletions server/services/relayedTransactions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package services

import (
"encoding/hex"
"encoding/json"
"math/big"
"strings"

"github.com/multiversx/mx-chain-core-go/data/transaction"
)

// innerTransactionOfRelayedV1 is used to parse the inner transaction of a relayed V1 transaction, and holds only the fields handled by Rosetta.
type innerTransactionOfRelayedV1 struct {
Nonce uint64 `json:"nonce"`
Value big.Int `json:"value"`
ReceiverPubKey []byte `json:"receiver"`
SenderPubKey []byte `json:"sender"`
}

func isRelayedV1Transaction(tx *transaction.ApiTransactionResult) bool {
return (tx.Type == string(transaction.TxTypeNormal)) &&
(tx.ProcessingTypeOnSource == transactionProcessingTypeRelayed) &&
(tx.ProcessingTypeOnDestination == transactionProcessingTypeRelayed)
}

func parseInnerTxOfRelayedV1(tx *transaction.ApiTransactionResult) (*innerTransactionOfRelayedV1, error) {
subparts := strings.Split(string(tx.Data), argumentsSeparator)
if len(subparts) != 2 {
return nil, errCannotParseRelayedV1
}

innerTxPayloadDecoded, err := hex.DecodeString(subparts[1])
if err != nil {
return nil, err
}

var innerTx innerTransactionOfRelayedV1

err = json.Unmarshal(innerTxPayloadDecoded, &innerTx)
if err != nil {
return nil, err
}

return &innerTx, nil
}
50 changes: 50 additions & 0 deletions server/services/relayedTransactions_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package services

import (
"testing"

"github.com/multiversx/mx-chain-core-go/data/transaction"
"github.com/multiversx/mx-chain-rosetta/testscommon"
"github.com/stretchr/testify/require"
)

func Test_IsRelayedV1Transaction(t *testing.T) {
t.Run("arbitrary tx", func(t *testing.T) {
tx := &transaction.ApiTransactionResult{}
require.False(t, isRelayedV1Transaction(tx))
})

t.Run("relayed v1 tx", func(t *testing.T) {
tx := &transaction.ApiTransactionResult{
Type: string(transaction.TxTypeNormal),
ProcessingTypeOnSource: transactionProcessingTypeRelayed,
ProcessingTypeOnDestination: transactionProcessingTypeRelayed,
}

require.True(t, isRelayedV1Transaction(tx))
})
}

func Test_ParseInnerTxOfRelayedV1(t *testing.T) {
t.Run("arbitrary tx", func(t *testing.T) {
tx := &transaction.ApiTransactionResult{}
innerTx, err := parseInnerTxOfRelayedV1(tx)
require.ErrorIs(t, err, errCannotParseRelayedV1)
require.Nil(t, innerTx)
})

t.Run("relayed v1 tx (Alice to Bob, 1 EGLD)", func(t *testing.T) {
tx := &transaction.ApiTransactionResult{
Data: []byte("relayedTx@7b226e6f6e6365223a372c2273656e646572223a2241546c484c76396f686e63616d433877673970645168386b77704742356a6949496f3349484b594e6165453d222c227265636569766572223a2267456e574f65576d6d413063306a6b71764d354241707a61644b46574e534f69417643575163776d4750673d222c2276616c7565223a313030303030303030303030303030303030302c226761735072696365223a313030303030303030302c226761734c696d6974223a35303030302c2264617461223a22222c227369676e6174757265223a222b4161696451714c4d6150314b4f414d42506a557554774955775137724f6d62586976446c6b4944775a315a48353053366377714a4163576a496a744f732f435177502b79597a6643356730637571526b55437842413d3d222c22636861696e4944223a224d513d3d222c2276657273696f6e223a327d"),
}

innerTx, err := parseInnerTxOfRelayedV1(tx)
require.NoError(t, err)
require.NotNil(t, innerTx)

require.Equal(t, uint64(7), innerTx.Nonce)
require.Equal(t, "1000000000000000000", innerTx.Value.String())
require.Equal(t, testscommon.TestPubKeyAlice, innerTx.SenderPubKey)
require.Equal(t, testscommon.TestPubKeyBob, innerTx.ReceiverPubKey)
})
}
15 changes: 15 additions & 0 deletions server/services/transactionEventsController.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,21 @@ func (controller *transactionEventsController) extractEventTransferValueOnly(tx
}, nil
}

func (controller *transactionEventsController) hasAnySignalError(tx *transaction.ApiTransactionResult) bool {
if !controller.hasEvents(tx) {
return false
}

for _, event := range tx.Logs.Events {
isSignalError := event.Identifier == transactionEventSignalError
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can use this const from core core.SignalErrorOperation

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, fixed.

if isSignalError {
return true
}
}

return false
}

func (controller *transactionEventsController) hasSignalErrorOfSendingValueToNonPayableContract(tx *transaction.ApiTransactionResult) bool {
if !controller.hasEvents(tx) {
return false
Expand Down
39 changes: 39 additions & 0 deletions server/services/transactionEventsController_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,33 @@ import (
"github.com/stretchr/testify/require"
)

func TestTransactionEventsController_HasAnySignalError(t *testing.T) {
networkProvider := testscommon.NewNetworkProviderMock()
controller := newTransactionEventsController(networkProvider)

t.Run("arbitrary tx", func(t *testing.T) {
tx := &transaction.ApiTransactionResult{}
txMatches := controller.hasAnySignalError(tx)
require.False(t, txMatches)
})

t.Run("tx with event 'signalError'", func(t *testing.T) {
tx := &transaction.ApiTransactionResult{
Logs: &transaction.ApiLogs{

Events: []*transaction.Events{
{
Identifier: transactionEventSignalError,
},
},
},
}

txMatches := controller.hasAnySignalError(tx)
require.True(t, txMatches)
})
}

func TestTransactionEventsController_HasSignalErrorOfSendingValueToNonPayableContract(t *testing.T) {
networkProvider := testscommon.NewNetworkProviderMock()
controller := newTransactionEventsController(networkProvider)
Expand Down Expand Up @@ -85,6 +112,18 @@ func TestTransactionEventsController_HasSignalErrorOfMetaTransactionIsInvalid(t
})
}

func Test(t *testing.T) {
event := transaction.Events{
Identifier: transactionEventSignalError,
Topics: [][]byte{
[]byte("foo"),
},
}

require.True(t, eventHasTopic(&event, "foo"))
require.False(t, eventHasTopic(&event, "bar"))
}

func TestEventHasTopic(t *testing.T) {
event := transaction.Events{
Identifier: transactionEventSignalError,
Expand Down
5 changes: 1 addition & 4 deletions server/services/transactionFilters.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,7 @@ func filterOutIntrashardRelayedTransactionAlreadyHeldInInvalidMiniblock(txs []*t
}

for _, tx := range txs {
isRelayedTransaction := (tx.Type == string(transaction.TxTypeNormal)) &&
(tx.ProcessingTypeOnSource == transactionProcessingTypeRelayed) &&
(tx.ProcessingTypeOnDestination == transactionProcessingTypeRelayed)

isRelayedTransaction := isRelayedV1Transaction(tx)
_, alreadyHeldInInvalidMiniblock := invalidTxs[tx.Hash]

if isRelayedTransaction && alreadyHeldInInvalidMiniblock {
Expand Down
25 changes: 21 additions & 4 deletions server/services/transactionsFeaturesDetector.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,19 @@ import (
)

type transactionsFeaturesDetector struct {
networkProvider NetworkProvider
eventsController *transactionEventsController
}

func newTransactionsFeaturesDetector(provider NetworkProvider) *transactionsFeaturesDetector {
return &transactionsFeaturesDetector{
networkProvider: provider,
eventsController: newTransactionEventsController(provider),
}
}

// Example SCRs can be found here: https://api.multiversx.com/transactions?function=ClaimDeveloperRewards
func (extractor *transactionsFeaturesDetector) doesContractResultHoldRewardsOfClaimDeveloperRewards(
func (detector *transactionsFeaturesDetector) doesContractResultHoldRewardsOfClaimDeveloperRewards(
contractResult *transaction.ApiTransactionResult,
allTransactionsInBlock []*transaction.ApiTransactionResult,
) bool {
Expand All @@ -44,7 +46,7 @@ func (extractor *transactionsFeaturesDetector) doesContractResultHoldRewardsOfCl
// that only consume the "data movement" component of the gas:
// - "sending value to non-payable contract"
// - "meta transaction is invalid"
func (extractor *transactionsFeaturesDetector) isInvalidTransactionOfTypeMoveBalanceThatOnlyConsumesDataMovementGas(tx *transaction.ApiTransactionResult) bool {
func (detector *transactionsFeaturesDetector) isInvalidTransactionOfTypeMoveBalanceThatOnlyConsumesDataMovementGas(tx *transaction.ApiTransactionResult) bool {
isInvalid := tx.Type == string(transaction.TxTypeInvalid)
isMoveBalance := tx.ProcessingTypeOnSource == transactionProcessingTypeMoveBalance && tx.ProcessingTypeOnDestination == transactionProcessingTypeMoveBalance

Expand All @@ -53,7 +55,22 @@ func (extractor *transactionsFeaturesDetector) isInvalidTransactionOfTypeMoveBal
}

// TODO: Analyze whether we can simplify the conditions below, or possibly discard them completely / replace them with simpler ones.
withSendingValueToNonPayableContract := extractor.eventsController.hasSignalErrorOfSendingValueToNonPayableContract(tx)
withMetaTransactionIsInvalid := extractor.eventsController.hasSignalErrorOfMetaTransactionIsInvalid(tx)
withSendingValueToNonPayableContract := detector.eventsController.hasSignalErrorOfSendingValueToNonPayableContract(tx)
withMetaTransactionIsInvalid := detector.eventsController.hasSignalErrorOfMetaTransactionIsInvalid(tx)
return withSendingValueToNonPayableContract || withMetaTransactionIsInvalid
}

func (detector *transactionsFeaturesDetector) isRelayedTransactionCompletelyIntrashardWithSignalError(tx *transaction.ApiTransactionResult, innerTx *innerTransactionOfRelayedV1) bool {
innerTxSenderShard := detector.networkProvider.ComputeShardIdOfPubKey(innerTx.SenderPubKey)
innerTxReceiverShard := detector.networkProvider.ComputeShardIdOfPubKey(innerTx.ReceiverPubKey)

isCompletelyIntrashard := tx.SourceShard == tx.DestinationShard &&
innerTxSenderShard == innerTxReceiverShard &&
innerTxSenderShard == tx.SourceShard
if !isCompletelyIntrashard {
return false
}

isWithSignalError := detector.eventsController.hasAnySignalError(tx)
return isWithSignalError
}
Loading
Loading