diff --git a/client/context/client_manager.go b/client/context/client_manager.go new file mode 100644 index 000000000..8fffb9d65 --- /dev/null +++ b/client/context/client_manager.go @@ -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 +} diff --git a/client/context/client_manager_test.go b/client/context/client_manager_test.go new file mode 100644 index 000000000..c060e11ce --- /dev/null +++ b/client/context/client_manager_test.go @@ -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()) +} \ No newline at end of file diff --git a/client/context/context.go b/client/context/context.go index 743c92355..b57e33665 100644 --- a/client/context/context.go +++ b/client/context/context.go @@ -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" @@ -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 @@ -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 +} diff --git a/client/context/query.go b/client/context/query.go index e526c0abb..72defb0d4 100644 --- a/client/context/query.go +++ b/client/context/query.go @@ -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 @@ -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 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 +} diff --git a/client/lcd/root.go b/client/lcd/root.go index bfa62f1cf..71c9a5114 100644 --- a/client/lcd/root.go +++ b/client/lcd/root.go @@ -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 @@ -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") diff --git a/store/multistoreproof.go b/store/multistoreproof.go new file mode 100644 index 000000000..e98a198e9 --- /dev/null +++ b/store/multistoreproof.go @@ -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 +} \ No newline at end of file diff --git a/store/multistoreproof_test.go b/store/multistoreproof_test.go new file mode 100644 index 000000000..b1667c726 --- /dev/null +++ b/store/multistoreproof_test.go @@ -0,0 +1,96 @@ +package store + +import ( + "encoding/hex" + "github.com/stretchr/testify/assert" + "github.com/tendermint/iavl" + cmn "github.com/tendermint/tendermint/libs/common" + "testing" +) + +func TestVerifyMultiStoreCommitInfo(t *testing.T) { + appHash, _ := hex.DecodeString("ebf3c1fb724d3458023c8fefef7b33add2fc1e84") + + substoreRootHash, _ := hex.DecodeString("ea5d468431015c2cd6295e9a0bb1fc0e49033828") + storeName := "acc" + + var multiStoreCommitInfo []SubstoreCommitID + + gocRootHash, _ := hex.DecodeString("62c171bb022e47d1f745608ff749e676dbd25f78") + multiStoreCommitInfo = append(multiStoreCommitInfo, SubstoreCommitID{ + Name: "gov", + Version: 689, + CommitHash: gocRootHash, + }) + + multiStoreCommitInfo = append(multiStoreCommitInfo, SubstoreCommitID{ + Name: "main", + Version: 689, + CommitHash: nil, + }) + + accRootHash, _ := hex.DecodeString("ea5d468431015c2cd6295e9a0bb1fc0e49033828") + multiStoreCommitInfo = append(multiStoreCommitInfo, SubstoreCommitID{ + Name: "acc", + Version: 689, + CommitHash: accRootHash, + }) + + multiStoreCommitInfo = append(multiStoreCommitInfo, SubstoreCommitID{ + Name: "ibc", + Version: 689, + CommitHash: nil, + }) + + stakeRootHash, _ := hex.DecodeString("987d1d27b8771d93aa3691262f661d2c85af7ca4") + multiStoreCommitInfo = append(multiStoreCommitInfo, SubstoreCommitID{ + Name: "stake", + Version: 689, + CommitHash: stakeRootHash, + }) + + slashingRootHash, _ := hex.DecodeString("388ee6e5b11f367069beb1eefd553491afe9d73e") + multiStoreCommitInfo = append(multiStoreCommitInfo, SubstoreCommitID{ + Name: "slashing", + Version: 689, + CommitHash: slashingRootHash, + }) + + commitHash, err := VerifyMultiStoreCommitInfo(storeName, multiStoreCommitInfo, appHash) + assert.Nil(t, err) + assert.Equal(t, commitHash, substoreRootHash) + + appHash, _ = hex.DecodeString("29de216bf5e2531c688de36caaf024cd3bb09ee3") + + _, err = VerifyMultiStoreCommitInfo(storeName, multiStoreCommitInfo, appHash) + assert.Error(t, err, "appHash doesn't match to the merkle root of multiStoreCommitInfo") +} + +func TestVerifyRangeProof(t *testing.T) { + tree := iavl.NewTree(nil, 0) + + rand := cmn.NewRand() + rand.Seed(0) // for determinism + for _, ikey := range []byte{0x11, 0x32, 0x50, 0x72, 0x99} { + key := []byte{ikey} + tree.Set(key, []byte(rand.Str(8))) + } + + root := tree.Hash() + + key := []byte{0x32} + val, proof, err := tree.GetWithProof(key) + assert.Nil(t, err) + assert.NotEmpty(t, val) + assert.NotEmpty(t, proof) + err = VerifyRangeProof(key, val, root, proof) + assert.Nil(t, err) + + key = []byte{0x40} + val, proof, err = tree.GetWithProof(key) + assert.Nil(t, err) + assert.Empty(t, val) + assert.NotEmpty(t, proof) + err = VerifyRangeProof(key, val, root, proof) + assert.Nil(t, err) +} \ No newline at end of file diff --git a/store/rootmultistore.go b/store/rootmultistore.go index 04f8e44e6..2a9a9373a 100644 --- a/store/rootmultistore.go +++ b/store/rootmultistore.go @@ -291,6 +291,25 @@ func (rs *rootMultiStore) Query(req abci.RequestQuery) abci.ResponseQuery { // trim the path and make the query req.Path = subpath res := queryable.Query(req) + + + // 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 !req.Prove || subpath != "/store" && subpath != "/key" { + return res + } + + //Load commit info from db + commitInfo, errMsg := getCommitInfo(rs.db,res.Height) + if errMsg != nil { + return sdk.ErrInternal(errMsg.Error()).QueryResult() + } + + res.Proof, errMsg = BuildMultiStoreProof(res.Proof, storeName, commitInfo.StoreInfos) + if errMsg != nil { + return sdk.ErrInternal(errMsg.Error()).QueryResult() + } + return res }