diff --git a/.github/workflows/regularly_check_mainnet.yml b/.github/workflows/regularly_check_mainnet.yml index 9fd74e62..c4c0e0f7 100644 --- a/.github/workflows/regularly_check_mainnet.yml +++ b/.github/workflows/regularly_check_mainnet.yml @@ -29,21 +29,21 @@ jobs: - name: check:data (shard 0) run: | - python3 ./systemtests/check_with_mesh_cli.py --mode=data --network=mainnet --shard=0 --num-blocks=3000 + python3 ./systemtests/check_with_mesh_cli.py --mode=data --network=mainnet --shard=0 --num-blocks=5000 env: MAINNET_PROXY_URL: ${{ secrets.MAINNET_PROXY_URL }} PYTHONPATH: . - name: check:data (shard 1) run: | - python3 ./systemtests/check_with_mesh_cli.py --mode=data --network=mainnet --shard=1 --num-blocks=3000 + python3 ./systemtests/check_with_mesh_cli.py --mode=data --network=mainnet --shard=1 --num-blocks=5000 env: MAINNET_PROXY_URL: ${{ secrets.MAINNET_PROXY_URL }} PYTHONPATH: . - name: check:data (shard 2) run: | - python3 ./systemtests/check_with_mesh_cli.py --mode=data --network=mainnet --shard=2 --num-blocks=3000 + python3 ./systemtests/check_with_mesh_cli.py --mode=data --network=mainnet --shard=2 --num-blocks=5000 env: MAINNET_PROXY_URL: ${{ secrets.MAINNET_PROXY_URL }} PYTHONPATH: . diff --git a/server/services/operations.go b/server/services/operations.go index 11c4fe44..00bc6d6f 100644 --- a/server/services/operations.go +++ b/server/services/operations.go @@ -5,17 +5,16 @@ import ( ) const ( - opGenesisBalanceMovement = "GenesisBalanceMovement" - opTransfer = "Transfer" - opFee = "Fee" - opReward = "Reward" - opScResult = "SmartContractResult" - opFeeRefundAsScResult = "FeeRefundAsSmartContractResult" - opDeveloperRewardsAsScResult = "DeveloperRewardsAsSmartContractResult" - opDeveloperRewards = "DeveloperRewards" - opFeeOfInvalidTx = "FeeOfInvalidTransaction" - opFeeRefund = "FeeRefund" - opCustomTransfer = "CustomTransfer" + opGenesisBalanceMovement = "GenesisBalanceMovement" + opTransfer = "Transfer" + opFee = "Fee" + opReward = "Reward" + opScResult = "SmartContractResult" + opFeeRefundAsScResult = "FeeRefundAsSmartContractResult" + opDeveloperRewards = "DeveloperRewards" + opFeeOfInvalidTx = "FeeOfInvalidTransaction" + opFeeRefund = "FeeRefund" + opCustomTransfer = "CustomTransfer" ) var ( @@ -27,7 +26,6 @@ var ( opReward, opScResult, opFeeRefundAsScResult, - opDeveloperRewardsAsScResult, opDeveloperRewards, opFeeOfInvalidTx, opFeeRefund, diff --git a/server/services/testdata/blocks_with_claim_developer_rewards.json b/server/services/testdata/blocks_with_claim_developer_rewards.json index 1ed7360c..01f7123b 100644 --- a/server/services/testdata/blocks_with_claim_developer_rewards.json +++ b/server/services/testdata/blocks_with_claim_developer_rewards.json @@ -8,30 +8,48 @@ "type": "normal", "processingTypeOnSource": "BuiltInFunctionCall", "processingTypeOnDestination": "BuiltInFunctionCall", - "hash": "96b1a0533ff17df3d1777889117023d0b178cc80fa72535d1b8ec1a13bcf3a75", - "epoch": 20, + "hash": "e0cb3e108c4e04a6d2406d718c20f976b365de3d1dd0067fd2fae996f7c9cbd1", + "nonce": 536, + "round": 1092226, + "epoch": 909, "value": "0", - "receiver": "erd1qqqqqqqqqqqqqpgqagjekf5mxv86hy5c62vvtug5vc6jmgcsq6uq8reras", + "receiver": "erd1qqqqqqqqqqqqqpgq2kndhnp65u9jvg79pz6luthf2tcfc9f6q6uq28n2n9", "sender": "erd1tn62hjp72rznp8vq0lplva5csav6rccpqqdungpxtqz0g2hcq6uq9k4cc6", + "gasPrice": 1000000000, + "gasLimit": 8000000, "data": "Q2xhaW1EZXZlbG9wZXJSZXdhcmRz", - "signature": "132aa54ec093c21e2d0d447bf9a9b1da5c33d4120da66fb53dbc78d1d3d007e2314fa06348302f503e96a91b697dd943189df0ad4c3750416a704de2ac007e06", + "signature": "b8771fe3f0184bed47f002543f5302b93bfeace7cf1c3ef138522e4c53e162745359d4eb4fa2cf40036ebc1c4db9e8269ea5b81047aa6d9f6965c5792d84e40e", "sourceShard": 0, "destinationShard": 0, + "miniblockType": "TxBlock", + "miniblockHash": "2684c32382e5acd3912d8ae7adfa934e1950d24927f0b99baae6617182a1eae8", "logs": { - "address": "erd1qqqqqqqqqqqqqpgqagjekf5mxv86hy5c62vvtug5vc6jmgcsq6uq8reras", + "address": "erd1qqqqqqqqqqqqqpgq2kndhnp65u9jvg79pz6luthf2tcfc9f6q6uq28n2n9", "events": [ { - "address": "erd1qqqqqqqqqqqqqpgqagjekf5mxv86hy5c62vvtug5vc6jmgcsq6uq8reras", + "address": "erd1qqqqqqqqqqqqqpgq2kndhnp65u9jvg79pz6luthf2tcfc9f6q6uq28n2n9", + "identifier": "ClaimDeveloperRewards", + "topics": [ + "AZ012mtA", + "XPSryD5QxTCdgH/D9naYh1mh4wEAG8mgJlgE9Cr4Brg=" + ], + "data": null, + "additionalData": null + }, + { + "address": "erd1qqqqqqqqqqqqqpgq2kndhnp65u9jvg79pz6luthf2tcfc9f6q6uq28n2n9", "identifier": "completedTxEvent", "topics": [ - "lrGgUz/xffPRd3iJEXAj0LF4zID6clNdG47BoTvPOnU=" + "4Ms+EIxOBKbSQG1xjCD5drNl3j0d0AZ/0vrplvfJy9E=" ], "data": null, "additionalData": null } ] }, + "status": "success", "operation": "ClaimDeveloperRewards", + "function": "ClaimDeveloperRewards", "initiallyPaidFee": "160685000000000" } ] @@ -42,33 +60,50 @@ "type": "unsigned", "processingTypeOnSource": "MoveBalance", "processingTypeOnDestination": "MoveBalance", - "hash": "d05c5f65f564d740aa1e81f7a96581d739783a43c232a6c86112afa1e6c318c4", - "epoch": 20, + "hash": "7969b30a33e9d925cacda530a4afa74269cd4e366f0598486e23b70f5e10cf98", + "nonce": 0, + "round": 1092226, + "epoch": 909, "value": "1774725000000", "receiver": "erd1tn62hjp72rznp8vq0lplva5csav6rccpqqdungpxtqz0g2hcq6uq9k4cc6", "sender": "erd1tn62hjp72rznp8vq0lplva5csav6rccpqqdungpxtqz0g2hcq6uq9k4cc6", - "previousTransactionHash": "96b1a0533ff17df3d1777889117023d0b178cc80fa72535d1b8ec1a13bcf3a75", - "originalTransactionHash": "96b1a0533ff17df3d1777889117023d0b178cc80fa72535d1b8ec1a13bcf3a75", + "gasPrice": 1000000000, + "previousTransactionHash": "e0cb3e108c4e04a6d2406d718c20f976b365de3d1dd0067fd2fae996f7c9cbd1", + "originalTransactionHash": "e0cb3e108c4e04a6d2406d718c20f976b365de3d1dd0067fd2fae996f7c9cbd1", "originalSender": "erd1tn62hjp72rznp8vq0lplva5csav6rccpqqdungpxtqz0g2hcq6uq9k4cc6", "sourceShard": 0, - "destinationShard": 0 + "destinationShard": 0, + "miniblockType": "SmartContractResultBlock", + "miniblockHash": "8ba6961a4191dc4f42296eab4f68a56048fd257c943f4e787e31befb8b155d84", + "status": "success", + "operation": "transfer", + "callType": "directCall", + "options": 0 }, { "type": "unsigned", "processingTypeOnSource": "MoveBalance", "processingTypeOnDestination": "MoveBalance", - "hash": "bc89442d5e77113f0b4e7383e5d078776fc6724690ba9f98d1704202c324e090", - "epoch": 20, + "hash": "06f81486996225b597bc7ed89b4062cd895491b33b1e81f34205ef48272bd644", + "nonce": 537, + "round": 1092226, + "epoch": 909, "value": "29185000000000", "receiver": "erd1tn62hjp72rznp8vq0lplva5csav6rccpqqdungpxtqz0g2hcq6uq9k4cc6", - "sender": "erd1qqqqqqqqqqqqqpgqagjekf5mxv86hy5c62vvtug5vc6jmgcsq6uq8reras", - "previousTransactionHash": "96b1a0533ff17df3d1777889117023d0b178cc80fa72535d1b8ec1a13bcf3a75", - "originalTransactionHash": "96b1a0533ff17df3d1777889117023d0b178cc80fa72535d1b8ec1a13bcf3a75", + "sender": "erd1qqqqqqqqqqqqqpgq2kndhnp65u9jvg79pz6luthf2tcfc9f6q6uq28n2n9", + "gasPrice": 1000000000, + "data": "QDZmNmI=", + "previousTransactionHash": "e0cb3e108c4e04a6d2406d718c20f976b365de3d1dd0067fd2fae996f7c9cbd1", + "originalTransactionHash": "e0cb3e108c4e04a6d2406d718c20f976b365de3d1dd0067fd2fae996f7c9cbd1", "sourceShard": 0, "destinationShard": 0, - "nonce": 29, - "data": "QDZmNmI=", - "isRefund": true + "miniblockType": "SmartContractResultBlock", + "miniblockHash": "8ba6961a4191dc4f42296eab4f68a56048fd257c943f4e787e31befb8b155d84", + "status": "success", + "operation": "transfer", + "isRefund": true, + "callType": "directCall", + "options": 0 } ] } diff --git a/server/services/transactionsFeaturesDetector.go b/server/services/transactionsFeaturesDetector.go index 14650070..f9ab6229 100644 --- a/server/services/transactionsFeaturesDetector.go +++ b/server/services/transactionsFeaturesDetector.go @@ -19,6 +19,9 @@ func newTransactionsFeaturesDetector(provider NetworkProvider) *transactionsFeat } // Example SCRs can be found here: https://api.multiversx.com/transactions?function=ClaimDeveloperRewards +// Unfortunately, the network does not provide a way to easily and properly detect whether a SCR +// is the result of claiming developer rewards. Here, we apply a best-effort (and suboptimal) strategy: +// we scan through all the other items in the block, in order to find a parent transaction that matches the operation in question. func (detector *transactionsFeaturesDetector) doesContractResultHoldRewardsOfClaimDeveloperRewards( contractResult *transaction.ApiTransactionResult, allTransactionsInBlock []*transaction.ApiTransactionResult, diff --git a/server/services/transactionsTransformer.go b/server/services/transactionsTransformer.go index 44a03734..6bc9485b 100644 --- a/server/services/transactionsTransformer.go +++ b/server/services/transactionsTransformer.go @@ -142,19 +142,25 @@ func (transformer *transactionsTransformer) unsignedTxToRosettaTx( } } - if !transformer.areClaimDeveloperRewardsEventsEnabled(scr.Epoch) { - // Handle developer rewards in a legacy manner (without looking at events / logs) - if transformer.featuresDetector.doesContractResultHoldRewardsOfClaimDeveloperRewards(scr, txsInBlock) { - return &types.Transaction{ - TransactionIdentifier: hashToTransactionIdentifier(scr.Hash), - Operations: []*types.Operation{ - { - Type: opDeveloperRewardsAsScResult, - Account: addressToAccountIdentifier(scr.Receiver), - Amount: transformer.extension.valueToNativeAmount(scr.Value), - }, - }, - } + // Handle developer rewards: + // + // (a) When the developer rewards are claimed in an intra-shard fashion, the network generates misleading SCRs. + // In addition to the regular refund SCR, there's a SCR that notarizes the rewards as a misleading balance transfer, from the developer to self: + // - https://explorer.multiversx.com/transactions?function=ClaimDeveloperRewards&senderShard=0&receiverShard=0 + // - and so on... + // + // (b) When the developer rewards are claimed in a cross-shard fashion, the network generates misleading SCRs. + // In addition to the regular refund SCR, there's a SCR that notarizes the rewards as a misleading balance transfer, from the contract to the developer: + // - https://explorer.multiversx.com/transactions?function=ClaimDeveloperRewards&senderShard=0&receiverShard=1 + // - and so on ... + // + // Either way, correct transaction events with identifier "ClaimDeveloperRewards" are generated. + // Here, we simply ignore all SCRs which **seem to hold a developer reward**, + // since they are properly handled by "addOperationsGivenTransactionEvents". + if transformer.featuresDetector.doesContractResultHoldRewardsOfClaimDeveloperRewards(scr, txsInBlock) { + return &types.Transaction{ + TransactionIdentifier: hashToTransactionIdentifier(scr.Hash), + Operations: []*types.Operation{}, } } @@ -585,18 +591,16 @@ func (transformer *transactionsTransformer) addOperationsGivenTransactionEvents( rosettaTx.Operations = append(rosettaTx.Operations, operations...) } - if transformer.areClaimDeveloperRewardsEventsEnabled(tx.Epoch) { - for _, event := range eventsClaimDeveloperRewards { - operations := []*types.Operation{ - { - Type: opDeveloperRewards, - Account: addressToAccountIdentifier(event.receiverAddress), - Amount: transformer.extension.valueToNativeAmount(event.value), - }, - } - - rosettaTx.Operations = append(rosettaTx.Operations, operations...) + for _, event := range eventsClaimDeveloperRewards { + operations := []*types.Operation{ + { + Type: opDeveloperRewards, + Account: addressToAccountIdentifier(event.receiverAddress), + Amount: transformer.extension.valueToNativeAmount(event.value), + }, } + + rosettaTx.Operations = append(rosettaTx.Operations, operations...) } return nil @@ -636,7 +640,3 @@ func (transformer *transactionsTransformer) extractOperationsFromEventESDT(event return make([]*types.Operation, 0) } - -func (transformer *transactionsTransformer) areClaimDeveloperRewardsEventsEnabled(epoch uint32) bool { - return transformer.provider.IsReleaseSpicaActive(epoch) -} diff --git a/server/services/transactionsTransformer_test.go b/server/services/transactionsTransformer_test.go index 14273a0a..25717feb 100644 --- a/server/services/transactionsTransformer_test.go +++ b/server/services/transactionsTransformer_test.go @@ -1260,16 +1260,15 @@ func TestTransactionsTransformer_TransformBlockTxsHavingClaimDeveloperRewards(t blocks, err := readTestBlocks("testdata/blocks_with_claim_developer_rewards.json") require.Nil(t, err) - t.Run("recover operations in legacy manner (without using events)", func(t *testing.T) { + t.Run("recover operations using ClaimDeveloperRewards events", func(t *testing.T) { txs, err := transformer.transformBlockTxs(blocks[0]) require.Nil(t, err) - require.Len(t, txs, 3) - require.Len(t, txs[0].Operations, 1) + require.Len(t, txs, 2) + require.Len(t, txs[0].Operations, 2) require.Len(t, txs[1].Operations, 1) - require.Len(t, txs[2].Operations, 1) expectedTx0 := &types.Transaction{ - TransactionIdentifier: hashToTransactionIdentifier("96b1a0533ff17df3d1777889117023d0b178cc80fa72535d1b8ec1a13bcf3a75"), + TransactionIdentifier: hashToTransactionIdentifier("e0cb3e108c4e04a6d2406d718c20f976b365de3d1dd0067fd2fae996f7c9cbd1"), Operations: []*types.Operation{ { Type: opFee, @@ -1278,30 +1277,22 @@ func TestTransactionsTransformer_TransformBlockTxsHavingClaimDeveloperRewards(t Amount: extension.valueToNativeAmount("-160685000000000"), Status: &opStatusSuccess, }, - }, - Metadata: extractTransactionMetadata(blocks[0].MiniBlocks[0].Transactions[0]), - } - - require.Equal(t, expectedTx0, txs[0]) - - expectedTx1 := &types.Transaction{ - TransactionIdentifier: hashToTransactionIdentifier("d05c5f65f564d740aa1e81f7a96581d739783a43c232a6c86112afa1e6c318c4"), - Operations: []*types.Operation{ { - Type: opDeveloperRewardsAsScResult, - OperationIdentifier: indexToOperationIdentifier(0), + Type: opDeveloperRewards, + OperationIdentifier: indexToOperationIdentifier(1), Account: addressToAccountIdentifier("erd1tn62hjp72rznp8vq0lplva5csav6rccpqqdungpxtqz0g2hcq6uq9k4cc6"), Amount: extension.valueToNativeAmount("1774725000000"), Status: &opStatusSuccess, }, }, + Metadata: extractTransactionMetadata(blocks[0].MiniBlocks[0].Transactions[0]), } - require.Equal(t, expectedTx1, txs[1]) + require.Equal(t, expectedTx0, txs[0]) // Fee refund - expectedTx2 := &types.Transaction{ - TransactionIdentifier: hashToTransactionIdentifier("bc89442d5e77113f0b4e7383e5d078776fc6724690ba9f98d1704202c324e090"), + expectedTx1 := &types.Transaction{ + TransactionIdentifier: hashToTransactionIdentifier("06f81486996225b597bc7ed89b4062cd895491b33b1e81f34205ef48272bd644"), Operations: []*types.Operation{ { Type: opFeeRefundAsScResult, @@ -1313,7 +1304,7 @@ func TestTransactionsTransformer_TransformBlockTxsHavingClaimDeveloperRewards(t }, } - require.Equal(t, expectedTx2, txs[2]) + require.Equal(t, expectedTx1, txs[1]) }) }