mirror of
https://github.com/tendermint/tendermint.git
synced 2026-01-08 06:15:33 +00:00
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
257 lines
7.6 KiB
Go
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)
|
|
// }
|