Skip to content

Commit

Permalink
Merge pull request #81 from multiversx/relayed-25
Browse files Browse the repository at this point in the history
Handle intra-shard relayed transactions with signal error
  • Loading branch information
andreibancioiu authored Sep 4, 2023
2 parents 9417edc + f524b19 commit 0ab9266
Show file tree
Hide file tree
Showing 23 changed files with 545 additions and 38 deletions.
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 {
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
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

0 comments on commit 0ab9266

Please sign in to comment.