package proxy import ( "fmt" "strings" "github.com/pkg/errors" "github.com/tendermint/tendermint/crypto/merkle" cmn "github.com/tendermint/tendermint/libs/common" "github.com/tendermint/tendermint/lite" lerr "github.com/tendermint/tendermint/lite/errors" rpcclient "github.com/tendermint/tendermint/rpc/client" ctypes "github.com/tendermint/tendermint/rpc/core/types" "github.com/tendermint/tendermint/types" ) // GetWithProof will query the key on the given node, and verify it has // a valid proof, as defined by the Verifier. // // If there is any error in checking, returns an error. func GetWithProof(prt *merkle.ProofRuntime, key []byte, reqHeight int64, node rpcclient.Client, cert lite.Verifier) ( val cmn.HexBytes, height int64, proof *merkle.Proof, err error) { if reqHeight < 0 { err = errors.New("Height cannot be negative") return } res, err := GetWithProofOptions(prt, "/key", key, rpcclient.ABCIQueryOptions{Height: reqHeight, Prove: true}, node, cert) if err != nil { return } resp := res.Response val, height = resp.Value, resp.Height return val, height, proof, err } // GetWithProofOptions is useful if you want full access to the ABCIQueryOptions. // XXX Usage of path? It's not used, and sometimes it's /, sometimes /key, sometimes /store. func GetWithProofOptions(prt *merkle.ProofRuntime, path string, key []byte, opts rpcclient.ABCIQueryOptions, node rpcclient.Client, cert lite.Verifier) ( *ctypes.ResultABCIQuery, error) { opts.Prove = true res, err := node.ABCIQueryWithOptions(path, key, opts) if err != nil { return nil, err } resp := res.Response // Validate the response, e.g. height. if resp.IsErr() { err = errors.Errorf("Query error for key %d: %d", key, resp.Code) return nil, err } if len(resp.Key) == 0 || resp.Proof == nil { return nil, lerr.ErrEmptyTree() } if resp.Height == 0 { return nil, errors.New("Height returned is zero") } // AppHash for height H is in header H+1 signedHeader, err := GetCertifiedCommit(resp.Height+1, node, cert) if err != nil { return nil, err } // Validate the proof against the certified header to ensure data integrity. if resp.Value != nil { // Value exists // XXX How do we encode the key into a string... storeName, err := parseQueryStorePath(path) if err != nil { return nil, err } kp := merkle.KeyPath{} kp = kp.AppendKey([]byte(storeName), merkle.KeyEncodingURL) kp = kp.AppendKey(resp.Key, merkle.KeyEncodingURL) err = prt.VerifyValue(resp.Proof, signedHeader.AppHash, kp.String(), resp.Value) if err != nil { return nil, errors.Wrap(err, "Couldn't verify value proof") } return &ctypes.ResultABCIQuery{Response: resp}, nil } else { // Value absent // Validate the proof against the certified header to ensure data integrity. // XXX How do we encode the key into a string... err = prt.VerifyAbsence(resp.Proof, signedHeader.AppHash, string(resp.Key)) if err != nil { return nil, errors.Wrap(err, "Couldn't verify absence proof") } return &ctypes.ResultABCIQuery{Response: resp}, nil } } func parseQueryStorePath(path string) (storeName string, err error) { if !strings.HasPrefix(path, "/") { return "", fmt.Errorf("expected path to start with /") } paths := strings.SplitN(path[1:], "/", 3) switch { case len(paths) != 3: return "", fmt.Errorf("expected format like /store//key") case paths[0] != "store": return "", fmt.Errorf("expected format like /store//key") case paths[2] != "key": return "", fmt.Errorf("expected format like /store//key") } return paths[1], nil } // GetCertifiedCommit gets the signed header for a given height and certifies // it. Returns error if unable to get a proven header. func GetCertifiedCommit(h int64, client rpcclient.Client, cert lite.Verifier) (types.SignedHeader, error) { // FIXME: cannot use cert.GetByHeight for now, as it also requires // Validators and will fail on querying tendermint for non-current height. // When this is supported, we should use it instead... rpcclient.WaitForHeight(client, h, nil) cresp, err := client.Commit(&h) if err != nil { return types.SignedHeader{}, err } // Validate downloaded checkpoint with our request and trust store. sh := cresp.SignedHeader if sh.Height != h { return types.SignedHeader{}, fmt.Errorf("height mismatch: want %v got %v", h, sh.Height) } if err = cert.Verify(sh); err != nil { return types.SignedHeader{}, err } return sh, nil }