Skip to content

Commit

Permalink
rpc: add chunked rpc interface (backport #6445) (#6717)
Browse files Browse the repository at this point in the history
* rpc: add chunked rpc interface (#6445)

(cherry picked from commit d913406)

# Conflicts:
#	light/proxy/routes.go
#	node/node.go
#	rpc/core/net.go
#	rpc/core/routes.go

* fix conflicts

Co-authored-by: Sam Kleinman <garen@tychoish.com>
Co-authored-by: marbar3778 <marbar3778@yahoo.com>
  • Loading branch information
3 people committed Jul 14, 2021
1 parent 2c2f511 commit da9eefd
Show file tree
Hide file tree
Showing 12 changed files with 157 additions and 1 deletion.
9 changes: 9 additions & 0 deletions light/proxy/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ func RPCRoutes(c *lrpc.Client) map[string]*rpcserver.RPCFunc {
"net_info": rpcserver.NewRPCFunc(makeNetInfoFunc(c), ""),
"blockchain": rpcserver.NewRPCFunc(makeBlockchainInfoFunc(c), "minHeight,maxHeight"),
"genesis": rpcserver.NewRPCFunc(makeGenesisFunc(c), ""),
"genesis_chunked": rpcserver.NewRPCFunc(makeGenesisChunkedFunc(c), ""),
"block": rpcserver.NewRPCFunc(makeBlockFunc(c), "height"),
"block_by_hash": rpcserver.NewRPCFunc(makeBlockByHashFunc(c), "hash"),
"block_results": rpcserver.NewRPCFunc(makeBlockResultsFunc(c), "height"),
Expand Down Expand Up @@ -92,6 +93,14 @@ func makeGenesisFunc(c *lrpc.Client) rpcGenesisFunc {
}
}

type rpcGenesisChunkedFunc func(ctx *rpctypes.Context, chunk uint) (*ctypes.ResultGenesisChunk, error)

func makeGenesisChunkedFunc(c *lrpc.Client) rpcGenesisChunkedFunc {
return func(ctx *rpctypes.Context, chunk uint) (*ctypes.ResultGenesisChunk, error) {
return c.GenesisChunked(ctx.Context(), chunk)
}
}

type rpcBlockFunc func(ctx *rpctypes.Context, height *int64) (*ctypes.ResultBlock, error)

func makeBlockFunc(c *lrpc.Client) rpcBlockFunc {
Expand Down
4 changes: 4 additions & 0 deletions light/rpc/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,10 @@ func (c *Client) Genesis(ctx context.Context) (*ctypes.ResultGenesis, error) {
return c.next.Genesis(ctx)
}

func (c *Client) GenesisChunked(ctx context.Context, id uint) (*ctypes.ResultGenesisChunk, error) {
return c.next.GenesisChunked(ctx, id)
}

// Block calls rpcclient#Block and then verifies the result.
func (c *Client) Block(ctx context.Context, height *int64) (*ctypes.ResultBlock, error) {
res, err := c.next.Block(ctx, height)
Expand Down
4 changes: 4 additions & 0 deletions node/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -1028,6 +1028,10 @@ func (n *Node) ConfigureRPC() error {

Config: *n.config.RPC,
})
if err := rpccore.InitGenesisChunks(); err != nil {
return err
}

return nil
}

Expand Down
9 changes: 9 additions & 0 deletions rpc/client/http/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,15 @@ func (c *baseRPCClient) Genesis(ctx context.Context) (*ctypes.ResultGenesis, err
return result, nil
}

func (c *baseRPCClient) GenesisChunked(ctx context.Context, id uint) (*ctypes.ResultGenesisChunk, error) {
result := new(ctypes.ResultGenesisChunk)
_, err := c.caller.Call(ctx, "genesis_chunked", map[string]interface{}{"chunk": id}, result)
if err != nil {
return nil, err
}
return result, nil
}

func (c *baseRPCClient) Block(ctx context.Context, height *int64) (*ctypes.ResultBlock, error) {
result := new(ctypes.ResultBlock)
params := make(map[string]interface{})
Expand Down
1 change: 1 addition & 0 deletions rpc/client/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ type SignClient interface {
// HistoryClient provides access to data from genesis to now in large chunks.
type HistoryClient interface {
Genesis(context.Context) (*ctypes.ResultGenesis, error)
GenesisChunked(context.Context, uint) (*ctypes.ResultGenesisChunk, error)
BlockchainInfo(ctx context.Context, minHeight, maxHeight int64) (*ctypes.ResultBlockchainInfo, error)
}

Expand Down
4 changes: 4 additions & 0 deletions rpc/client/local/local.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,10 @@ func (c *Local) Genesis(ctx context.Context) (*ctypes.ResultGenesis, error) {
return core.Genesis(c.ctx)
}

func (c *Local) GenesisChunked(ctx context.Context, id uint) (*ctypes.ResultGenesisChunk, error) {
return core.GenesisChunked(c.ctx, id)
}

func (c *Local) Block(ctx context.Context, height *int64) (*ctypes.ResultBlock, error) {
return core.Block(c.ctx, height)
}
Expand Down
25 changes: 24 additions & 1 deletion rpc/client/mocks/client.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

27 changes: 27 additions & 0 deletions rpc/client/rpc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package client_test

import (
"context"
"encoding/base64"
"fmt"
"math"
"net/http"
Expand All @@ -14,6 +15,7 @@ import (
"github.com/stretchr/testify/require"

abci "github.com/tendermint/tendermint/abci/types"
tmjson "github.com/tendermint/tendermint/libs/json"
"github.com/tendermint/tendermint/libs/log"
tmmath "github.com/tendermint/tendermint/libs/math"
mempl "github.com/tendermint/tendermint/mempool"
Expand Down Expand Up @@ -186,6 +188,31 @@ func TestGenesisAndValidators(t *testing.T) {
}
}

func TestGenesisChunked(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

for _, c := range GetClients() {
first, err := c.GenesisChunked(ctx, 0)
require.NoError(t, err)

decoded := make([]string, 0, first.TotalChunks)
for i := 0; i < first.TotalChunks; i++ {
chunk, err := c.GenesisChunked(ctx, uint(i))
require.NoError(t, err)
data, err := base64.StdEncoding.DecodeString(chunk.Data)
require.NoError(t, err)
decoded = append(decoded, string(data))

}
doc := []byte(strings.Join(decoded, ""))

var out types.GenesisDoc
require.NoError(t, tmjson.Unmarshal(doc, &out),
"first: %+v, doc: %s", first, string(doc))
}
}

func TestABCIQuery(t *testing.T) {
for i, c := range GetClients() {
// write something
Expand Down
38 changes: 38 additions & 0 deletions rpc/core/env.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
package core

import (
"encoding/base64"
"fmt"
"time"

cfg "github.com/tendermint/tendermint/config"
"github.com/tendermint/tendermint/consensus"
"github.com/tendermint/tendermint/crypto"
tmjson "github.com/tendermint/tendermint/libs/json"
"github.com/tendermint/tendermint/libs/log"
mempl "github.com/tendermint/tendermint/mempool"
"github.com/tendermint/tendermint/p2p"
Expand All @@ -25,6 +27,10 @@ const (
// SubscribeTimeout is the maximum time we wait to subscribe for an event.
// must be less than the server's write timeout (see rpcserver.DefaultConfig)
SubscribeTimeout = 5 * time.Second

// genesisChunkSize is the maximum size, in bytes, of each
// chunk in the genesis structure for the chunked API
genesisChunkSize = 16 * 1024 * 1024 // 16
)

var (
Expand Down Expand Up @@ -91,6 +97,9 @@ type Environment struct {
Logger log.Logger

Config cfg.RPCConfig

// cache of chunked genesis data.
genChunks []string
}

//----------------------------------------------
Expand Down Expand Up @@ -130,6 +139,35 @@ func validatePerPage(perPagePtr *int) int {
return perPage
}

// InitGenesisChunks configures the environment and should be called on service
// startup.
func InitGenesisChunks() error {
if env.genChunks != nil {
return nil
}

if env.GenDoc == nil {
return nil
}

data, err := tmjson.Marshal(env.GenDoc)
if err != nil {
return err
}

for i := 0; i < len(data); i += genesisChunkSize {
end := i + genesisChunkSize

if end > len(data) {
end = len(data)
}

env.genChunks = append(env.genChunks, base64.StdEncoding.EncodeToString(data[i:end]))
}

return nil
}

func validateSkipCount(page, perPage int) int {
skipCount := (page - 1) * perPage
if skipCount < 0 {
Expand Down
26 changes: 26 additions & 0 deletions rpc/core/net.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,35 @@ func UnsafeDialPeers(ctx *rpctypes.Context, peers []string, persistent, uncondit
// Genesis returns genesis file.
// More: https://docs.tendermint.com/master/rpc/#/Info/genesis
func Genesis(ctx *rpctypes.Context) (*ctypes.ResultGenesis, error) {
if len(env.genChunks) > 1 {
return nil, errors.New("genesis response is large, please use the genesis_chunked API instead")
}

return &ctypes.ResultGenesis{Genesis: env.GenDoc}, nil
}

func GenesisChunked(ctx *rpctypes.Context, chunk uint) (*ctypes.ResultGenesisChunk, error) {
if env.genChunks == nil {
return nil, fmt.Errorf("service configuration error, genesis chunks are not initialized")
}

if len(env.genChunks) == 0 {
return nil, fmt.Errorf("service configuration error, there are no chunks")
}

id := int(chunk)

if id > len(env.genChunks)-1 {
return nil, fmt.Errorf("there are %d chunks, %d is invalid", len(env.genChunks)-1, id)
}

return &ctypes.ResultGenesisChunk{
TotalChunks: len(env.genChunks),
ChunkNumber: id,
Data: env.genChunks[id],
}, nil
}

func getIDs(peers []string) ([]string, error) {
ids := make([]string, 0, len(peers))

Expand Down
1 change: 1 addition & 0 deletions rpc/core/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ var Routes = map[string]*rpc.RPCFunc{
"net_info": rpc.NewRPCFunc(NetInfo, ""),
"blockchain": rpc.NewRPCFunc(BlockchainInfo, "minHeight,maxHeight"),
"genesis": rpc.NewRPCFunc(Genesis, ""),
"genesis_chunked": rpc.NewRPCFunc(GenesisChunked, "chunk"),
"block": rpc.NewRPCFunc(Block, "height"),
"block_by_hash": rpc.NewRPCFunc(BlockByHash, "hash"),
"block_results": rpc.NewRPCFunc(BlockResults, "height"),
Expand Down
10 changes: 10 additions & 0 deletions rpc/core/types/responses.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,16 @@ type ResultGenesis struct {
Genesis *types.GenesisDoc `json:"genesis"`
}

// ResultGenesisChunk is the output format for the chunked/paginated
// interface. These chunks are produced by converting the genesis
// document to JSON and then splitting the resulting payload into
// 16 megabyte blocks and then base64 encoding each block.
type ResultGenesisChunk struct {
ChunkNumber int `json:"chunk"`
TotalChunks int `json:"total"`
Data string `json:"data"`
}

// Single block (with meta)
type ResultBlock struct {
BlockID types.BlockID `json:"block_id"`
Expand Down

0 comments on commit da9eefd

Please sign in to comment.