You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

233 lines
7.5 KiB

  1. package commands
  2. import (
  3. "context"
  4. "errors"
  5. "fmt"
  6. "net/http"
  7. "os"
  8. "os/signal"
  9. "path/filepath"
  10. "strings"
  11. "syscall"
  12. "time"
  13. "github.com/spf13/cobra"
  14. dbm "github.com/tendermint/tm-db"
  15. "github.com/tendermint/tendermint/config"
  16. "github.com/tendermint/tendermint/libs/log"
  17. tmmath "github.com/tendermint/tendermint/libs/math"
  18. "github.com/tendermint/tendermint/light"
  19. lproxy "github.com/tendermint/tendermint/light/proxy"
  20. lrpc "github.com/tendermint/tendermint/light/rpc"
  21. dbs "github.com/tendermint/tendermint/light/store/db"
  22. rpcserver "github.com/tendermint/tendermint/rpc/jsonrpc/server"
  23. )
  24. // LightCmd constructs the base command called when invoked without any subcommands.
  25. func MakeLightCommand(conf *config.Config, logger log.Logger) *cobra.Command {
  26. var (
  27. listenAddr string
  28. primaryAddr string
  29. witnessAddrsJoined string
  30. chainID string
  31. dir string
  32. maxOpenConnections int
  33. sequential bool
  34. trustingPeriod time.Duration
  35. trustedHeight int64
  36. trustedHash []byte
  37. trustLevelStr string
  38. logLevel string
  39. logFormat string
  40. primaryKey = []byte("primary")
  41. witnessesKey = []byte("witnesses")
  42. )
  43. checkForExistingProviders := func(db dbm.DB) (string, []string, error) {
  44. primaryBytes, err := db.Get(primaryKey)
  45. if err != nil {
  46. return "", []string{""}, err
  47. }
  48. witnessesBytes, err := db.Get(witnessesKey)
  49. if err != nil {
  50. return "", []string{""}, err
  51. }
  52. witnessesAddrs := strings.Split(string(witnessesBytes), ",")
  53. return string(primaryBytes), witnessesAddrs, nil
  54. }
  55. saveProviders := func(db dbm.DB, primaryAddr, witnessesAddrs string) error {
  56. err := db.Set(primaryKey, []byte(primaryAddr))
  57. if err != nil {
  58. return fmt.Errorf("failed to save primary provider: %w", err)
  59. }
  60. err = db.Set(witnessesKey, []byte(witnessesAddrs))
  61. if err != nil {
  62. return fmt.Errorf("failed to save witness providers: %w", err)
  63. }
  64. return nil
  65. }
  66. cmd := &cobra.Command{
  67. Use: "light [chainID]",
  68. Short: "Run a light client proxy server, verifying Tendermint rpc",
  69. Long: `Run a light client proxy server, verifying Tendermint rpc.
  70. All calls that can be tracked back to a block header by a proof
  71. will be verified before passing them back to the caller. Other than
  72. that, it will present the same interface as a full Tendermint node.
  73. Furthermore to the chainID, a fresh instance of a light client will
  74. need a primary RPC address and a trusted hash and height. It is also highly
  75. recommended to provide additional witness RPC addresses, especially if
  76. not using sequential verification.
  77. To restart the node, thereafter only the chainID is required.
  78. When /abci_query is called, the Merkle key path format is:
  79. /{store name}/{key}
  80. Please verify with your application that this Merkle key format is used (true
  81. for applications built w/ Cosmos SDK).
  82. `,
  83. RunE: func(cmd *cobra.Command, args []string) error {
  84. chainID = args[0]
  85. logger.Info("Creating client...", "chainID", chainID)
  86. var witnessesAddrs []string
  87. if witnessAddrsJoined != "" {
  88. witnessesAddrs = strings.Split(witnessAddrsJoined, ",")
  89. }
  90. lightDB, err := dbm.NewGoLevelDB("light-client-db", dir)
  91. if err != nil {
  92. return fmt.Errorf("can't create a db: %w", err)
  93. }
  94. // create a prefixed db on the chainID
  95. db := dbm.NewPrefixDB(lightDB, []byte(chainID))
  96. if primaryAddr == "" { // check to see if we can start from an existing state
  97. var err error
  98. primaryAddr, witnessesAddrs, err = checkForExistingProviders(db)
  99. if err != nil {
  100. return fmt.Errorf("failed to retrieve primary or witness from db: %w", err)
  101. }
  102. if primaryAddr == "" {
  103. return errors.New("no primary address was provided nor found. Please provide a primary (using -p)." +
  104. " Run the command: tendermint light --help for more information")
  105. }
  106. } else {
  107. err := saveProviders(db, primaryAddr, witnessAddrsJoined)
  108. if err != nil {
  109. logger.Error("Unable to save primary and or witness addresses", "err", err)
  110. }
  111. }
  112. if len(witnessesAddrs) < 1 && !sequential {
  113. logger.Info("In skipping verification mode it is highly recommended to provide at least one witness")
  114. }
  115. trustLevel, err := tmmath.ParseFraction(trustLevelStr)
  116. if err != nil {
  117. return fmt.Errorf("can't parse trust level: %w", err)
  118. }
  119. options := []light.Option{light.Logger(logger)}
  120. vo := light.SkippingVerification(trustLevel)
  121. if sequential {
  122. vo = light.SequentialVerification()
  123. }
  124. options = append(options, vo)
  125. // Initiate the light client. If the trusted store already has blocks in it, this
  126. // will be used else we use the trusted options.
  127. c, err := light.NewHTTPClient(
  128. context.Background(),
  129. chainID,
  130. light.TrustOptions{
  131. Period: trustingPeriod,
  132. Height: trustedHeight,
  133. Hash: trustedHash,
  134. },
  135. primaryAddr,
  136. witnessesAddrs,
  137. dbs.New(db),
  138. options...,
  139. )
  140. if err != nil {
  141. return err
  142. }
  143. cfg := rpcserver.DefaultConfig()
  144. cfg.MaxBodyBytes = conf.RPC.MaxBodyBytes
  145. cfg.MaxHeaderBytes = conf.RPC.MaxHeaderBytes
  146. cfg.MaxOpenConnections = maxOpenConnections
  147. // If necessary adjust global WriteTimeout to ensure it's greater than
  148. // TimeoutBroadcastTxCommit.
  149. // See https://github.com/tendermint/tendermint/issues/3435
  150. if cfg.WriteTimeout <= conf.RPC.TimeoutBroadcastTxCommit {
  151. cfg.WriteTimeout = conf.RPC.TimeoutBroadcastTxCommit + 1*time.Second
  152. }
  153. p, err := lproxy.NewProxy(c, listenAddr, primaryAddr, cfg, logger, lrpc.KeyPathFn(lrpc.DefaultMerkleKeyPathFn()))
  154. if err != nil {
  155. return err
  156. }
  157. ctx, cancel := signal.NotifyContext(cmd.Context(), syscall.SIGTERM)
  158. defer cancel()
  159. go func() {
  160. <-ctx.Done()
  161. p.Listener.Close()
  162. }()
  163. logger.Info("Starting proxy...", "laddr", listenAddr)
  164. if err := p.ListenAndServe(ctx); err != http.ErrServerClosed {
  165. // Error starting or closing listener:
  166. logger.Error("proxy ListenAndServe", "err", err)
  167. }
  168. return nil
  169. },
  170. Args: cobra.ExactArgs(1),
  171. Example: `light cosmoshub-3 -p http://52.57.29.196:26657 -w http://public-seed-node.cosmoshub.certus.one:26657
  172. --height 962118 --hash 28B97BE9F6DE51AC69F70E0B7BFD7E5C9CD1A595B7DC31AFF27C50D4948020CD`,
  173. }
  174. cmd.Flags().StringVar(&listenAddr, "laddr", "tcp://localhost:8888",
  175. "serve the proxy on the given address")
  176. cmd.Flags().StringVarP(&primaryAddr, "primary", "p", "",
  177. "connect to a Tendermint node at this address")
  178. cmd.Flags().StringVarP(&witnessAddrsJoined, "witnesses", "w", "",
  179. "tendermint nodes to cross-check the primary node, comma-separated")
  180. cmd.Flags().StringVarP(&dir, "dir", "d", os.ExpandEnv(filepath.Join("$HOME", ".tendermint-light")),
  181. "specify the directory")
  182. cmd.Flags().IntVar(
  183. &maxOpenConnections,
  184. "max-open-connections",
  185. 900,
  186. "maximum number of simultaneous connections (including WebSocket).")
  187. cmd.Flags().DurationVar(&trustingPeriod, "trusting-period", 168*time.Hour,
  188. "trusting period that headers can be verified within. Should be significantly less than the unbonding period")
  189. cmd.Flags().Int64Var(&trustedHeight, "height", 1, "Trusted header's height")
  190. cmd.Flags().BytesHexVar(&trustedHash, "hash", []byte{}, "Trusted header's hash")
  191. cmd.Flags().StringVar(&logLevel, "log-level", log.LogLevelInfo, "The logging level (debug|info|warn|error|fatal)")
  192. cmd.Flags().StringVar(&logFormat, "log-format", log.LogFormatPlain, "The logging format (text|json)")
  193. cmd.Flags().StringVar(&trustLevelStr, "trust-level", "1/3",
  194. "trust level. Must be between 1/3 and 3/3",
  195. )
  196. cmd.Flags().BoolVar(&sequential, "sequential", false,
  197. "sequential verification. Verify all headers sequentially as opposed to using skipping verification",
  198. )
  199. return cmd
  200. }