package commands import ( "context" "errors" "fmt" "net/http" "os" "os/signal" "path/filepath" "strings" "syscall" "time" "github.com/spf13/cobra" dbm "github.com/tendermint/tm-db" "github.com/tendermint/tendermint/config" "github.com/tendermint/tendermint/libs/log" tmmath "github.com/tendermint/tendermint/libs/math" "github.com/tendermint/tendermint/light" lproxy "github.com/tendermint/tendermint/light/proxy" lrpc "github.com/tendermint/tendermint/light/rpc" dbs "github.com/tendermint/tendermint/light/store/db" rpcserver "github.com/tendermint/tendermint/rpc/jsonrpc/server" ) // LightCmd constructs the base command called when invoked without any subcommands. func MakeLightCommand(conf *config.Config, logger log.Logger) *cobra.Command { var ( listenAddr string primaryAddr string witnessAddrsJoined string chainID string dir string maxOpenConnections int sequential bool trustingPeriod time.Duration trustedHeight int64 trustedHash []byte trustLevelStr string logLevel string logFormat string primaryKey = []byte("primary") witnessesKey = []byte("witnesses") ) checkForExistingProviders := func(db dbm.DB) (string, []string, error) { primaryBytes, err := db.Get(primaryKey) if err != nil { return "", []string{""}, err } witnessesBytes, err := db.Get(witnessesKey) if err != nil { return "", []string{""}, err } witnessesAddrs := strings.Split(string(witnessesBytes), ",") return string(primaryBytes), witnessesAddrs, nil } saveProviders := func(db dbm.DB, primaryAddr, witnessesAddrs string) error { err := db.Set(primaryKey, []byte(primaryAddr)) if err != nil { return fmt.Errorf("failed to save primary provider: %w", err) } err = db.Set(witnessesKey, []byte(witnessesAddrs)) if err != nil { return fmt.Errorf("failed to save witness providers: %w", err) } return nil } cmd := &cobra.Command{ Use: "light [chainID]", Short: "Run a light client proxy server, verifying Tendermint rpc", Long: `Run a light client proxy server, verifying Tendermint rpc. All calls that can be tracked back to a block header by a proof will be verified before passing them back to the caller. Other than that, it will present the same interface as a full Tendermint node. Furthermore to the chainID, a fresh instance of a light client will need a primary RPC address and a trusted hash and height. It is also highly recommended to provide additional witness RPC addresses, especially if not using sequential verification. To restart the node, thereafter only the chainID is required. When /abci_query is called, the Merkle key path format is: /{store name}/{key} Please verify with your application that this Merkle key format is used (true for applications built w/ Cosmos SDK). `, RunE: func(cmd *cobra.Command, args []string) error { chainID = args[0] logger.Info("Creating client...", "chainID", chainID) var witnessesAddrs []string if witnessAddrsJoined != "" { witnessesAddrs = strings.Split(witnessAddrsJoined, ",") } lightDB, err := dbm.NewGoLevelDB("light-client-db", dir) if err != nil { return fmt.Errorf("can't create a db: %w", err) } // create a prefixed db on the chainID db := dbm.NewPrefixDB(lightDB, []byte(chainID)) if primaryAddr == "" { // check to see if we can start from an existing state var err error primaryAddr, witnessesAddrs, err = checkForExistingProviders(db) if err != nil { return fmt.Errorf("failed to retrieve primary or witness from db: %w", err) } if primaryAddr == "" { return errors.New("no primary address was provided nor found. Please provide a primary (using -p)." + " Run the command: tendermint light --help for more information") } } else { err := saveProviders(db, primaryAddr, witnessAddrsJoined) if err != nil { logger.Error("Unable to save primary and or witness addresses", "err", err) } } if len(witnessesAddrs) < 1 && !sequential { logger.Info("In skipping verification mode it is highly recommended to provide at least one witness") } trustLevel, err := tmmath.ParseFraction(trustLevelStr) if err != nil { return fmt.Errorf("can't parse trust level: %w", err) } options := []light.Option{light.Logger(logger)} vo := light.SkippingVerification(trustLevel) if sequential { vo = light.SequentialVerification() } options = append(options, vo) // Initiate the light client. If the trusted store already has blocks in it, this // will be used else we use the trusted options. c, err := light.NewHTTPClient( context.Background(), chainID, light.TrustOptions{ Period: trustingPeriod, Height: trustedHeight, Hash: trustedHash, }, primaryAddr, witnessesAddrs, dbs.New(db), options..., ) if err != nil { return err } cfg := rpcserver.DefaultConfig() cfg.MaxBodyBytes = conf.RPC.MaxBodyBytes cfg.MaxHeaderBytes = conf.RPC.MaxHeaderBytes cfg.MaxOpenConnections = maxOpenConnections // If necessary adjust global WriteTimeout to ensure it's greater than // TimeoutBroadcastTxCommit. // See https://github.com/tendermint/tendermint/issues/3435 if cfg.WriteTimeout <= conf.RPC.TimeoutBroadcastTxCommit { cfg.WriteTimeout = conf.RPC.TimeoutBroadcastTxCommit + 1*time.Second } p, err := lproxy.NewProxy(c, listenAddr, primaryAddr, cfg, logger, lrpc.KeyPathFn(lrpc.DefaultMerkleKeyPathFn())) if err != nil { return err } ctx, cancel := signal.NotifyContext(cmd.Context(), syscall.SIGTERM) defer cancel() go func() { <-ctx.Done() p.Listener.Close() }() logger.Info("Starting proxy...", "laddr", listenAddr) if err := p.ListenAndServe(ctx); err != http.ErrServerClosed { // Error starting or closing listener: logger.Error("proxy ListenAndServe", "err", err) } return nil }, Args: cobra.ExactArgs(1), Example: `light cosmoshub-3 -p http://52.57.29.196:26657 -w http://public-seed-node.cosmoshub.certus.one:26657 --height 962118 --hash 28B97BE9F6DE51AC69F70E0B7BFD7E5C9CD1A595B7DC31AFF27C50D4948020CD`, } cmd.Flags().StringVar(&listenAddr, "laddr", "tcp://localhost:8888", "serve the proxy on the given address") cmd.Flags().StringVarP(&primaryAddr, "primary", "p", "", "connect to a Tendermint node at this address") cmd.Flags().StringVarP(&witnessAddrsJoined, "witnesses", "w", "", "tendermint nodes to cross-check the primary node, comma-separated") cmd.Flags().StringVarP(&dir, "dir", "d", os.ExpandEnv(filepath.Join("$HOME", ".tendermint-light")), "specify the directory") cmd.Flags().IntVar( &maxOpenConnections, "max-open-connections", 900, "maximum number of simultaneous connections (including WebSocket).") cmd.Flags().DurationVar(&trustingPeriod, "trusting-period", 168*time.Hour, "trusting period that headers can be verified within. Should be significantly less than the unbonding period") cmd.Flags().Int64Var(&trustedHeight, "height", 1, "Trusted header's height") cmd.Flags().BytesHexVar(&trustedHash, "hash", []byte{}, "Trusted header's hash") cmd.Flags().StringVar(&logLevel, "log-level", log.LogLevelInfo, "The logging level (debug|info|warn|error|fatal)") cmd.Flags().StringVar(&logFormat, "log-format", log.LogFormatPlain, "The logging format (text|json)") cmd.Flags().StringVar(&trustLevelStr, "trust-level", "1/3", "trust level. Must be between 1/3 and 3/3", ) cmd.Flags().BoolVar(&sequential, "sequential", false, "sequential verification. Verify all headers sequentially as opposed to using skipping verification", ) return cmd }