Files
tendermint/lite/proxy/wrapper.go
Anton Kaliaev 44a3fbf109 rpc/lib/client & server: try to conform to JSON-RPC 2.0 spec (#4141)
https://www.jsonrpc.org/specification

What is done in this PR:

    JSONRPCClient: validate that Response.ID matches Request.ID I wanted
    to do the same for the WSClient, but since we're sending events as
    responses, not notifications, checking IDs would require storing
    them in memory indefinitely (and we won't be able to remove them
    upon client unsubscribing because ID is different then).

    Request.ID is now optional. Notification is a Request without an ID.
    Previously "" or 0 were considered as notifications

    Remove #event suffix from ID from an event response (partially fixes
    #2949) ID must be either string, int or null AND must be equal to
    request's ID. Now, because we've implemented events as responses, WS
    clients are tripping when they see Response.ID("0#event") !=
    Request.ID("0"). Implementing events as requests would require a lot
    of time (~ 2 days to completely rewrite WS client and server)

    generate unique ID for each request

    switch to integer IDs instead of "json-client-XYZ"
    id=0 method=/subscribe
    id=0 result=...
    id=1 method=/abci_query
    id=1 result=...

> send events (resulting from /subscribe) as requests+notifications (not
responses)

this will require a lot of work. probably not worth it

* rpc: generate an unique ID for each request
in conformance with JSON-RPC spec

* WSClient: check for unsolicited responses

* fix golangci warnings

* save commit

* fix errors

* remove ID from responses from subscribe
Refs #2949

* clients are safe for concurrent access

* tm-bench: switch to int ID

* fixes after my own review

* comment out sentIDs in WSClient
see commit body for the reason

* remove body.Close
it will be closed automatically

* stop ws connection outside of write/read routines
also, use t.Rate in tm-bench indexer when calculating ID

fix gocritic issues

* update swagger.yaml

* Apply suggestions from code review

* fix stylecheck and golint linter warnings

* update changelog

* update changelog2
2019-11-15 14:16:04 +04:00

257 lines
7.6 KiB
Go

package proxy
import (
"context"
cmn "github.com/tendermint/tendermint/libs/common"
"github.com/tendermint/tendermint/crypto/merkle"
"github.com/tendermint/tendermint/lite"
rpcclient "github.com/tendermint/tendermint/rpc/client"
ctypes "github.com/tendermint/tendermint/rpc/core/types"
rpctypes "github.com/tendermint/tendermint/rpc/lib/types"
)
var _ rpcclient.Client = Wrapper{}
// Wrapper wraps a rpcclient with a Verifier and double-checks any input that is
// provable before passing it along. Allows you to make any rpcclient fully secure.
type Wrapper struct {
rpcclient.Client
cert *lite.DynamicVerifier
prt *merkle.ProofRuntime
}
// SecureClient uses a given Verifier to wrap an connection to an untrusted
// host and return a cryptographically secure rpc client.
//
// If it is wrapping an HTTP rpcclient, it will also wrap the websocket interface
func SecureClient(c rpcclient.Client, cert *lite.DynamicVerifier) Wrapper {
prt := defaultProofRuntime()
wrap := Wrapper{c, cert, prt}
// TODO: no longer possible as no more such interface exposed....
// if we wrap http client, then we can swap out the event switch to filter
// if hc, ok := c.(*rpcclient.HTTP); ok {
// evt := hc.WSEvents.EventSwitch
// hc.WSEvents.EventSwitch = WrappedSwitch{evt, wrap}
// }
return wrap
}
// ABCIQueryWithOptions exposes all options for the ABCI query and verifies the returned proof
func (w Wrapper) ABCIQueryWithOptions(path string, data cmn.HexBytes,
opts rpcclient.ABCIQueryOptions) (*ctypes.ResultABCIQuery, error) {
res, err := GetWithProofOptions(w.prt, path, data, opts, w.Client, w.cert)
return res, err
}
// ABCIQuery uses default options for the ABCI query and verifies the returned proof
func (w Wrapper) ABCIQuery(path string, data cmn.HexBytes) (*ctypes.ResultABCIQuery, error) {
return w.ABCIQueryWithOptions(path, data, rpcclient.DefaultABCIQueryOptions)
}
// Tx queries for a given tx and verifies the proof if it was requested
func (w Wrapper) Tx(hash []byte, prove bool) (*ctypes.ResultTx, error) {
res, err := w.Client.Tx(hash, prove)
if !prove || err != nil {
return res, err
}
h := res.Height
sh, err := GetCertifiedCommit(h, w.Client, w.cert)
if err != nil {
return res, err
}
err = res.Proof.Validate(sh.DataHash)
return res, err
}
// BlockchainInfo requests a list of headers and verifies them all...
// Rather expensive.
//
// TODO: optimize this if used for anything needing performance
func (w Wrapper) BlockchainInfo(minHeight, maxHeight int64) (*ctypes.ResultBlockchainInfo, error) {
r, err := w.Client.BlockchainInfo(minHeight, maxHeight)
if err != nil {
return nil, err
}
// go and verify every blockmeta in the result....
for _, meta := range r.BlockMetas {
// get a checkpoint to verify from
res, err := w.Commit(&meta.Header.Height)
if err != nil {
return nil, err
}
sh := res.SignedHeader
err = ValidateBlockMeta(meta, sh)
if err != nil {
return nil, err
}
}
return r, nil
}
// Block returns an entire block and verifies all signatures
func (w Wrapper) Block(height *int64) (*ctypes.ResultBlock, error) {
resBlock, err := w.Client.Block(height)
if err != nil {
return nil, err
}
// get a checkpoint to verify from
resCommit, err := w.Commit(height)
if err != nil {
return nil, err
}
sh := resCommit.SignedHeader
err = ValidateBlock(resBlock.Block, sh)
if err != nil {
return nil, err
}
return resBlock, nil
}
// Commit downloads the Commit and certifies it with the lite.
//
// This is the foundation for all other verification in this module
func (w Wrapper) Commit(height *int64) (*ctypes.ResultCommit, error) {
if height == nil {
resStatus, err := w.Client.Status()
if err != nil {
return nil, err
}
// NOTE: If resStatus.CatchingUp, there is a race
// condition where the validator set for the next height
// isn't available until some time after the blockstore
// has height h on the remote node. This isn't an issue
// once the node has caught up, and a syncing node likely
// won't have this issue esp with the implementation we
// have here, but we may have to address this at some
// point.
height = new(int64)
*height = resStatus.SyncInfo.LatestBlockHeight
}
rpcclient.WaitForHeight(w.Client, *height, nil)
res, err := w.Client.Commit(height)
// if we got it, then verify it
if err == nil {
sh := res.SignedHeader
err = w.cert.Verify(sh)
}
return res, err
}
func (w Wrapper) RegisterOpDecoder(typ string, dec merkle.OpDecoder) {
w.prt.RegisterOpDecoder(typ, dec)
}
// SubscribeWS subscribes for events using the given query and remote address as
// a subscriber, but does not verify responses (UNSAFE)!
func (w Wrapper) SubscribeWS(ctx *rpctypes.Context, query string) (*ctypes.ResultSubscribe, error) {
out, err := w.Client.Subscribe(context.Background(), ctx.RemoteAddr(), query)
if err != nil {
return nil, err
}
go func() {
for {
select {
case resultEvent := <-out:
// XXX(melekes) We should have a switch here that performs a validation
// depending on the event's type.
ctx.WSConn.TryWriteRPCResponse(
rpctypes.NewRPCSuccessResponse(
ctx.WSConn.Codec(),
ctx.JSONReq.ID,
resultEvent,
))
case <-w.Client.Quit():
return
}
}
}()
return &ctypes.ResultSubscribe{}, nil
}
// UnsubscribeWS calls original client's Unsubscribe using remote address as a
// subscriber.
func (w Wrapper) UnsubscribeWS(ctx *rpctypes.Context, query string) (*ctypes.ResultUnsubscribe, error) {
err := w.Client.Unsubscribe(context.Background(), ctx.RemoteAddr(), query)
if err != nil {
return nil, err
}
return &ctypes.ResultUnsubscribe{}, nil
}
// UnsubscribeAllWS calls original client's UnsubscribeAll using remote address
// as a subscriber.
func (w Wrapper) UnsubscribeAllWS(ctx *rpctypes.Context) (*ctypes.ResultUnsubscribe, error) {
err := w.Client.UnsubscribeAll(context.Background(), ctx.RemoteAddr())
if err != nil {
return nil, err
}
return &ctypes.ResultUnsubscribe{}, nil
}
// // WrappedSwitch creates a websocket connection that auto-verifies any info
// // coming through before passing it along.
// //
// // Since the verification takes 1-2 rpc calls, this is obviously only for
// // relatively low-throughput situations that can tolerate a bit extra latency
// type WrappedSwitch struct {
// types.EventSwitch
// client rpcclient.Client
// }
// // FireEvent verifies any block or header returned from the eventswitch
// func (s WrappedSwitch) FireEvent(event string, data events.EventData) {
// tm, ok := data.(types.TMEventData)
// if !ok {
// fmt.Printf("bad type %#v\n", data)
// return
// }
// // check to validate it if possible, and drop if not valid
// switch t := tm.(type) {
// case types.EventDataNewBlockHeader:
// err := verifyHeader(s.client, t.Header)
// if err != nil {
// fmt.Printf("Invalid header: %#v\n", err)
// return
// }
// case types.EventDataNewBlock:
// err := verifyBlock(s.client, t.Block)
// if err != nil {
// fmt.Printf("Invalid block: %#v\n", err)
// return
// }
// // TODO: can we verify tx as well? anything else
// }
// // looks good, we fire it
// s.EventSwitch.FireEvent(event, data)
// }
// func verifyHeader(c rpcclient.Client, head *types.Header) error {
// // get a checkpoint to verify from
// commit, err := c.Commit(&head.Height)
// if err != nil {
// return err
// }
// check := certclient.CommitFromResult(commit)
// return ValidateHeader(head, check)
// }
//
// func verifyBlock(c rpcclient.Client, block *types.Block) error {
// // get a checkpoint to verify from
// commit, err := c.Commit(&block.Height)
// if err != nil {
// return err
// }
// check := certclient.CommitFromResult(commit)
// return ValidateBlock(block, check)
// }