Skip to content
1 change: 1 addition & 0 deletions factory/api/apiResolverFactory.go
Original file line number Diff line number Diff line change
Expand Up @@ -742,6 +742,7 @@ func createAPIBlockProcessorArgs(args *ApiResolverArgs, apiTransactionHandler ex
EnableEpochsHandler: args.CoreComponents.EnableEpochsHandler(),
ProofsPool: args.DataComponents.Datapool().Proofs(),
BlockChain: args.DataComponents.Blockchain(),
EnableRoundsHandler: args.CoreComponents.EnableRoundsHandler(),
}

return blockApiArgs, nil
Expand Down
2 changes: 1 addition & 1 deletion factory/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -537,7 +537,7 @@ type LogsFacade interface {
type ReceiptsRepository interface {
SaveReceipts(holder common.ReceiptsHolder, header data.HeaderHandler, headerHash []byte) error
SaveReceiptsForExecResult(holder common.ReceiptsHolder, execResult data.BaseExecutionResultHandler) error
LoadReceipts(header data.HeaderHandler, headerHash []byte) (common.ReceiptsHolder, error)
LoadReceipts(receiptsHash []byte, header data.HeaderHandler, headerHash []byte) (common.ReceiptsHolder, error)
IsInterfaceNil() bool
}

Expand Down
1 change: 1 addition & 0 deletions integrationTests/testProcessorNodeWithTestWebServer.go
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,7 @@ func createFacadeComponents(tpn *TestProcessorNode) nodeFacade.ApiResolver {
EnableEpochsHandler: &enableEpochsHandlerMock.EnableEpochsHandlerStub{},
ProofsPool: tpn.ProofsPool,
BlockChain: tpn.BlockChain,
EnableRoundsHandler: tpn.EnableRoundsHandler,
}
blockAPIHandler, err := blockAPI.CreateAPIBlockProcessor(argsBlockAPI)
log.LogIfError(err)
Expand Down
91 changes: 91 additions & 0 deletions node/chainSimulator/chainSimulator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (

"github.com/multiversx/mx-chain-core-go/core"
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/stretchr/testify/assert"
"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -585,6 +586,96 @@ func TestSimulator_SendTransactions(t *testing.T) {
chainSimulatorCommon.CheckGenerateTransactions(t, chainSimulator)
}

func TestSimulator_MoveBalanceCheckReceipt(t *testing.T) {
if testing.Short() {
t.Skip("this is not a short test")
}

chainSimulator, 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.EpochConfig.EnableEpochs.SupernovaEnableEpoch = uint32(2)
cfg.RoundConfig.RoundActivations[string(common.SupernovaRoundFlag)] = config.ActivationRoundByName{
Round: "46",
}
},
})
require.Nil(t, err)
require.NotNil(t, chainSimulator)

defer chainSimulator.Close()

wallet0, err := chainSimulator.GenerateAndMintWalletAddress(0, chainSimulatorCommon.OneEGLD)
require.Nil(t, err)
err = chainSimulator.GenerateBlocks(1)
require.Nil(t, err)

ftx := &transaction.Transaction{
Nonce: 0,
Value: big.NewInt(1),
SndAddr: wallet0.Bytes,
RcvAddr: wallet0.Bytes,
Data: []byte(""),
GasLimit: 100_000,
GasPrice: 1_000_000_000,
ChainID: []byte(configs.ChainID),
Version: 1,
Signature: []byte("010101"),
}

checkReceipts := func(te *testing.T, aB *apiBlock.Block, value string) {
called := false
for _, mb := range aB.MiniBlocks {
if mb.Type == block.ReceiptBlock.String() {
called = true
require.Equal(te, 1, len(mb.Receipts))
require.Equal(te, value, mb.Receipts[0].Value.String())
}
}
require.True(te, called)
}

apiTx, err := chainSimulator.SendTxAndGenerateBlockTilTxIsExecuted(ftx, 10)
require.Nil(t, err)
require.NotNil(t, apiTx)

blockWithTxs, err := chainSimulator.GetNodeHandler(0).GetFacadeHandler().GetBlockByNonce(apiTx.BlockNonce, apiBlock.BlockQueryOptions{
WithTransactions: true,
WithLogs: true,
})
require.Nil(t, err)
require.Equal(t, 2, len(blockWithTxs.MiniBlocks))
checkReceipts(t, blockWithTxs, "50000000000000")

err = chainSimulator.GenerateBlocks(50)
require.Nil(t, err)

ftx.Nonce++
apiTx, err = chainSimulator.SendTxAndGenerateBlockTilTxIsExecuted(ftx, 10)
require.Nil(t, err)
require.NotNil(t, apiTx)

blockWithTxs, err = chainSimulator.GetNodeHandler(0).GetFacadeHandler().GetBlockByNonce(apiTx.BlockNonce, apiBlock.BlockQueryOptions{
WithTransactions: true,
WithLogs: true,
})
require.Nil(t, err)
require.Equal(t, 2, len(blockWithTxs.MiniBlocks))
checkReceipts(t, blockWithTxs, "500000000000")
}

func TestSimulator_SentMoveBalanceNoGasForFee(t *testing.T) {
if testing.Short() {
t.Skip("this is not a short test")
Expand Down
10 changes: 10 additions & 0 deletions node/external/blockAPI/apiBlockFactory_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ func createMockArgsAPIBlockProc() *ArgAPIBlockProcessor {
EnableEpochsHandler: &enableEpochsHandlerMock.EnableEpochsHandlerStub{},
ProofsPool: &dataRetrieverTestCommon.ProofsPoolMock{},
BlockChain: chainHandler,
EnableRoundsHandler: &testscommon.EnableRoundsHandlerStub{},
}
}

Expand Down Expand Up @@ -210,6 +211,15 @@ func TestCreateAPIBlockProcessorNilArgs(t *testing.T) {
_, err := CreateAPIBlockProcessor(arguments)
assert.Equal(t, process.ErrNilBlockChain, err)
})
t.Run("NilEnableRoundsHandler", func(t *testing.T) {
t.Parallel()

arguments := createMockArgsAPIBlockProc()
arguments.EnableRoundsHandler = nil

_, err := CreateAPIBlockProcessor(arguments)
assert.Equal(t, process.ErrNilEnableRoundsHandler, err)
})
}

func TestGetBlockByHash_KeyNotFound(t *testing.T) {
Expand Down
33 changes: 18 additions & 15 deletions node/external/blockAPI/baseBlock.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package blockAPI

import (
"encoding/hex"
"errors"
"fmt"
"math/big"
"strings"
Expand Down Expand Up @@ -63,12 +62,13 @@ type baseAPIBlockProcessor struct {
enableEpochsHandler common.EnableEpochsHandler
proofsPool dataRetriever.ProofsPool
blockchain data.ChainHandler
enableRoundsHandler common.EnableRoundsHandler
}

var log = logger.GetOrCreate("node/blockAPI")

func (bap *baseAPIBlockProcessor) getIntrashardMiniblocksFromReceiptsStorage(header data.HeaderHandler, headerHash []byte, options api.BlockQueryOptions) ([]*api.MiniBlock, error) {
receiptsHolder, err := bap.receiptsRepository.LoadReceipts(header, headerHash)
func (bap *baseAPIBlockProcessor) getIntrashardMiniblocksFromReceiptsStorage(receiptsHash []byte, header data.HeaderHandler, headerHash []byte, options api.BlockQueryOptions) ([]*api.MiniBlock, error) {
receiptsHolder, err := bap.receiptsRepository.LoadReceipts(receiptsHash, header, headerHash)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -234,7 +234,7 @@ func (bap *baseAPIBlockProcessor) getAndAttachTxsToMbByEpoch(
case block.InvalidBlock:
apiMiniblock.Transactions, err = bap.getTxsFromMiniblock(miniBlock, miniblockHash, header, transaction.TxTypeInvalid, dataRetriever.TransactionUnit, firstProcessedTxIndex, lastProcessedTxIndex)
case block.ReceiptBlock:
apiMiniblock.Receipts, err = bap.getReceiptsFromMiniblock(miniBlock, header.GetEpoch())
apiMiniblock.Receipts, err = bap.getReceiptsFromMiniblock(miniBlock, header.GetEpoch(), header.GetRound())
}

if err != nil {
Expand All @@ -251,8 +251,16 @@ func (bap *baseAPIBlockProcessor) getAndAttachTxsToMbByEpoch(
return nil
}

func (bap *baseAPIBlockProcessor) getReceiptsFromMiniblock(miniblock *block.MiniBlock, epoch uint32) ([]*transaction.ApiReceipt, error) {
storer, err := bap.store.GetStorer(dataRetriever.UnsignedTransactionUnit)
func (bap *baseAPIBlockProcessor) getReceiptsStorerUnitType(round uint64) dataRetriever.UnitType {
if bap.enableRoundsHandler.IsFlagEnabledInRound(common.SupernovaRoundFlag, round) {
return dataRetriever.ReceiptsUnit
}
return dataRetriever.UnsignedTransactionUnit
}

func (bap *baseAPIBlockProcessor) getReceiptsFromMiniblock(miniblock *block.MiniBlock, epoch uint32, round uint64) ([]*transaction.ApiReceipt, error) {
unit := bap.getReceiptsStorerUnitType(round)
storer, err := bap.store.GetStorer(unit)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -727,14 +735,8 @@ func proofToAPIProof(proof data.HeaderProofHandler) *api.HeaderProof {
func (bap *baseAPIBlockProcessor) addMbsAndNumTxsAsyncExecution(apiBlock *api.Block, blockHeader data.HeaderHandler, headerHash []byte, options api.BlockQueryOptions) error {
executionResultBytes, err := bap.getFromStorerWithEpoch(dataRetriever.ExecutionResultsUnit, headerHash, blockHeader.GetEpoch())
if err != nil {
// It's possible to have a block without an execution result (transactions from block are not executed yet)
if errors.Is(err, dblookupext.ErrNotFoundInStorage) {
mbs, totalTxs, errG := bap.getMbsAndTxsIfMissingExecutionResult(blockHeader, options)
apiBlock.MiniBlocks = mbs
apiBlock.NumTxs = totalTxs
return errG
}
return err
// do not return a partial block if the execution result is missing
return errBlockNotFound
}

executionResultHandler, err := process.UnmarshalExecutionResult(bap.marshalizer, executionResultBytes)
Expand All @@ -757,7 +759,8 @@ func (bap *baseAPIBlockProcessor) addMbsAndNumTxsAsyncExecution(apiBlock *api.Bl
mbsBeforeExecutionAndCleanup := removeExecutedTxsFromMbs(mbsBeforeExecution, executedTxsMap)

allMbs := append(mbsBeforeExecutionAndCleanup, mbsAfterExecution...)
intraMb, err := bap.getIntrashardMiniblocksFromReceiptsStorage(blockHeader, headerHash, options)
receiptsHash := executionResultHandler.GetReceiptsHash()

Copilot AI Mar 23, 2026

Copy link

Choose a reason for hiding this comment

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

In async execution, receiptsHash := executionResultHandler.GetReceiptsHash() can be nil/empty (e.g. if the field is missing in older execution results / default-initialized). Passing that through to LoadReceipts makes decideStorageKey choose an empty key instead of falling back to the header hash for the empty-receipts case, which can prevent intrashard miniblocks from being loaded. Consider falling back to blockHeader.GetReceiptsHash() (or to bap.emptyReceiptsHash) when len(receiptsHash)==0 before calling getIntrashardMiniblocksFromReceiptsStorage.

Suggested change
receiptsHash := executionResultHandler.GetReceiptsHash()
receiptsHash := executionResultHandler.GetReceiptsHash()
if len(receiptsHash) == 0 {
// Fallback to the block header receipts hash for older/default async results
receiptsHash = blockHeader.GetReceiptsHash()
}
if len(receiptsHash) == 0 {
// If still empty, use the predefined empty receipts hash
receiptsHash = bap.emptyReceiptsHash
}

Copilot uses AI. Check for mistakes.
intraMb, err := bap.getIntrashardMiniblocksFromReceiptsStorage(receiptsHash, blockHeader, headerHash, options)
if err != nil {
return err
}
Expand Down
65 changes: 6 additions & 59 deletions node/external/blockAPI/baseBlock_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import (
"github.com/multiversx/mx-chain-go/common"
"github.com/multiversx/mx-chain-go/common/holders"
"github.com/multiversx/mx-chain-go/dataRetriever"
dblookupext2 "github.com/multiversx/mx-chain-go/dblookupext"
"github.com/multiversx/mx-chain-go/node/mock"
"github.com/multiversx/mx-chain-go/storage"
"github.com/multiversx/mx-chain-go/testscommon"
Expand Down Expand Up @@ -47,6 +46,7 @@ func createBaseBlockProcessor() *baseAPIBlockProcessor {
logsFacade: &testscommon.LogsFacadeStub{},
receiptsRepository: &testscommon.ReceiptsRepositoryStub{},
enableEpochsHandler: &enableEpochsHandlerMock.EnableEpochsHandlerStub{},
enableRoundsHandler: &testscommon.EnableRoundsHandlerStub{},
}
}

Expand All @@ -73,7 +73,7 @@ func TestBaseBlockGetIntraMiniblocksSCRS(t *testing.T) {
_ = storer.Put(scrHash, scResultBytes)

baseAPIBlockProc.receiptsRepository = &testscommon.ReceiptsRepositoryStub{
LoadReceiptsCalled: func(header data.HeaderHandler, headerHash []byte) (common.ReceiptsHolder, error) {
LoadReceiptsCalled: func(_ []byte, header data.HeaderHandler, headerHash []byte) (common.ReceiptsHolder, error) {
return holders.NewReceiptsHolder([]*block.MiniBlock{miniblock}), nil
},
}
Expand All @@ -89,7 +89,7 @@ func TestBaseBlockGetIntraMiniblocksSCRS(t *testing.T) {
}

blockHeader := &block.Header{ReceiptsHash: []byte("aaaa"), Epoch: 0}
intraMbs, err := baseAPIBlockProc.getIntrashardMiniblocksFromReceiptsStorage(blockHeader, []byte{}, api.BlockQueryOptions{WithTransactions: true})
intraMbs, err := baseAPIBlockProc.getIntrashardMiniblocksFromReceiptsStorage(blockHeader.GetReceiptsHash(), blockHeader, []byte{}, api.BlockQueryOptions{WithTransactions: true})
require.Nil(t, err)
require.Equal(t, &api.MiniBlock{
Hash: "f4add7b23eb83cf290422b0f6b770e3007b8ed3cd9683797fc90c8b4881f27bd",
Expand Down Expand Up @@ -134,7 +134,7 @@ func TestBaseBlockGetIntraMiniblocksReceipts(t *testing.T) {
_ = storer.Put(receiptHash, receiptBytes)

baseAPIBlockProc.receiptsRepository = &testscommon.ReceiptsRepositoryStub{
LoadReceiptsCalled: func(header data.HeaderHandler, headerHash []byte) (common.ReceiptsHolder, error) {
LoadReceiptsCalled: func(_ []byte, header data.HeaderHandler, headerHash []byte) (common.ReceiptsHolder, error) {
return holders.NewReceiptsHolder([]*block.MiniBlock{miniblock}), nil
},
}
Expand All @@ -154,7 +154,7 @@ func TestBaseBlockGetIntraMiniblocksReceipts(t *testing.T) {
}

blockHeader := &block.Header{ReceiptsHash: []byte("aaaa"), Epoch: 0}
intraMbs, err := baseAPIBlockProc.getIntrashardMiniblocksFromReceiptsStorage(blockHeader, []byte{}, api.BlockQueryOptions{WithTransactions: true})
intraMbs, err := baseAPIBlockProc.getIntrashardMiniblocksFromReceiptsStorage(blockHeader.GetReceiptsHash(), blockHeader, []byte{}, api.BlockQueryOptions{WithTransactions: true})
require.Nil(t, err)
require.Equal(t, &api.MiniBlock{
Hash: "596545f64319f2fcf8e0ebae06f40f3353d603f6070255588a48018c7b30c951",
Expand Down Expand Up @@ -905,64 +905,19 @@ func TestBaseAPIBlockProcessor_AddMbsAndNumTxsAsyncExecutionBasedOnExecutionResu
t.Parallel()

baseAPIBlockProc := createBaseBlockProcessor()
baseAPIBlockProc.txStatusComputer = &mock.StatusComputerStub{
ComputeStatusWhenInStorageKnowingMiniblockCalled: func(mbType block.Type, tx *transaction.ApiTransactionResult) (transaction.TxStatus, error) {
return transaction.TxStatusPending, nil
},
}

blockHeader := &block.Header{
Nonce: 100,
Round: 1000,
Epoch: 5,
MiniBlockHeaders: []block.MiniBlockHeader{
{
Hash: []byte("mb_hash_1"),
SenderShardID: 0,
ReceiverShardID: 1,
TxCount: 2,
},
},
}

// Create miniblock data
mb1 := &block.MiniBlock{
TxHashes: [][]byte{
[]byte("tx_hash_1"),
[]byte("tx_hash_2"),
},
}
mbBytes, _ := baseAPIBlockProc.marshalizer.Marshal(mb1)

tx1 := &transaction.Transaction{
Nonce: 1,
}
tx1Bytes, _ := baseAPIBlockProc.marshalizer.Marshal(tx1)

baseAPIBlockProc.store = &storageMocks.ChainStorerStub{
GetStorerCalled: func(unitType dataRetriever.UnitType) (storage.Storer, error) {
return &storageMocks.StorerStub{
GetFromEpochCalled: func(key []byte, epoch uint32) ([]byte, error) {
if string(key) == "header_hash" {
return nil, dblookupext2.ErrNotFoundInStorage
}
if string(key) == "mb_hash_1" {
return mbBytes, nil
}
return nil, errors.New("not found")
},
GetBulkFromEpochCalled: func(keys [][]byte, epoch uint32) ([]data.KeyValuePair, error) {
return []data.KeyValuePair{
{
Key: []byte("tx_hash_1"),
Value: tx1Bytes,
},
{
Key: []byte("tx_hash_2"),
Value: tx1Bytes,
},
}, nil
},
}, nil
},
}
Expand All @@ -988,15 +943,7 @@ func TestBaseAPIBlockProcessor_AddMbsAndNumTxsAsyncExecutionBasedOnExecutionResu
[]byte("header_hash"),
api.BlockQueryOptions{WithTransactions: true},
)

require.NoError(t, err)
require.NotNil(t, apiBlock.MiniBlocks)
require.Equal(t, 1, len(apiBlock.MiniBlocks))
require.Equal(t, 2, len(apiBlock.MiniBlocks[0].Transactions))
// All transactions should have pending status when no execution result is found
for _, tx := range apiBlock.MiniBlocks[0].Transactions {
require.Equal(t, transaction.TxStatusPending, tx.Status)
}
require.Equal(t, errBlockNotFound, err)
}

func TestBaseAPIBlockProcessor_AddMbsAndNumTxsAsyncExecutionBasedOnExecutionResult_UnmarshalError(t *testing.T) {
Expand Down
1 change: 1 addition & 0 deletions node/external/blockAPI/blockArgs.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ type ArgAPIBlockProcessor struct {
AccountsRepository state.AccountsRepository
ScheduledTxsExecutionHandler process.ScheduledTxsExecutionHandler
EnableEpochsHandler common.EnableEpochsHandler
EnableRoundsHandler common.EnableRoundsHandler
ProofsPool dataRetriever.ProofsPool
BlockChain data.ChainHandler
}
3 changes: 3 additions & 0 deletions node/external/blockAPI/check.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@ func checkNilArg(arg *ArgAPIBlockProcessor) error {
if check.IfNil(arg.BlockChain) {
return process.ErrNilBlockChain
}
if check.IfNil(arg.EnableRoundsHandler) {
return process.ErrNilEnableRoundsHandler
}

return core.CheckHandlerCompatibility(arg.EnableEpochsHandler, []core.EnableEpochFlag{
common.RefactorPeersMiniBlocksFlag,
Expand Down
2 changes: 1 addition & 1 deletion node/external/blockAPI/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,6 @@ type logsFacade interface {
}

type receiptsRepository interface {
LoadReceipts(header data.HeaderHandler, headerHash []byte) (common.ReceiptsHolder, error)
LoadReceipts(receiptsHash []byte, header data.HeaderHandler, headerHash []byte) (common.ReceiptsHolder, error)
IsInterfaceNil() bool
}
Loading
Loading