diff --git a/api/blocks/blocks.go b/api/blocks/blocks.go index bddb3ac12..86aa7e921 100644 --- a/api/blocks/blocks.go +++ b/api/blocks/blocks.go @@ -6,8 +6,11 @@ package blocks import ( + "encoding/hex" + "fmt" "net/http" + "github.com/ethereum/go-ethereum/rlp" "github.com/gorilla/mux" "github.com/pkg/errors" "github.com/vechain/thor/v2/api/utils" @@ -34,9 +37,17 @@ func (b *Blocks) handleGetBlock(w http.ResponseWriter, req *http.Request) error if err != nil { return utils.BadRequest(errors.WithMessage(err, "revision")) } - expanded := req.URL.Query().Get("expanded") - if expanded != "" && expanded != "false" && expanded != "true" { - return utils.BadRequest(errors.WithMessage(errors.New("should be boolean"), "expanded")) + raw, err := utils.StringToBoolean(req.URL.Query().Get("raw"), false) + if err != nil { + return utils.BadRequest(errors.WithMessage(err, "raw")) + } + expanded, err := utils.StringToBoolean(req.URL.Query().Get("expanded"), false) + if err != nil { + return utils.BadRequest(errors.WithMessage(err, "expanded")) + } + + if raw && expanded { + return utils.BadRequest(errors.WithMessage(errors.New("Raw and Expanded are mutually exclusive"), "raw&expanded")) } summary, err := utils.GetSummary(revision, b.repo, b.bft) @@ -47,6 +58,16 @@ func (b *Blocks) handleGetBlock(w http.ResponseWriter, req *http.Request) error return err } + if raw { + rlpEncoded, err := rlp.EncodeToBytes(summary.Header) + if err != nil { + return err + } + return utils.WriteJSON(w, &JSONRawBlockSummary{ + fmt.Sprintf("0x%s", hex.EncodeToString(rlpEncoded)), + }) + } + isTrunk, err := b.isTrunk(summary.Header.ID(), summary.Header.Number()) if err != nil { return err @@ -61,7 +82,7 @@ func (b *Blocks) handleGetBlock(w http.ResponseWriter, req *http.Request) error } jSummary := buildJSONBlockSummary(summary, isTrunk, isFinalized) - if expanded == "true" { + if expanded { txs, err := b.repo.GetBlockTransactions(summary.Header.ID()) if err != nil { return err diff --git a/api/blocks/blocks_test.go b/api/blocks/blocks_test.go index dcb6c4e94..8c0439e59 100644 --- a/api/blocks/blocks_test.go +++ b/api/blocks/blocks_test.go @@ -6,6 +6,7 @@ package blocks_test import ( + "encoding/hex" "encoding/json" "math" "math/big" @@ -15,6 +16,7 @@ import ( "strings" "testing" + "github.com/ethereum/go-ethereum/rlp" "github.com/gorilla/mux" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -55,6 +57,8 @@ func TestBlock(t *testing.T) { "testGetFinalizedBlock": testGetFinalizedBlock, "testGetJustifiedBlock": testGetJustifiedBlock, "testGetBlockWithRevisionNumberTooHigh": testGetBlockWithRevisionNumberTooHigh, + "testMutuallyExclusiveQueries": testMutuallyExclusiveQueries, + "testGetRawBlock": testGetRawBlock, } { t.Run(name, tt) } @@ -67,6 +71,22 @@ func testBadQueryParams(t *testing.T) { assert.Equal(t, http.StatusBadRequest, statusCode) assert.Equal(t, "expanded: should be boolean", strings.TrimSpace(string(res))) + + badQueryParams = "?raw=1" + res, statusCode, err = tclient.RawHTTPClient().RawHTTPGet("/blocks/best" + badQueryParams) + require.NoError(t, err) + + assert.Equal(t, http.StatusBadRequest, statusCode) + assert.Equal(t, "raw: should be boolean", strings.TrimSpace(string(res))) +} + +func testMutuallyExclusiveQueries(t *testing.T) { + badQueryParams := "?expanded=true&raw=true" + res, statusCode, err := tclient.RawHTTPClient().RawHTTPGet("/blocks/best" + badQueryParams) + require.NoError(t, err) + + assert.Equal(t, http.StatusBadRequest, statusCode) + assert.Equal(t, "raw&expanded: Raw and Expanded are mutually exclusive", strings.TrimSpace(string(res))) } func testGetBestBlock(t *testing.T) { @@ -80,6 +100,41 @@ func testGetBestBlock(t *testing.T) { assert.Equal(t, http.StatusOK, statusCode) } +func testGetRawBlock(t *testing.T) { + res, statusCode, err := tclient.RawHTTPClient().RawHTTPGet("/blocks/best?raw=true") + require.NoError(t, err) + rawBlock := new(blocks.JSONRawBlockSummary) + if err := json.Unmarshal(res, &rawBlock); err != nil { + t.Fatal(err) + } + + blockBytes, err := hex.DecodeString(rawBlock.Raw[2:len(rawBlock.Raw)]) + if err != nil { + t.Fatal(err) + } + + header := block.Header{} + err = rlp.DecodeBytes(blockBytes, &header) + if err != nil { + t.Fatal(err) + } + + expHeader := blk.Header() + assert.Equal(t, expHeader.Number(), header.Number(), "Number should be equal") + assert.Equal(t, expHeader.ID(), header.ID(), "Hash should be equal") + assert.Equal(t, expHeader.ParentID(), header.ParentID(), "ParentID should be equal") + assert.Equal(t, expHeader.Timestamp(), header.Timestamp(), "Timestamp should be equal") + assert.Equal(t, expHeader.TotalScore(), header.TotalScore(), "TotalScore should be equal") + assert.Equal(t, expHeader.GasLimit(), header.GasLimit(), "GasLimit should be equal") + assert.Equal(t, expHeader.GasUsed(), header.GasUsed(), "GasUsed should be equal") + assert.Equal(t, expHeader.Beneficiary(), header.Beneficiary(), "Beneficiary should be equal") + assert.Equal(t, expHeader.TxsRoot(), header.TxsRoot(), "TxsRoot should be equal") + assert.Equal(t, expHeader.StateRoot(), header.StateRoot(), "StateRoot should be equal") + assert.Equal(t, expHeader.ReceiptsRoot(), header.ReceiptsRoot(), "ReceiptsRoot should be equal") + + assert.Equal(t, http.StatusOK, statusCode) +} + func testGetBlockByHeight(t *testing.T) { res, statusCode, err := tclient.RawHTTPClient().RawHTTPGet("/blocks/1") require.NoError(t, err) diff --git a/api/blocks/types.go b/api/blocks/types.go index 38261b2e5..989b63041 100644 --- a/api/blocks/types.go +++ b/api/blocks/types.go @@ -33,6 +33,10 @@ type JSONBlockSummary struct { IsFinalized bool `json:"isFinalized"` } +type JSONRawBlockSummary struct { + Raw string `json:"raw"` +} + type JSONCollapsedBlock struct { *JSONBlockSummary Transactions []thor.Bytes32 `json:"transactions"` diff --git a/api/doc/thor.yaml b/api/doc/thor.yaml index 732a5f1a3..dcf0ae6b9 100644 --- a/api/doc/thor.yaml +++ b/api/doc/thor.yaml @@ -292,6 +292,7 @@ paths: parameters: - $ref: '#/components/parameters/RevisionInPath' - $ref: '#/components/parameters/ExpandedInQuery' + - $ref: '#/components/parameters/RawBlockInQuery' tags: - Blocks summary: Retrieve a block @@ -2353,6 +2354,18 @@ components: type: boolean example: false + RawBlockInQuery: + name: raw + in: query + required: false + description: | + Whether the block should be returned in RLP encoding or not. + - `true` returns `block` as an RLP encoded object + - `false` returns `block` as a structured JSON object + schema: + type: boolean + example: false + PendingInQuery: name: pending in: query diff --git a/api/utils/http.go b/api/utils/http.go index 652c3e408..2235797de 100644 --- a/api/utils/http.go +++ b/api/utils/http.go @@ -9,6 +9,8 @@ import ( "encoding/json" "io" "net/http" + + "github.com/pkg/errors" ) type httpError struct { @@ -36,6 +38,17 @@ func BadRequest(cause error) error { } } +func StringToBoolean(boolStr string, defaultVal bool) (bool, error) { + if boolStr == "" { + return defaultVal, nil + } else if boolStr == "false" { + return false, nil + } else if boolStr == "true" { + return true, nil + } + return false, errors.New("should be boolean") +} + // Forbidden convenience method to create http forbidden error. func Forbidden(cause error) error { return &httpError{