Skip to content

Commit

Permalink
IRISHUB-238: Add multiply store proof build and verification
Browse files Browse the repository at this point in the history
  • Loading branch information
HaoyangLiu committed Aug 30, 2018
1 parent fd8c1e5 commit 703c643
Show file tree
Hide file tree
Showing 8 changed files with 401 additions and 0 deletions.
46 changes: 46 additions & 0 deletions client/context/client_manager.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package context

import (
"github.com/pkg/errors"
rpcclient "github.com/tendermint/tendermint/rpc/client"
"strings"
"sync"
)

// ClientManager is a manager of a set of rpc clients to full nodes.
// This manager can do load balancing upon these rpc clients.
type ClientManager struct {
clients []rpcclient.Client
currentIndex int
mutex sync.Mutex
}

// NewClientManager create a new ClientManager
func NewClientManager(nodeURIs string) (*ClientManager, error) {
if nodeURIs != "" {
nodeURLArray := strings.Split(nodeURIs, ",")
var clients []rpcclient.Client
for _, url := range nodeURLArray {
client := rpcclient.NewHTTP(url, "/websocket")
clients = append(clients, client)
}
mgr := &ClientManager{
currentIndex: 0,
clients: clients,
}
return mgr, nil
}
return nil, errors.New("missing node URIs")
}

func (mgr *ClientManager) getClient() rpcclient.Client {
mgr.mutex.Lock()
defer mgr.mutex.Unlock()

client := mgr.clients[mgr.currentIndex]
mgr.currentIndex++
if mgr.currentIndex >= len(mgr.clients) {
mgr.currentIndex = 0
}
return client
}
16 changes: 16 additions & 0 deletions client/context/client_manager_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package context

import (
"github.com/stretchr/testify/assert"
"testing"
)

func TestClientManager(t *testing.T) {
nodeURIs := "10.10.10.10:26657,20.20.20.20:26657,30.30.30.30:26657"
clientMgr, err := NewClientManager(nodeURIs)
assert.Empty(t, err)
endpoint := clientMgr.getClient()
assert.NotEqual(t, endpoint, clientMgr.getClient())
clientMgr.getClient()
assert.Equal(t, endpoint, clientMgr.getClient())
}
15 changes: 15 additions & 0 deletions client/context/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/spf13/viper"

rpcclient "github.com/tendermint/tendermint/rpc/client"
tendermintLite "github.com/tendermint/tendermint/lite"
)

const ctxAccStoreName = "acc"
Expand All @@ -32,6 +33,8 @@ type CLIContext struct {
Async bool
JSON bool
PrintResponse bool
Certifier tendermintLite.Certifier
ClientManager *ClientManager
}

// NewCLIContext returns a new initialized CLIContext with parameters from the
Expand Down Expand Up @@ -117,3 +120,15 @@ func (ctx CLIContext) WithUseLedger(useLedger bool) CLIContext {
ctx.UseLedger = useLedger
return ctx
}

// WithCertifier - return a copy of the context with an updated Certifier
func (ctx CLIContext) WithCertifier(certifier tendermintLite.Certifier) CLIContext {
ctx.Certifier = certifier
return ctx
}

// WithClientManager - return a copy of the context with an updated ClientManager
func (ctx CLIContext) WithClientManager(clientManager *ClientManager) CLIContext {
ctx.ClientManager = clientManager
return ctx
}
79 changes: 79 additions & 0 deletions client/context/query.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ import (
cmn "github.com/tendermint/tendermint/libs/common"
rpcclient "github.com/tendermint/tendermint/rpc/client"
ctypes "github.com/tendermint/tendermint/rpc/core/types"
"github.com/cosmos/cosmos-sdk/store"
"github.com/cosmos/cosmos-sdk/wire"
"strings"
tendermintLiteProxy "github.com/tendermint/tendermint/lite/proxy"
abci "github.com/tendermint/tendermint/abci/types"
)

// GetNode returns an RPC client. If the context's client is not defined, an
Expand Down Expand Up @@ -304,12 +309,86 @@ func (ctx CLIContext) query(path string, key cmn.HexBytes) (res []byte, err erro
return res, errors.Errorf("query failed: (%d) %s", resp.Code, resp.Log)
}

// Data from trusted node or subspace query doesn't need verification
if ctx.TrustNode || !isQueryStoreWithProof(path) {
return resp.Value, nil
}

err = ctx.verifyProof(path, resp)
if err != nil {
return nil, err
}

return resp.Value, nil
}

// verifyProof perform response proof verification
func (ctx CLIContext) verifyProof(path string, resp abci.ResponseQuery) error {

// TODO: Later we consider to return error for missing valid certifier to verify data from untrusted node
if ctx.Certifier == nil {
if ctx.Logger != nil {
io.WriteString(ctx.Logger, fmt.Sprintf("Missing valid certifier to verify data from untrusted node\n"))
}
return nil
}

node, err := ctx.GetNode()
if err != nil {
return err
}

// TODO: need improvement
// If the the node http client connect to a full node which can't produce or receive new blocks,
// then here the code will wait for a while and return error if time is out.
// AppHash for height H is in header H+1
commit, err := tendermintLiteProxy.GetCertifiedCommit(resp.Height+1, node, ctx.Certifier)
if err != nil {
return err
}

var multiStoreProof store.MultiStoreProof
cdc := wire.NewCodec()
err = cdc.UnmarshalBinary(resp.Proof, &multiStoreProof)
if err != nil {
return errors.Wrap(err, "failed to unmarshalBinary rangeProof")
}

// Validate the substore commit hash against trusted appHash
substoreCommitHash, err := store.VerifyMultiStoreCommitInfo(multiStoreProof.StoreName,
multiStoreProof.CommitIDList, commit.Header.AppHash)
if err != nil {
return errors.Wrap(err, "failed in verifying the proof against appHash")
}
err = store.VerifyRangeProof(resp.Key, resp.Value, substoreCommitHash, &multiStoreProof.RangeProof)
if err != nil {
return errors.Wrap(err, "failed in the range proof verification")
}
return nil
}

// queryStore performs a query from a Tendermint node with the provided a store
// name and path.
func (ctx CLIContext) queryStore(key cmn.HexBytes, storeName, endPath string) ([]byte, error) {
path := fmt.Sprintf("/store/%s/%s", storeName, endPath)
return ctx.query(path, key)
}

// isQueryStoreWithProof expects a format like /<queryType>/<storeName>/<subpath>
// queryType can be app or store
// if subpath equals to "/store" or "/key", then return true
func isQueryStoreWithProof(path string) (bool) {
if !strings.HasPrefix(path, "/") {
return false
}
paths := strings.SplitN(path[1:], "/", 3)
if len(paths) != 3 {
return false
}
// Currently, only when query subpath is "/store" or "/key", will proof be included in response.
// If there are some changes about proof building in iavlstore.go, we must change code here to keep consistency with iavlstore.go
if paths[2] == "store" || paths[2] == "key" {
return true
}
return false
}
14 changes: 14 additions & 0 deletions client/lcd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ import (
cmn "github.com/tendermint/tendermint/libs/common"
"github.com/tendermint/tendermint/libs/log"
tmserver "github.com/tendermint/tendermint/rpc/lib/server"
tendermintLiteProxy "github.com/tendermint/tendermint/lite/proxy"
"github.com/tendermint/tendermint/libs/cli"
tendermintLite "github.com/tendermint/tendermint/lite"
)

// ServeCommand will generate a long-running rest server
Expand Down Expand Up @@ -80,6 +83,17 @@ func createHandler(cdc *wire.Codec) http.Handler {

cliCtx := context.NewCLIContext().WithCodec(cdc).WithLogger(os.Stdout)

chainID := viper.GetString(client.FlagChainID)
home := viper.GetString(cli.HomeFlag)
nodeURI := viper.GetString(client.FlagNode)
var certifier tendermintLite.Certifier
if chainID != "" && home != "" && nodeURI != ""{
certifier, err = tendermintLiteProxy.GetCertifier(chainID, home, nodeURI)
if err != nil {
panic(err)
}
cliCtx = cliCtx.WithCertifier(certifier)
}
// TODO: make more functional? aka r = keys.RegisterRoutes(r)
r.HandleFunc("/version", CLIVersionRequestHandler).Methods("GET")
r.HandleFunc("/node_version", NodeVersionRequestHandler(cliCtx)).Methods("GET")
Expand Down
116 changes: 116 additions & 0 deletions store/multistoreproof.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package store

import (
"bytes"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/pkg/errors"
"github.com/tendermint/iavl"
cmn "github.com/tendermint/tendermint/libs/common"
)

// commitID of substores, such as acc store, gov store
type SubstoreCommitID struct {
Name string `json:"name"`
Version int64 `json:"version"`
CommitHash cmn.HexBytes `json:"commit_hash"`
}

// proof of store which have multi substores
type MultiStoreProof struct {
CommitIDList []SubstoreCommitID `json:"commit_id_list"`
StoreName string `json:"store_name"`
RangeProof iavl.RangeProof `json:"range_proof"`
}

// build MultiStoreProof based on iavl proof and storeInfos
func BuildMultiStoreProof(iavlProof []byte, storeName string, storeInfos []storeInfo) ([]byte, error) {
var rangeProof iavl.RangeProof
err := cdc.UnmarshalBinary(iavlProof, &rangeProof)
if err != nil {
return nil, err
}

var multiStoreProof MultiStoreProof
for _, storeInfo := range storeInfos {

commitID := SubstoreCommitID{
Name: storeInfo.Name,
Version: storeInfo.Core.CommitID.Version,
CommitHash: storeInfo.Core.CommitID.Hash,
}
multiStoreProof.CommitIDList = append(multiStoreProof.CommitIDList, commitID)
}
multiStoreProof.StoreName = storeName
multiStoreProof.RangeProof = rangeProof

proof, err := cdc.MarshalBinary(multiStoreProof)
if err != nil {
return nil, err
}

return proof, nil
}

// verify multiStoreCommitInfo against appHash
func VerifyMultiStoreCommitInfo(storeName string, multiStoreCommitInfo []SubstoreCommitID, appHash []byte) ([]byte, error) {
var substoreCommitHash []byte
var storeInfos []storeInfo
var height int64
for _, multiStoreCommitID := range multiStoreCommitInfo {

if multiStoreCommitID.Name == storeName {
substoreCommitHash = multiStoreCommitID.CommitHash
height = multiStoreCommitID.Version
}
storeInfo := storeInfo{
Name: multiStoreCommitID.Name,
Core: storeCore{
CommitID: sdk.CommitID{
Version: multiStoreCommitID.Version,
Hash: multiStoreCommitID.CommitHash,
},
},
}

storeInfos = append(storeInfos, storeInfo)
}
if len(substoreCommitHash) == 0 {
return nil, cmn.NewError("failed to get substore root commit hash by store name")
}

ci := commitInfo{
Version: height,
StoreInfos: storeInfos,
}

if !bytes.Equal(appHash, ci.Hash()) {
return nil, cmn.NewError("the merkle root of multiStoreCommitInfo doesn't equal to appHash")
}
return substoreCommitHash, nil
}

// verify iavl proof
func VerifyRangeProof(key, value []byte, substoreCommitHash []byte, rangeProof *iavl.RangeProof) error {

// Validate the proof to ensure data integrity.
err := rangeProof.Verify(substoreCommitHash)
if err != nil {
return errors.Wrap(err, "proof root hash doesn't equal to substore commit root hash")
}

if len(value) != 0 {
// Validate existence proof
err = rangeProof.VerifyItem(key, value)
if err != nil {
return errors.Wrap(err, "failed in existence verification")
}
} else {
// Validate absence proof
err = rangeProof.VerifyAbsence(key)
if err != nil {
return errors.Wrap(err, "failed in absence verification")
}
}

return nil
}
Loading

0 comments on commit 703c643

Please sign in to comment.