github.com/lazyledger/lazyledger-core@v0.35.0-dev.0.20210613111200-4c651f053571/cmd/tendermint/commands/light.go (about)

     1  package commands
     2  
     3  import (
     4  	"bufio"
     5  	"context"
     6  	"errors"
     7  	"fmt"
     8  	"io"
     9  	"net/http"
    10  	"os"
    11  	"path/filepath"
    12  	"regexp"
    13  	"strings"
    14  	"time"
    15  
    16  	coreiface "github.com/ipfs/interface-go-ipfs-core"
    17  	"github.com/spf13/cobra"
    18  
    19  	"github.com/lazyledger/lazyledger-core/crypto/merkle"
    20  	"github.com/lazyledger/lazyledger-core/ipfs"
    21  	dbm "github.com/lazyledger/lazyledger-core/libs/db"
    22  	"github.com/lazyledger/lazyledger-core/libs/db/badgerdb"
    23  	"github.com/lazyledger/lazyledger-core/libs/log"
    24  	tmmath "github.com/lazyledger/lazyledger-core/libs/math"
    25  	tmos "github.com/lazyledger/lazyledger-core/libs/os"
    26  	"github.com/lazyledger/lazyledger-core/light"
    27  	lproxy "github.com/lazyledger/lazyledger-core/light/proxy"
    28  	lrpc "github.com/lazyledger/lazyledger-core/light/rpc"
    29  	dbs "github.com/lazyledger/lazyledger-core/light/store/db"
    30  	rpchttp "github.com/lazyledger/lazyledger-core/rpc/client/http"
    31  	rpcserver "github.com/lazyledger/lazyledger-core/rpc/jsonrpc/server"
    32  )
    33  
    34  // LightCmd represents the base command when called without any subcommands
    35  var LightCmd = &cobra.Command{
    36  	Use:   "light [chainID]",
    37  	Short: "Run a light client proxy server, verifying Tendermint rpc",
    38  	Long: `Run a light client proxy server, verifying Tendermint rpc.
    39  
    40  All calls that can be tracked back to a block header by a proof
    41  will be verified before passing them back to the caller. Other than
    42  that, it will present the same interface as a full Tendermint node.
    43  
    44  Furthermore to the chainID, a fresh instance of a light client will
    45  need a primary RPC address, a trusted hash and height and witness RPC addresses
    46  (if not using sequential verification). To restart the node, thereafter
    47  only the chainID is required.
    48  
    49  When /abci_query is called, the Merkle key path format is:
    50  
    51  	/{store name}/{key}
    52  
    53  Please verify with your application that this Merkle key format is used (true
    54  for applications built w/ Cosmos SDK).
    55  `,
    56  	RunE: runProxy,
    57  	Args: cobra.ExactArgs(1),
    58  	Example: `light cosmoshub-3 -p http://52.57.29.196:26657 -w http://public-seed-node.cosmoshub.certus.one:26657
    59  	--height 962118 --hash 28B97BE9F6DE51AC69F70E0B7BFD7E5C9CD1A595B7DC31AFF27C50D4948020CD`,
    60  }
    61  
    62  var (
    63  	listenAddr         string
    64  	primaryAddr        string
    65  	witnessAddrsJoined string
    66  	chainID            string
    67  	dir                string
    68  	maxOpenConnections int
    69  
    70  	daSampling     bool
    71  	numSamples     uint32
    72  	sequential     bool
    73  	trustingPeriod time.Duration
    74  	trustedHeight  int64
    75  	trustedHash    []byte
    76  	trustLevelStr  string
    77  
    78  	verbose bool
    79  
    80  	primaryKey   = []byte("primary")
    81  	witnessesKey = []byte("witnesses")
    82  )
    83  
    84  func init() {
    85  	LightCmd.Flags().StringVar(&listenAddr, "laddr", "tcp://localhost:8888",
    86  		"serve the proxy on the given address")
    87  	LightCmd.Flags().StringVarP(&primaryAddr, "primary", "p", "",
    88  		"connect to a Tendermint node at this address")
    89  	LightCmd.Flags().StringVarP(&witnessAddrsJoined, "witnesses", "w", "",
    90  		"tendermint nodes to cross-check the primary node, comma-separated")
    91  	LightCmd.Flags().StringVarP(&dir, "dir", "d", os.ExpandEnv(filepath.Join("$HOME", ".tendermint-light")),
    92  		"specify the directory")
    93  	LightCmd.Flags().IntVar(
    94  		&maxOpenConnections,
    95  		"max-open-connections",
    96  		900,
    97  		"maximum number of simultaneous connections (including WebSocket).")
    98  	LightCmd.Flags().DurationVar(&trustingPeriod, "trusting-period", 168*time.Hour,
    99  		"trusting period that headers can be verified within. Should be significantly less than the unbonding period")
   100  	LightCmd.Flags().Int64Var(&trustedHeight, "height", 1, "Trusted header's height")
   101  	LightCmd.Flags().BytesHexVar(&trustedHash, "hash", []byte{}, "Trusted header's hash")
   102  	LightCmd.Flags().BoolVar(&verbose, "verbose", false, "Verbose output")
   103  	LightCmd.Flags().StringVar(&trustLevelStr, "trust-level", "1/3",
   104  		"trust level. Must be between 1/3 and 3/3",
   105  	)
   106  	LightCmd.Flags().BoolVar(&sequential, "sequential", false,
   107  		"sequential verification. Verify all headers sequentially as opposed to using skipping verification",
   108  	)
   109  	LightCmd.Flags().BoolVar(&daSampling, "da-sampling", false,
   110  		"data availability sampling. Verify each header's data availability via sampling",
   111  	)
   112  	LightCmd.Flags().Uint32Var(&numSamples, "num-samples", 15,
   113  		"Number of data availability samples until block data deemed available.")
   114  }
   115  
   116  func runProxy(cmd *cobra.Command, args []string) error {
   117  	// Initialise logger.
   118  	logger := log.NewTMLogger(log.NewSyncWriter(os.Stdout))
   119  	var option log.Option
   120  	if verbose {
   121  		option, _ = log.AllowLevel("debug")
   122  	} else {
   123  		option, _ = log.AllowLevel("info")
   124  	}
   125  	logger = log.NewFilter(logger, option)
   126  
   127  	chainID = args[0]
   128  	logger.Info("Creating client...", "chainID", chainID)
   129  
   130  	witnessesAddrs := []string{}
   131  	if witnessAddrsJoined != "" {
   132  		witnessesAddrs = strings.Split(witnessAddrsJoined, ",")
   133  	}
   134  
   135  	db, err := badgerdb.NewDB("light-client-db", dir)
   136  	if err != nil {
   137  		return fmt.Errorf("can't create a db: %w", err)
   138  	}
   139  
   140  	if primaryAddr == "" { // check to see if we can start from an existing state
   141  		var err error
   142  		primaryAddr, witnessesAddrs, err = checkForExistingProviders(db)
   143  		if err != nil {
   144  			return fmt.Errorf("failed to retrieve primary or witness from db: %w", err)
   145  		}
   146  		if primaryAddr == "" {
   147  			return errors.New("no primary address was provided nor found. Please provide a primary (using -p)." +
   148  				" Run the command: tendermint light --help for more information")
   149  		}
   150  	} else {
   151  		err := saveProviders(db, primaryAddr, witnessAddrsJoined)
   152  		if err != nil {
   153  			logger.Error("Unable to save primary and or witness addresses", "err", err)
   154  		}
   155  	}
   156  
   157  	trustLevel, err := tmmath.ParseFraction(trustLevelStr)
   158  	if err != nil {
   159  		return fmt.Errorf("can't parse trust level: %w", err)
   160  	}
   161  
   162  	options := []light.Option{
   163  		light.Logger(logger),
   164  		light.ConfirmationFunction(func(action string) bool {
   165  			fmt.Println(action)
   166  			scanner := bufio.NewScanner(os.Stdin)
   167  			for {
   168  				scanner.Scan()
   169  				response := scanner.Text()
   170  				switch response {
   171  				case "y", "Y":
   172  					return true
   173  				case "n", "N":
   174  					return false
   175  				default:
   176  					fmt.Println("please input 'Y' or 'n' and press ENTER")
   177  				}
   178  			}
   179  		}),
   180  	}
   181  
   182  	var ipfsCloser io.Closer
   183  	switch {
   184  	case daSampling:
   185  		cfg := ipfs.DefaultConfig()
   186  		cfg.RootDir = dir
   187  		cfg.ServeAPI = true
   188  		// TODO(ismail): share badger instance
   189  		apiProvider := ipfs.Embedded(true, cfg, logger)
   190  		var dag coreiface.APIDagService
   191  		dag, ipfsCloser, err = apiProvider()
   192  		if err != nil {
   193  			return fmt.Errorf("could not start ipfs API: %w", err)
   194  		}
   195  		options = append(options, light.DataAvailabilitySampling(numSamples, dag))
   196  	case sequential:
   197  		options = append(options, light.SequentialVerification())
   198  	default:
   199  		options = append(options, light.SkippingVerification(trustLevel))
   200  	}
   201  
   202  	var c *light.Client
   203  	if trustedHeight > 0 && len(trustedHash) > 0 { // fresh installation
   204  		c, err = light.NewHTTPClient(
   205  			context.Background(),
   206  			chainID,
   207  			light.TrustOptions{
   208  				Period: trustingPeriod,
   209  				Height: trustedHeight,
   210  				Hash:   trustedHash,
   211  			},
   212  			primaryAddr,
   213  			witnessesAddrs,
   214  			dbs.New(db, chainID),
   215  			options...,
   216  		)
   217  	} else { // continue from latest state
   218  		c, err = light.NewHTTPClientFromTrustedStore(
   219  			chainID,
   220  			trustingPeriod,
   221  			primaryAddr,
   222  			witnessesAddrs,
   223  			dbs.New(db, chainID),
   224  			options...,
   225  		)
   226  	}
   227  	if err != nil {
   228  		return err
   229  	}
   230  
   231  	rpcClient, err := rpchttp.New(primaryAddr, "/websocket")
   232  	if err != nil {
   233  		return fmt.Errorf("http client for %s: %w", primaryAddr, err)
   234  	}
   235  
   236  	cfg := rpcserver.DefaultConfig()
   237  	cfg.MaxBodyBytes = config.RPC.MaxBodyBytes
   238  	cfg.MaxHeaderBytes = config.RPC.MaxHeaderBytes
   239  	cfg.MaxOpenConnections = maxOpenConnections
   240  	// If necessary adjust global WriteTimeout to ensure it's greater than
   241  	// TimeoutBroadcastTxCommit.
   242  	// See https://github.com/tendermint/tendermint/issues/3435
   243  	if cfg.WriteTimeout <= config.RPC.TimeoutBroadcastTxCommit {
   244  		cfg.WriteTimeout = config.RPC.TimeoutBroadcastTxCommit + 1*time.Second
   245  	}
   246  
   247  	p := lproxy.Proxy{
   248  		Addr:   listenAddr,
   249  		Config: cfg,
   250  		Client: lrpc.NewClient(rpcClient, c, lrpc.KeyPathFn(defaultMerkleKeyPathFn())),
   251  		Logger: logger,
   252  	}
   253  	// Stop upon receiving SIGTERM or CTRL-C.
   254  	tmos.TrapSignal(logger, func() {
   255  		p.Listener.Close()
   256  		if ipfsCloser != nil {
   257  			ipfsCloser.Close()
   258  		}
   259  	})
   260  
   261  	logger.Info("Starting proxy...", "laddr", listenAddr)
   262  	if err := p.ListenAndServe(); err != http.ErrServerClosed {
   263  		// Error starting or closing listener:
   264  		logger.Error("proxy ListenAndServe", "err", err)
   265  	}
   266  
   267  	return nil
   268  }
   269  
   270  func checkForExistingProviders(db dbm.DB) (string, []string, error) {
   271  	primaryBytes, err := db.Get(primaryKey)
   272  	if err != nil {
   273  		return "", []string{""}, err
   274  	}
   275  	witnessesBytes, err := db.Get(witnessesKey)
   276  	if err != nil {
   277  		return "", []string{""}, err
   278  	}
   279  	witnessesAddrs := strings.Split(string(witnessesBytes), ",")
   280  	return string(primaryBytes), witnessesAddrs, nil
   281  }
   282  
   283  func saveProviders(db dbm.DB, primaryAddr, witnessesAddrs string) error {
   284  	err := db.Set(primaryKey, []byte(primaryAddr))
   285  	if err != nil {
   286  		return fmt.Errorf("failed to save primary provider: %w", err)
   287  	}
   288  	err = db.Set(witnessesKey, []byte(witnessesAddrs))
   289  	if err != nil {
   290  		return fmt.Errorf("failed to save witness providers: %w", err)
   291  	}
   292  	return nil
   293  }
   294  
   295  func defaultMerkleKeyPathFn() lrpc.KeyPathFunc {
   296  	// regexp for extracting store name from /abci_query path
   297  	storeNameRegexp := regexp.MustCompile(`\/store\/(.+)\/key`)
   298  
   299  	return func(path string, key []byte) (merkle.KeyPath, error) {
   300  		matches := storeNameRegexp.FindStringSubmatch(path)
   301  		if len(matches) != 2 {
   302  			return nil, fmt.Errorf("can't find store name in %s using %s", path, storeNameRegexp)
   303  		}
   304  		storeName := matches[1]
   305  
   306  		kp := merkle.KeyPath{}
   307  		kp = kp.AppendKey([]byte(storeName), merkle.KeyEncodingURL)
   308  		kp = kp.AppendKey(key, merkle.KeyEncodingURL)
   309  		return kp, nil
   310  	}
   311  }