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

Block coordinates on vm query #5447

Merged
25 changes: 25 additions & 0 deletions api/groups/vmValuesGroup.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"sync"

"github.com/gin-gonic/gin"
"github.com/multiversx/mx-chain-core-go/core"
"github.com/multiversx/mx-chain-core-go/core/check"
"github.com/multiversx/mx-chain-core-go/data/vm"
"github.com/multiversx/mx-chain-go/api/errors"
Expand Down Expand Up @@ -139,6 +140,11 @@ func (vvg *vmValuesGroup) doExecuteQuery(context *gin.Context) (*vm.VMOutputApi,
return nil, "", err
}

command.BlockNonce, command.BlockHash, command.BlockRootHash, err = extractBlockCoordinates(context)
if err != nil {
return nil, "", err
}

vmOutputApi, err := vvg.getFacade().ExecuteSCQuery(command)
if err != nil {
return nil, "", err
Expand All @@ -152,6 +158,25 @@ func (vvg *vmValuesGroup) doExecuteQuery(context *gin.Context) (*vm.VMOutputApi,
return vmOutputApi, vmExecErrMsg, nil
}

func extractBlockCoordinates(context *gin.Context) (core.OptionalUint64, []byte, []byte, error) {
blockNonce, err := parseUint64UrlParam(context, urlParamBlockNonce)
if err != nil {
return core.OptionalUint64{}, nil, nil, err
}

blockHash, err := parseHexBytesUrlParam(context, urlParamBlockHash)
if err != nil {
return core.OptionalUint64{}, nil, nil, err
bogdan-rosianu marked this conversation as resolved.
Show resolved Hide resolved
}

blockRootHash, err := parseHexBytesUrlParam(context, urlParamBlockRootHash)
if err != nil {
return core.OptionalUint64{}, nil, nil, err
}

return blockNonce, blockHash, blockRootHash, nil
}
bogdan-rosianu marked this conversation as resolved.
Show resolved Hide resolved

func (vvg *vmValuesGroup) createSCQuery(request *VMValueRequest) (*process.SCQuery, error) {
decodedAddress, err := vvg.getFacade().DecodeAddressPubkey(request.ScAddress)
if err != nil {
Expand Down
116 changes: 105 additions & 11 deletions api/groups/vmValuesGroup_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@ import (
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"math/big"
"net/http"
"net/http/httptest"
"testing"

"github.com/gin-gonic/gin"
"github.com/multiversx/mx-chain-core-go/core"
"github.com/multiversx/mx-chain-core-go/data/vm"
apiErrors "github.com/multiversx/mx-chain-go/api/errors"
"github.com/multiversx/mx-chain-go/api/groups"
Expand Down Expand Up @@ -137,32 +139,126 @@ func TestGetInt_ShouldWork(t *testing.T) {
require.Equal(t, value, response.Data)
}

func TestQuery_ShouldWork(t *testing.T) {
func TestQuery(t *testing.T) {
t.Parallel()

facade := mock.FacadeStub{
ExecuteSCQueryHandler: func(query *process.SCQuery) (vmOutput *vm.VMOutputApi, e error) {
t.Run("invalid block nonce should error", testQueryShouldError("/vm-values/query?blockNonce=invalid_nonce"))
t.Run("invalid block hash should error", testQueryShouldError("/vm-values/query?blockHash=invalid_nonce"))
t.Run("invalid block root hash should error", testQueryShouldError("/vm-values/query?blockRootHash=invalid_nonce"))
t.Run("should work - block nonce", func(t *testing.T) {
t.Parallel()

return &vm.VMOutputApi{
ReturnData: [][]byte{big.NewInt(42).Bytes()},
}, nil
},
}
providedBlockNonce := core.OptionalUint64{
Value: 123,
HasValue: true,
}
facade := mock.FacadeStub{
ExecuteSCQueryHandler: func(query *process.SCQuery) (vmOutput *vm.VMOutputApi, e error) {
require.Equal(t, providedBlockNonce, query.BlockNonce)
return &vm.VMOutputApi{
ReturnData: [][]byte{big.NewInt(42).Bytes()},
}, nil
},
}
url := fmt.Sprintf("/vm-values/query?blockNonce=%d", providedBlockNonce.Value)
testQueryShouldWork(t, url, &facade)
})
t.Run("should work - block hash", func(t *testing.T) {
t.Parallel()

providedBlockHash := []byte("provided hash")
facade := mock.FacadeStub{
ExecuteSCQueryHandler: func(query *process.SCQuery) (vmOutput *vm.VMOutputApi, e error) {
require.Equal(t, providedBlockHash, query.BlockHash)
return &vm.VMOutputApi{
ReturnData: [][]byte{big.NewInt(42).Bytes()},
}, nil
},
}
url := fmt.Sprintf("/vm-values/query?blockHash=%s", hex.EncodeToString(providedBlockHash))
testQueryShouldWork(t, url, &facade)
})
t.Run("should work - block root hash", func(t *testing.T) {
t.Parallel()

providedBlockRootHash := []byte("provided root hash")
facade := mock.FacadeStub{
ExecuteSCQueryHandler: func(query *process.SCQuery) (vmOutput *vm.VMOutputApi, e error) {
require.Equal(t, providedBlockRootHash, query.BlockRootHash)
return &vm.VMOutputApi{
ReturnData: [][]byte{big.NewInt(42).Bytes()},
}, nil
},
}
url := fmt.Sprintf("/vm-values/query?blockRootHash=%s", hex.EncodeToString(providedBlockRootHash))
testQueryShouldWork(t, url, &facade)
})
t.Run("should work - no block coordinates", func(t *testing.T) {
t.Parallel()

facade := mock.FacadeStub{
ExecuteSCQueryHandler: func(query *process.SCQuery) (vmOutput *vm.VMOutputApi, e error) {

return &vm.VMOutputApi{
ReturnData: [][]byte{big.NewInt(42).Bytes()},
}, nil
},
}
testQueryShouldWork(t, "/vm-values/query", &facade)
})
}

func testQueryShouldWork(t *testing.T, url string, facade shared.FacadeHandler) {
request := groups.VMValueRequest{
ScAddress: dummyScAddress,
FuncName: "function",
Args: []string{},
}

response := vmOutputResponse{}
statusCode := doPost(t, &facade, "/vm-values/query", request, &response)
statusCode := doPost(t, facade, url, request, &response)

require.Equal(t, http.StatusOK, statusCode)
require.Equal(t, "", response.Error)
require.Equal(t, int64(42), big.NewInt(0).SetBytes(response.Data.ReturnData[0]).Int64())
}

func testQueryShouldError(url string) func(t *testing.T) {
return func(t *testing.T) {
t.Parallel()

request := &groups.VMValueRequest{
ScAddress: dummyScAddress,
FuncName: "function",
Args: []string{},
}
requestAsBytes, _ := json.Marshal(request)

facade := mock.FacadeStub{
ExecuteSCQueryHandler: func(query *process.SCQuery) (vmOutput *vm.VMOutputApi, e error) {
return &vm.VMOutputApi{
ReturnData: [][]byte{big.NewInt(42).Bytes()},
}, nil
},
}

group, err := groups.NewVmValuesGroup(&facade)
require.NoError(t, err)

server := startWebServer(group, "vm-values", getVmValuesRoutesConfig())

httpRequest, _ := http.NewRequest("POST", url, bytes.NewBuffer(requestAsBytes))

responseRecorder := httptest.NewRecorder()
server.ServeHTTP(responseRecorder, httpRequest)

responseI := shared.GenericAPIResponse{}
loadResponse(responseRecorder.Body, &responseI)
require.Equal(t, shared.ReturnCodeRequestError, responseI.Code)
require.NotEmpty(t, responseI.Error)
}
}

func TestCreateSCQuery_ArgumentIsNotHexShouldErr(t *testing.T) {
request := groups.VMValueRequest{
ScAddress: dummyScAddress,
Expand Down Expand Up @@ -280,7 +376,6 @@ func TestAllRoutes_WhenBadJsonShouldErr(t *testing.T) {
func TestAllRoutes_DecodeAddressPubkeyFailsShouldErr(t *testing.T) {
t.Parallel()

expectedErr := errors.New("expected error")
cnt := 0
facade := mock.FacadeStub{
DecodeAddressPubkeyCalled: func(pk string) ([]byte, error) {
Expand Down Expand Up @@ -384,7 +479,6 @@ func TestVMValuesGroup_UpdateFacade(t *testing.T) {
require.Contains(t, "", response.Error)
require.Equal(t, hex.EncodeToString(valueBuff), response.Data)

expectedErr := errors.New("expected error")
newFacade := &mock.FacadeStub{
ExecuteSCQueryHandler: func(query *process.SCQuery) (vmOutput *vm.VMOutputApi, e error) {

Expand Down
95 changes: 95 additions & 0 deletions dataRetriever/blockchain/apiBlockchain.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package blockchain

import (
"sync"

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

type apiBlockchain struct {
mainBlockchain data.ChainHandler
currentRootHash []byte
mut sync.RWMutex
}

// NewApiBlockchain returns a new instance of apiBlockchain
func NewApiBlockchain(mainBlockchain data.ChainHandler) (*apiBlockchain, error) {
if check.IfNil(mainBlockchain) {
return nil, ErrNilBlockChain
}

return &apiBlockchain{
mainBlockchain: mainBlockchain,
}, nil
}

// GetGenesisHeader returns the genesis header from the main chain handler
func (abc *apiBlockchain) GetGenesisHeader() data.HeaderHandler {
return abc.mainBlockchain.GetGenesisHeader()
}

// SetGenesisHeader returns an error
func (abc *apiBlockchain) SetGenesisHeader(_ data.HeaderHandler) error {
return ErrOperationNotPermitted
}

// GetGenesisHeaderHash returns the genesis header hash from the main chain handler
func (abc *apiBlockchain) GetGenesisHeaderHash() []byte {
return abc.mainBlockchain.GetGenesisHeaderHash()
}

// SetGenesisHeaderHash does nothing
func (abc *apiBlockchain) SetGenesisHeaderHash(_ []byte) {
}
bogdan-rosianu marked this conversation as resolved.
Show resolved Hide resolved

// GetCurrentBlockHeader returns the current block header from the main chain handler
func (abc *apiBlockchain) GetCurrentBlockHeader() data.HeaderHandler {
return abc.mainBlockchain.GetCurrentBlockHeader()
}

// SetCurrentBlockHeaderAndRootHash saves the root hash locally, which will be returned until a further call with an empty rootHash is made
func (abc *apiBlockchain) SetCurrentBlockHeaderAndRootHash(_ data.HeaderHandler, rootHash []byte) error {
abc.mut.Lock()
abc.currentRootHash = rootHash
abc.mut.Unlock()

return nil
}

// GetCurrentBlockHeaderHash returns the current block header hash from the main chain handler
func (abc *apiBlockchain) GetCurrentBlockHeaderHash() []byte {
return abc.mainBlockchain.GetCurrentBlockHeaderHash()
}

// SetCurrentBlockHeaderHash does nothing
func (abc *apiBlockchain) SetCurrentBlockHeaderHash(_ []byte) {
}

// GetCurrentBlockRootHash returns the current block root hash from the main chain handler, if no local root hash is set
// if there is a local root hash, it will be returned until its reset
func (abc *apiBlockchain) GetCurrentBlockRootHash() []byte {
Copy link
Contributor

Choose a reason for hiding this comment

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

in a normal VM Query, is SetCurrentBlockHeaderAndRootHash called each time? Otherwise, a gateway user might send a request for a block from yesterday and the next request (from another user, without coordinates) will use the root hash from yesterday instead of the current one

Copy link
Contributor Author

Choose a reason for hiding this comment

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

tried to cover all the cases when executeScCall would early exit, in order to properly reset the root hash and avoid this situation

Copy link
Contributor

Choose a reason for hiding this comment

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

Why would it need to reset at exit?

I would instead, from the API call prepare the apiBlockchain with either the main blockchain current data, or with the preferred blockchain data given by the developer, and then do the sc query/etc.

In that case there should be no edge cases that need to be taken care of, and will be more straight forward as you will always set the block coordinates you are working with on each of the calls.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

implemented the suggestion

abc.mut.RLock()
if len(abc.currentRootHash) > 0 {
defer abc.mut.RUnlock()
return abc.currentRootHash
}

abc.mut.RUnlock()

return abc.mainBlockchain.GetCurrentBlockRootHash()
}

// SetFinalBlockInfo does nothing
func (abc *apiBlockchain) SetFinalBlockInfo(_ uint64, _ []byte, _ []byte) {
}

// GetFinalBlockInfo returns the final block header hash from the main chain handler
func (abc *apiBlockchain) GetFinalBlockInfo() (nonce uint64, blockHash []byte, rootHash []byte) {
return abc.mainBlockchain.GetFinalBlockInfo()
}

// IsInterfaceNil returns true if there is no value under the interface
func (abc *apiBlockchain) IsInterfaceNil() bool {
return abc == nil
}