From c1d145cd9a573a021cadad877ef36ac09da5f32c Mon Sep 17 00:00:00 2001 From: miiu Date: Wed, 17 Jun 2026 09:20:33 +0300 Subject: [PATCH 1/5] check for duplicated txs --- process/block/baseProcess.go | 24 ++++ process/block/baseProcess_test.go | 184 ++++++++++++++++++++++++++++++ process/errors.go | 3 + 3 files changed, 211 insertions(+) diff --git a/process/block/baseProcess.go b/process/block/baseProcess.go index 45ab3688c5..751bf45046 100644 --- a/process/block/baseProcess.go +++ b/process/block/baseProcess.go @@ -1259,6 +1259,13 @@ func (bp *baseProcessor) checkHeaderBodyCorrelation(miniBlockHeaders []data.Mini return process.ErrDuplicatedHashInBlock } + if proposal { + err := checkForDuplicatedTxHashes(body) + if err != nil { + return err + } + } + var mbHdr data.MiniBlockHeaderHandler var miniBlock *block.MiniBlock var mbHash []byte @@ -1299,6 +1306,23 @@ func (bp *baseProcessor) checkHeaderBodyCorrelation(miniBlockHeaders []data.Mini return nil } +func checkForDuplicatedTxHashes(body *block.Body) error { + txHashesSeen := make(map[string]struct{}) + for _, miniBlock := range body.MiniBlocks { + if miniBlock == nil { + continue + } + for _, txHash := range miniBlock.TxHashes { + txHashStr := string(txHash) + if _, ok := txHashesSeen[txHashStr]; ok { + return process.ErrDuplicatedTransactionInBlockBody + } + txHashesSeen[txHashStr] = struct{}{} + } + } + return nil +} + // checkConstructionStateProcessingTypeAndIndexesCorrectness validates the (miniBlock, // miniBlockHeader) pair belonging to a block of shard blockShardID against the legal // (hdrPT, sender == blockShardID?, allowed state) rows. PartialExecuted is allowed alongside diff --git a/process/block/baseProcess_test.go b/process/block/baseProcess_test.go index da362d74e3..a7374462c4 100644 --- a/process/block/baseProcess_test.go +++ b/process/block/baseProcess_test.go @@ -4592,6 +4592,190 @@ func TestCheckHeaderBodyCorrelationProposal(t *testing.T) { ) require.NoError(t, err) }) + + t.Run("duplicate tx hash across miniblocks with proposal should error", func(t *testing.T) { + coreComponents, dataComponents, bootstrapComponents, statusComponents := createComponentHolderMocks() + coreComponents.Hash = &hashingMocks.HasherMock{} + bootstrapComponents.Coordinator, _ = sharding.NewMultiShardCoordinator(3, 0) + arguments := CreateMockArguments(coreComponents, dataComponents, bootstrapComponents, statusComponents) + bp, _ := blproc.NewShardProcessor(arguments) + + miniBlock1 := &block.MiniBlock{ + SenderShardID: 0, + ReceiverShardID: 1, + TxHashes: [][]byte{[]byte("tx1"), []byte("tx2")}, + Type: block.TxBlock, + Reserved: nil, + } + miniBlock2 := &block.MiniBlock{ + SenderShardID: 0, + ReceiverShardID: 2, + TxHashes: [][]byte{[]byte("tx1")}, + Type: block.TxBlock, + Reserved: nil, + } + + mbHash1, _ := core.CalculateHash(coreComponents.IntMarsh, coreComponents.Hash, miniBlock1) + mbHash2, _ := core.CalculateHash(coreComponents.IntMarsh, coreComponents.Hash, miniBlock2) + + mbHeaders := make([]data.MiniBlockHeaderHandler, 2) + mbHeaders[0] = &block.MiniBlockHeader{ + Hash: mbHash1, + SenderShardID: 0, + ReceiverShardID: 1, + TxCount: 2, + Type: block.TxBlock, + Reserved: nil, + } + _ = mbHeaders[0].SetConstructionState(int32(block.Proposed)) + _ = mbHeaders[0].SetProcessingType(int32(block.Normal)) + mbHeaders[1] = &block.MiniBlockHeader{ + Hash: mbHash2, + SenderShardID: 0, + ReceiverShardID: 2, + TxCount: 1, + Type: block.TxBlock, + Reserved: nil, + } + _ = mbHeaders[1].SetConstructionState(int32(block.Proposed)) + _ = mbHeaders[1].SetProcessingType(int32(block.Normal)) + + err := bp.CheckHeaderBodyCorrelationProposal( + mbHeaders, + &block.Body{MiniBlocks: []*block.MiniBlock{miniBlock1, miniBlock2}}, + shardID, + ) + require.Equal(t, process.ErrDuplicatedTransactionInBlockBody, err) + }) + + t.Run("duplicate tx hash within single miniblock with proposal should error", func(t *testing.T) { + coreComponents, dataComponents, bootstrapComponents, statusComponents := createComponentHolderMocks() + coreComponents.Hash = &hashingMocks.HasherMock{} + bootstrapComponents.Coordinator, _ = sharding.NewMultiShardCoordinator(3, 0) + arguments := CreateMockArguments(coreComponents, dataComponents, bootstrapComponents, statusComponents) + bp, _ := blproc.NewShardProcessor(arguments) + + miniBlock := &block.MiniBlock{ + SenderShardID: 0, + ReceiverShardID: 1, + TxHashes: [][]byte{[]byte("tx1"), []byte("tx1")}, + Type: block.TxBlock, + Reserved: nil, + } + + mbHash, _ := core.CalculateHash(coreComponents.IntMarsh, coreComponents.Hash, miniBlock) + + mbHeaders := make([]data.MiniBlockHeaderHandler, 1) + mbHeaders[0] = &block.MiniBlockHeader{ + Hash: mbHash, + SenderShardID: 0, + ReceiverShardID: 1, + TxCount: 2, + Type: block.TxBlock, + Reserved: nil, + } + _ = mbHeaders[0].SetConstructionState(int32(block.Proposed)) + _ = mbHeaders[0].SetProcessingType(int32(block.Normal)) + + err := bp.CheckHeaderBodyCorrelationProposal( + mbHeaders, + &block.Body{MiniBlocks: []*block.MiniBlock{miniBlock}}, + shardID, + ) + require.Equal(t, process.ErrDuplicatedTransactionInBlockBody, err) + }) + + t.Run("duplicate tx hash across miniblocks without proposal should not error", func(t *testing.T) { + coreComponents, dataComponents, bootstrapComponents, statusComponents := createComponentHolderMocks() + coreComponents.Hash = &hashingMocks.HasherMock{} + bootstrapComponents.Coordinator, _ = sharding.NewMultiShardCoordinator(3, 0) + arguments := CreateMockArguments(coreComponents, dataComponents, bootstrapComponents, statusComponents) + bp, _ := blproc.NewShardProcessor(arguments) + + miniBlock1 := &block.MiniBlock{ + SenderShardID: 0, + ReceiverShardID: 1, + TxHashes: [][]byte{[]byte("tx1"), []byte("tx2")}, + Type: block.TxBlock, + Reserved: nil, + } + miniBlock2 := &block.MiniBlock{ + SenderShardID: 0, + ReceiverShardID: 2, + TxHashes: [][]byte{[]byte("tx1")}, + Type: block.TxBlock, + Reserved: nil, + } + + mbHash1, _ := core.CalculateHash(coreComponents.IntMarsh, coreComponents.Hash, miniBlock1) + mbHash2, _ := core.CalculateHash(coreComponents.IntMarsh, coreComponents.Hash, miniBlock2) + + hdr := &block.Header{ + ShardID: shardID, + MiniBlockHeaders: []block.MiniBlockHeader{ + { + Hash: mbHash1, + SenderShardID: 0, + ReceiverShardID: 1, + TxCount: 2, + Type: block.TxBlock, + Reserved: nil, + }, + { + Hash: mbHash2, + SenderShardID: 0, + ReceiverShardID: 2, + TxCount: 1, + Type: block.TxBlock, + Reserved: nil, + }, + }, + } + + err := bp.CheckHeaderBodyCorrelation( + hdr, + &block.Body{MiniBlocks: []*block.MiniBlock{miniBlock1, miniBlock2}}, + ) + require.NoError(t, err) + }) + + t.Run("duplicate tx hash within single miniblock without proposal should not error", func(t *testing.T) { + coreComponents, dataComponents, bootstrapComponents, statusComponents := createComponentHolderMocks() + coreComponents.Hash = &hashingMocks.HasherMock{} + bootstrapComponents.Coordinator, _ = sharding.NewMultiShardCoordinator(3, 0) + arguments := CreateMockArguments(coreComponents, dataComponents, bootstrapComponents, statusComponents) + bp, _ := blproc.NewShardProcessor(arguments) + + miniBlock := &block.MiniBlock{ + SenderShardID: 0, + ReceiverShardID: 1, + TxHashes: [][]byte{[]byte("tx1"), []byte("tx1")}, + Type: block.TxBlock, + Reserved: nil, + } + + mbHash, _ := core.CalculateHash(coreComponents.IntMarsh, coreComponents.Hash, miniBlock) + + hdr := &block.Header{ + ShardID: shardID, + MiniBlockHeaders: []block.MiniBlockHeader{ + { + Hash: mbHash, + SenderShardID: 0, + ReceiverShardID: 1, + TxCount: 2, + Type: block.TxBlock, + Reserved: nil, + }, + }, + } + + err := bp.CheckHeaderBodyCorrelation( + hdr, + &block.Body{MiniBlocks: []*block.MiniBlock{miniBlock}}, + ) + require.NoError(t, err) + }) } func TestBaseProcessor_GetFinalMiniBlocksFromExecutionResult(t *testing.T) { diff --git a/process/errors.go b/process/errors.go index 68b2860c9d..41f1237a67 100644 --- a/process/errors.go +++ b/process/errors.go @@ -1395,6 +1395,9 @@ var ErrDuplicatedHashInBlock = errors.New("duplicated hash in block") // ErrDoubleTransactionsFound signals that double transactions found var ErrDoubleTransactionsFound = errors.New("double transactions found") +// ErrDuplicatedTransactionInBlockBody signals that a transaction hash appears in more than one miniblock of the block body +var ErrDuplicatedTransactionInBlockBody = errors.New("duplicated transaction in block body") + // ErrPeerAlreadyAuthenticated signals that a peer authentication message was received for a peer that already has an existing mapping var ErrPeerAlreadyAuthenticated = errors.New("peer already authenticated") From 0479ea64331ab7c1cb6a8ab20cd00605ed8d5803 Mon Sep 17 00:00:00 2001 From: miiu Date: Wed, 17 Jun 2026 20:39:24 +0300 Subject: [PATCH 2/5] test --- node/chainSimulator/chainSimulator_test.go | 109 +++++++++++++++++++++ 1 file changed, 109 insertions(+) diff --git a/node/chainSimulator/chainSimulator_test.go b/node/chainSimulator/chainSimulator_test.go index 45b656d9ca..3275568703 100644 --- a/node/chainSimulator/chainSimulator_test.go +++ b/node/chainSimulator/chainSimulator_test.go @@ -1,6 +1,8 @@ package chainSimulator import ( + "bytes" + "encoding/hex" "fmt" "math/big" "strings" @@ -11,6 +13,7 @@ import ( apiBlock "github.com/multiversx/mx-chain-core-go/data/api" "github.com/multiversx/mx-chain-core-go/data/block" "github.com/multiversx/mx-chain-core-go/data/transaction" + "github.com/multiversx/mx-chain-go/node/external/transactionAPI" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -755,6 +758,112 @@ func TestSimulator_SendMoveBalanceTxBeforeAndAfterSupernovaWithMoreGasLimit(t *t chainSimulatorCommon.GenerateMoveBalanceTxsInShardsWithMoreGasLimit(t, chainSimulator) } +// TestRemoveSCRFromPoolAndDestinationShouldBeRequested checks that, after an +// ESDT issue SCR is manually removed from the pools, the destination shard +// requests it again and the SCR becomes available through the API. +func TestRemoveSCRFromPoolAndDestinationShouldBeRequested(t *testing.T) { + activationEpoch := uint32(4) + + baseIssuingCost := "1000" + + cs, err := NewChainSimulator(ArgsChainSimulator{ + BypassTxSignatureCheck: true, + BypassCreateBlockTimeCheck: true, + TempDir: t.TempDir(), + PathToInitialConfig: defaultPathToInitialConfig, + NumOfShards: defaultNumOfShards, + RoundDurationInMillis: defaultRoundDurationInMillis, + SupernovaRoundDurationInMillis: defaultSupernovaRoundDurationInMillis, + RoundsPerEpoch: defaultRoundsPerEpoch, + SupernovaRoundsPerEpoch: defaultSupernovaRoundsPerEpoch, + ApiInterface: api.NewNoApiInterface(), + MinNodesPerShard: defaultMinNodesPerShard, + MetaChainMinNodes: defaultMetaChainMinNodes, + AlterConfigsFunction: func(cfg *config.Configs) { + cfg.EpochConfig.EnableEpochs.StakingV2EnableEpoch = 0 + cfg.SystemSCConfig.ESDTSystemSCConfig.BaseIssuingCost = baseIssuingCost + cfg.EpochConfig.EnableEpochs.SupernovaEnableEpoch = uint32(2) + cfg.RoundConfig.RoundActivations[string(common.SupernovaRoundFlag)] = config.ActivationRoundByName{ + Round: "46", + } + + }, + }) + require.Nil(t, err) + require.NotNil(t, cs) + + defer cs.Close() + + wallet0, err := cs.GenerateAndMintWalletAddress(0, chainSimulatorCommon.OneEGLD) + require.Nil(t, err) + + err = cs.GenerateBlocksUntilEpochIsReached(int32(activationEpoch)) + require.Nil(t, err) + + nftTicker := []byte("NFTTICKER") + nonce := uint64(0) + + callValue, _ := big.NewInt(0).SetString(baseIssuingCost, 10) + + txDataField := bytes.Join( + [][]byte{ + []byte("issueNonFungible"), + []byte(hex.EncodeToString([]byte("asdname"))), + []byte(hex.EncodeToString(nftTicker)), + }, + []byte("@"), + ) + + tx := &transaction.Transaction{ + Nonce: nonce, + SndAddr: wallet0.Bytes, + RcvAddr: core.ESDTSCAddress, + GasLimit: 100_000_000, + GasPrice: 1_000_000_000, + Signature: []byte("dummySig"), + Data: txDataField, + Value: callValue, + ChainID: []byte(configs.ChainID), + Version: 1, + } + + txResult, err := cs.SendTxAndGenerateBlockTilTxIsExecuted(tx, 10) + require.Nil(t, err) + require.NotNil(t, txResult) + require.Equal(t, "success", txResult.Status.String()) + + // SCRS remove from pool + keys := cs.GetNodeHandler(core.MetachainShardId).GetDataComponents().Datapool().UnsignedTransactions().Keys() + scrsForShardZero := cs.GetNodeHandler(0).GetDataComponents().Datapool().UnsignedTransactions().Keys() + for _, key := range keys { + cs.GetNodeHandler(core.MetachainShardId).GetDataComponents().Datapool().UnsignedTransactions().RemoveDataFromAllShards(key) + cs.GetNodeHandler(0).GetDataComponents().Datapool().UnsignedTransactions().RemoveDataFromAllShards(key) + } + + scrHash := scrsForShardZero[0] + res, err := cs.GetNodeHandler(0).GetFacadeHandler().GetTransaction(hex.EncodeToString(scrHash), true) + require.Nil(t, res) + require.True(t, strings.Contains(err.Error(), transactionAPI.ErrTransactionNotFound.Error())) + + called := false + count := 0 + for { + count++ + err = cs.GenerateBlocks(1) + require.Nil(t, err) + + res, err = cs.GetNodeHandler(0).GetFacadeHandler().GetTransaction(hex.EncodeToString(scrHash), true) + if res != nil { + called = true + break + } + if count == 100 { + require.FailNow(t, "cannot find SCR on the destination shard") + } + } + require.True(t, called) +} + func TestChainSimulator_VerifyEconomicsMetricsSupernova(t *testing.T) { if testing.Short() { t.Skip("this is not a short test") From 70f0ef3cfc2ccb937ce7c325791e615fc74275e1 Mon Sep 17 00:00:00 2001 From: miiu Date: Thu, 18 Jun 2026 09:57:29 +0300 Subject: [PATCH 3/5] fix linter --- node/chainSimulator/chainSimulator_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node/chainSimulator/chainSimulator_test.go b/node/chainSimulator/chainSimulator_test.go index 3275568703..ad710560d9 100644 --- a/node/chainSimulator/chainSimulator_test.go +++ b/node/chainSimulator/chainSimulator_test.go @@ -852,7 +852,7 @@ func TestRemoveSCRFromPoolAndDestinationShouldBeRequested(t *testing.T) { err = cs.GenerateBlocks(1) require.Nil(t, err) - res, err = cs.GetNodeHandler(0).GetFacadeHandler().GetTransaction(hex.EncodeToString(scrHash), true) + res, _ = cs.GetNodeHandler(0).GetFacadeHandler().GetTransaction(hex.EncodeToString(scrHash), true) if res != nil { called = true break From 5a22582e78b340ac237513c5bd56f9699f310ae4 Mon Sep 17 00:00:00 2001 From: miiu Date: Thu, 18 Jun 2026 10:24:23 +0300 Subject: [PATCH 4/5] fix comment --- process/errors.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/process/errors.go b/process/errors.go index ecf2df8f44..2aef2176c5 100644 --- a/process/errors.go +++ b/process/errors.go @@ -1395,7 +1395,7 @@ var ErrDuplicatedHashInBlock = errors.New("duplicated hash in block") // ErrDoubleTransactionsFound signals that double transactions found var ErrDoubleTransactionsFound = errors.New("double transactions found") -// ErrDuplicatedTransactionInBlockBody signals that a transaction hash appears in more than one miniblock of the block body +// ErrDuplicatedTransactionInBlockBody signals that a transaction hash appears more than once in the block body var ErrDuplicatedTransactionInBlockBody = errors.New("duplicated transaction in block body") // ErrPeerAlreadyAuthenticated signals that a peer authentication message was received for a peer that already has an existing mapping From 40b753fd3f3fbac7ffa73d6dc0e5c310c4f064fa Mon Sep 17 00:00:00 2001 From: miiu Date: Thu, 18 Jun 2026 10:57:28 +0300 Subject: [PATCH 5/5] remove proposal condition --- process/block/baseProcess.go | 12 +++++------- process/block/baseProcess_test.go | 8 ++++---- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/process/block/baseProcess.go b/process/block/baseProcess.go index 751bf45046..768542c571 100644 --- a/process/block/baseProcess.go +++ b/process/block/baseProcess.go @@ -1259,13 +1259,6 @@ func (bp *baseProcessor) checkHeaderBodyCorrelation(miniBlockHeaders []data.Mini return process.ErrDuplicatedHashInBlock } - if proposal { - err := checkForDuplicatedTxHashes(body) - if err != nil { - return err - } - } - var mbHdr data.MiniBlockHeaderHandler var miniBlock *block.MiniBlock var mbHash []byte @@ -1303,6 +1296,11 @@ func (bp *baseProcessor) checkHeaderBodyCorrelation(miniBlockHeaders []data.Mini delete(mbHashesFromHdr, mbHashStr) } + err = checkForDuplicatedTxHashes(body) + if err != nil { + return err + } + return nil } diff --git a/process/block/baseProcess_test.go b/process/block/baseProcess_test.go index a7374462c4..a994369190 100644 --- a/process/block/baseProcess_test.go +++ b/process/block/baseProcess_test.go @@ -4685,7 +4685,7 @@ func TestCheckHeaderBodyCorrelationProposal(t *testing.T) { require.Equal(t, process.ErrDuplicatedTransactionInBlockBody, err) }) - t.Run("duplicate tx hash across miniblocks without proposal should not error", func(t *testing.T) { + t.Run("duplicate tx hash across miniblocks without proposal should error", func(t *testing.T) { coreComponents, dataComponents, bootstrapComponents, statusComponents := createComponentHolderMocks() coreComponents.Hash = &hashingMocks.HasherMock{} bootstrapComponents.Coordinator, _ = sharding.NewMultiShardCoordinator(3, 0) @@ -4736,10 +4736,10 @@ func TestCheckHeaderBodyCorrelationProposal(t *testing.T) { hdr, &block.Body{MiniBlocks: []*block.MiniBlock{miniBlock1, miniBlock2}}, ) - require.NoError(t, err) + require.Equal(t, process.ErrDuplicatedTransactionInBlockBody, err) }) - t.Run("duplicate tx hash within single miniblock without proposal should not error", func(t *testing.T) { + t.Run("duplicate tx hash within single miniblock without proposal should error", func(t *testing.T) { coreComponents, dataComponents, bootstrapComponents, statusComponents := createComponentHolderMocks() coreComponents.Hash = &hashingMocks.HasherMock{} bootstrapComponents.Coordinator, _ = sharding.NewMultiShardCoordinator(3, 0) @@ -4774,7 +4774,7 @@ func TestCheckHeaderBodyCorrelationProposal(t *testing.T) { hdr, &block.Body{MiniBlocks: []*block.MiniBlock{miniBlock}}, ) - require.NoError(t, err) + require.Equal(t, process.ErrDuplicatedTransactionInBlockBody, err) }) }