Skip to content

Commit

Permalink
Add /blocks , /blocks/{hashOrHeight} APIs (#732)
Browse files Browse the repository at this point in the history
* Add /blocks , /blocks/{hashOrHeight} api handlers
- Add storage.WalkBlocks func for handlers
- Add resource.Block

* Block cursor as height

* Add  SkipCursor for supporting cursor as height

* Add blocks handler tests

* Add api doc for blocks api

* Default cursor is common.GenesisBlockHeight

* Fix bug about SkipCursor

* Fix test for walk

* Typo
  • Loading branch information
anarcher authored Nov 19, 2018
1 parent 67f02e5 commit 8465a03
Show file tree
Hide file tree
Showing 18 changed files with 481 additions and 17 deletions.
4 changes: 2 additions & 2 deletions docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ SEBAK, the next BOScoin network with ISAAC consensus protocol.
<!-- partial(API_v1/transactions.md) -->

<!-- include(API_v1/paging.md) -->
<!-- include(API_v1/models.md) -->
<!-- include(API_v1/accounts.md) -->
<!-- include(API_v1/transactions.md) -->
<!-- include(API_v1/models.md) -->
<!-- include(API_v1/operations.md) -->

<!-- include(API_v1/blocks.md) -->
39 changes: 39 additions & 0 deletions docs/API_v1/blocks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Group Blocks
Blocks API

## Blocks [/api/v1/blocks?cursor={cursor}&limit={limit}&reverse={reverse}]

+ Parameters
+ cursor: `1207` (string, optional) - a block height as cursor
+ reverse: `false` (string, optional)
+ limit: `100` (integer, optional)

### Retrieve blocks [GET]

<p>Retrieve all valid blocks </p>

<p> Streaming mode supported with header "Accept": "text/event-stream" </p>

+ Response 200 (application/hal+json; charset=utf-8)
+ Attributes (Blocks)

+ Response 500 (application/problem+json; charset=utf-8)
+ Attributes (Problem)


## Block Details [/api/v1/blocks/{hashOrHeight}]

+ Parameters
+ hashOrHeight: `CLNes5kkg7ozgnHBhpBXHMHFtPKo7z4RF8NZpNGRUB4i` `1207` (string,required) - a block hash or height

### Retrieve a block [GET]

<p> Retrieve a block by the hash or height <p>

+ Response 200 (application/hal+json; charset=utf-8)
+ Attributes (Block)
+ Response 404 (application/problem+json; charset=utf-8)
+ Attributes (Problem NotFound)
+ Response 500 (application/problem+json; charset=utf-8)
+ Attributes (Problem)

44 changes: 44 additions & 0 deletions docs/API_v1/models.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,50 @@
+ self
+ href: `/api/v1/transactions/7nLuyg8radTExzBM2WhG37AwohBwEySBw4vj2xdtdjAs/operations`

### Blocks
+ _embedded
+ records (array)
+ (object):
+ _links
+ self
+ href: `/api/v1/blocks/AcFpZMr6EhxBuCw3xADUzepa395wmh3c5fo2cyxYCi1q`
+ confirmed: 2018-11-18T18:44:47.900933000+09:00
+ hash: `AcFpZMr6EhxBuCw3xADUzepa395wmh3c5fo2cyxYCi1q`
+ height: 1
+ prev_block_hash: `J8TQCCtsiLcRZpYtVN3ozCFByd24fjXe2BgodLkeXN7S`,
+ proposed_time: `2018-04-17T5:07:31.000000000Z`
+ proposer: `GDIRF4UWPACXPPI4GW7CMTACTCNDIKJEHZK44RITZB4TD3YUM6CCVNGJ`
+ proposer_transaction: `EQNSFnhzzz3bDpaZQekWPPNtr3kmRs5fUafBYAkHGXRP`
+ round: 0,
+ transactions (array)
+ `BivUS2tYjm1ZYXZNvKqRDa1eyBRTcE3DeuEDJVtuwNcm`
+ transactions_root: `BR2gsNw5WGjZ6HFPNr8fFAQPu42dqk1P7VVV7p5Efnru`
+ version: 0
+ _links
+ next
+ href: `/api/v1/blocks?cursor=1&limit=100&reverse=false`
+ prev
+ href: `/api/v1/blocks?cursor=1&limit=100&reverse=true`
+ self
+ href: `/api/v1/blocks`

### Block
+ _links
+ self
+ href: `/api/v1/blocks/AcFpZMr6EhxBuCw3xADUzepa395wmh3c5fo2cyxYCi1q`
+ confirmed: 2018-11-18T18:44:47.900933000+09:00
+ hash: `AcFpZMr6EhxBuCw3xADUzepa395wmh3c5fo2cyxYCi1q`
+ height: 3
+ prev_block_hash: `J8TQCCtsiLcRZpYtVN3ozCFByd24fjXe2BgodLkeXN7S`,
+ proposed_time: `2018-04-17T5:07:31.000000000Z`
+ proposer: `GDIRF4UWPACXPPI4GW7CMTACTCNDIKJEHZK44RITZB4TD3YUM6CCVNGJ`
+ proposer_transaction: `EQNSFnhzzz3bDpaZQekWPPNtr3kmRs5fUafBYAkHGXRP`
+ round: 0
+ transactions (array)
+ `BivUS2tYjm1ZYXZNvKqRDa1eyBRTcE3DeuEDJVtuwNcm`
+ transactions_root: `BR2gsNw5WGjZ6HFPNr8fFAQPu42dqk1P7VVV7p5Efnru`
+ version: 0

### Problem
+ status: 500 (number)
+ title: `problem error message`
Expand Down
21 changes: 21 additions & 0 deletions lib/block/block.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,11 @@ func getBlockKeyPrefixHeight(height uint64) string {
return fmt.Sprintf("%s%020d", common.BlockPrefixHeight, height)
}

//GetBlockKeyPrefixHeight returns index key by height. It is used to make cursor as height.
func GetBlockKeyPrefixHeight(height uint64) string {
return getBlockKeyPrefixHeight(height)
}

func (b Block) NewBlockKeyConfirmed() string {
return fmt.Sprintf(
"%s%s-%s%s",
Expand Down Expand Up @@ -246,3 +251,19 @@ func GetLatestBlock(st *storage.LevelDBBackend) Block {

return b
}

func WalkBlocks(st *storage.LevelDBBackend, option *storage.WalkOption, walkFunc func(*Block, []byte) (bool, error)) error {
err := st.Walk(common.BlockPrefixHeight, option, func(key, value []byte) (bool, error) {
var hash string
if err := json.Unmarshal(value, &hash); err != nil {
return false, err
}

b, err := GetBlock(st, hash)
if err != nil {
return false, err
}
return walkFunc(&b, key)
})
return err
}
1 change: 1 addition & 0 deletions lib/errors/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,4 +105,5 @@ var (
PageQueryLimitMaxExceed = NewError(187, "maximum value of limit query parameter exceed")
TransactionPoolFull = NewError(188, "transaction pool is full")
TransactionAlreadyExistsInPool = NewError(189, "transaction already exists in pool")
BadRequestParameter = NewError(190, "request parameter is invalid")
)
1 change: 1 addition & 0 deletions lib/network/httputils/httputils.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ var (
errors.BlockTransactionDoesNotExists.Code: http.StatusNotFound,
errors.BlockAccountDoesNotExists.Code: http.StatusNotFound,
errors.TransactionPoolFull.Code: http.StatusLocked,
errors.BadRequestParameter.Code: http.StatusBadRequest,
}
)

Expand Down
2 changes: 2 additions & 0 deletions lib/node/runner/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ const (
GetTransactionOperationsHandlerPattern = "/transactions/{id}/operations"
PostTransactionPattern = "/transactions"
GetTransactionHistoryHandlerPattern = "/transactions/{id}/history"
GetBlocksHandlerPattern = "/blocks"
GetBlockHandlerPattern = "/blocks/{hashOrHeight}"
GetNodeInfoPattern = "/"
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ func prepareAPIServer() (*httptest.Server, *storage.LevelDBBackend) {
router.HandleFunc(GetAccountHandlerPattern, apiHandler.GetAccountHandler).Methods("GET")
router.HandleFunc(GetAccountHandlerPattern, apiHandler.GetAccountHandler).Methods("GET")
router.HandleFunc(GetTransactionOperationsHandlerPattern, apiHandler.GetOperationsByTxHashHandler).Methods("GET")
router.HandleFunc(GetBlocksHandlerPattern, apiHandler.GetBlocksHandler).Methods("GET")
router.HandleFunc(GetBlockHandlerPattern, apiHandler.GetBlockHandler).Methods("GET")
ts := httptest.NewServer(router)
return ts, storage
}
Expand Down
47 changes: 47 additions & 0 deletions lib/node/runner/api/block.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package api

import (
"net/http"
"strconv"

"boscoin.io/sebak/lib/block"
"boscoin.io/sebak/lib/errors"
"boscoin.io/sebak/lib/network/httputils"
"boscoin.io/sebak/lib/node/runner/api/resource"

"github.com/gorilla/mux"
)

func (api NetworkHandlerAPI) GetBlockHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
hash := vars["hashOrHeight"]
if hash == "" {
err := errors.BadRequestParameter
httputils.WriteJSONError(w, err)
return
}

var isHash bool
height, err := strconv.ParseUint(hash, 10, 64)
if err != nil {
isHash = true
}

var res resource.Resource
{
var b block.Block
var err error
if isHash {
b, err = block.GetBlock(api.storage, hash)
} else {
b, err = block.GetBlockByHeight(api.storage, height)
}

if err != nil {
httputils.WriteJSONError(w, err)
return
}
res = resource.NewBlock(&b)
}
httputils.MustWriteJSON(w, 200, res)
}
49 changes: 49 additions & 0 deletions lib/node/runner/api/block_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package api

import (
"bufio"
"encoding/json"
"io/ioutil"
"strings"
"testing"

"boscoin.io/sebak/lib/block"
"github.com/stretchr/testify/require"
)

func TestBlockHandler(t *testing.T) {
ts, st := prepareAPIServer()
defer st.Close()
defer ts.Close()

genesis := block.GetLatestBlock(st)

reqFunc := func(url string) map[string]interface{} {

respBody := request(ts, url, false)
defer respBody.Close()
bs, err := ioutil.ReadAll(bufio.NewReader(respBody))
require.NoError(t, err)

result := make(map[string]interface{})
err = json.Unmarshal(bs, &result)
require.NoError(t, err)

return result
}

{
url := strings.Replace(GetBlockHandlerPattern, "{hashOrHeight}", "1", 1)
res := reqFunc(url)
require.Equal(t, res["hash"], genesis.Hash)
require.Equal(t, res["transactions_root"], genesis.TransactionsRoot)
}

{
url := strings.Replace(GetBlockHandlerPattern, "{hashOrHeight}", genesis.Hash, 1)
res := reqFunc(url)
require.Equal(t, res["hash"], genesis.Hash)
require.Equal(t, res["transactions_root"], genesis.TransactionsRoot)
}

}
71 changes: 71 additions & 0 deletions lib/node/runner/api/blocks.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package api

import (
"net/http"
"strconv"

"boscoin.io/sebak/lib/block"
"boscoin.io/sebak/lib/common"
"boscoin.io/sebak/lib/common/observer"
"boscoin.io/sebak/lib/network/httputils"
"boscoin.io/sebak/lib/node/runner/api/resource"
"boscoin.io/sebak/lib/storage"
)

func (api NetworkHandlerAPI) GetBlocksHandler(w http.ResponseWriter, r *http.Request) {
p, err := NewPageQuery(r)
if err != nil {
httputils.WriteJSONError(w, err)
return
}

var (
cursor []byte // cursor as height
blocks []resource.Resource
)

var option *storage.WalkOption
{
height, err := strconv.ParseUint(string(p.Cursor()), 10, 64)
if err != nil {
height = common.GenesisBlockHeight // default cursor is genesis block height
}
option = storage.NewWalkOption(block.GetBlockKeyPrefixHeight(height), p.Limit(), p.Reverse(), false)
if httputils.IsEventStream(r) {
option.Limit = 10
}
}

{
err := block.WalkBlocks(api.storage, option, func(b *block.Block, key []byte) (next bool, err error) {
blocks = append(blocks, resource.NewBlock(b))
height := b.Height
if height > 1 {
if option.Reverse {
height--
} else {
height++
}
}
cursor = []byte(strconv.FormatUint(height, 10))
return true, nil
})
if err != nil {
httputils.WriteJSONError(w, err)
return

}
}

if httputils.IsEventStream(r) {
es := NewEventStream(w, r, renderEventStream, DefaultContentType)
for _, b := range blocks {
es.Render(b)
}
es.Run(observer.BlockObserver, block.EventBlockPrefix)
return
}

list := p.ResourceList(blocks, cursor)
httputils.MustWriteJSON(w, 200, list)
}
Loading

0 comments on commit 8465a03

Please sign in to comment.