diff --git a/cmd/tendermint/commands/lite.go b/cmd/tendermint/commands/lite.go index 6caa7406b..10f013070 100644 --- a/cmd/tendermint/commands/lite.go +++ b/cmd/tendermint/commands/lite.go @@ -1,6 +1,7 @@ package commands import ( + "errors" "fmt" "net/http" "os" @@ -12,6 +13,7 @@ import ( dbm "github.com/tendermint/tm-db" "github.com/tendermint/tendermint/libs/log" + tmmath "github.com/tendermint/tendermint/libs/math" tmos "github.com/tendermint/tendermint/libs/os" "github.com/tendermint/tendermint/light" lproxy "github.com/tendermint/tendermint/light/proxy" @@ -31,16 +33,11 @@ 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. -Example: +Furthermore to the chainID, a fresh instance of a light client will +need a primary RPC address, a trusted hash and height and witness RPC addresses +(if not using sequential verification). To restart the node, thereafter +only the chainID is required. -start a fresh instance: - -light cosmoshub-3 -p http://52.57.29.196:26657 -w http://public-seed-node.cosmoshub.certus.one:26657 - --height 962118 --hash 28B97BE9F6DE51AC69F70E0B7BFD7E5C9CD1A595B7DC31AFF27C50D4948020CD - -continue from latest state: - -light cosmoshub-3 -p http://52.57.29.196:26657 -w http://public-seed-node.cosmoshub.certus.one:26657 `, RunE: runProxy, Args: cobra.ExactArgs(1), @@ -56,11 +53,16 @@ var ( home string maxOpenConnections int + sequential bool trustingPeriod time.Duration trustedHeight int64 trustedHash []byte + trustLevelStr string verbose bool + + primaryKey = []byte("primary") + witnessesKey = []byte("witnesses") ) func init() { @@ -77,10 +79,16 @@ func init() { 900, "Maximum number of simultaneous connections (including WebSocket).") LightCmd.Flags().DurationVar(&trustingPeriod, "trusting-period", 168*time.Hour, - "Trusting period. Should be significantly less than the unbonding period") + "Trusting period that headers can be verified within. Should be significantly less than the unbonding period") LightCmd.Flags().Int64Var(&trustedHeight, "height", 1, "Trusted header's height") LightCmd.Flags().BytesHexVar(&trustedHash, "hash", []byte{}, "Trusted header's hash") LightCmd.Flags().BoolVar(&verbose, "verbose", false, "Verbose output") + LightCmd.Flags().StringVar(&trustLevelStr, "trust-level", "1/3", + "Trust level. Must be between 1/3 and 3/3", + ) + LightCmd.Flags().BoolVar(&sequential, "sequential", false, + "Sequential Verification. Verify all headers sequentially as opposed to using skipping verification", + ) } func runProxy(cmd *cobra.Command, args []string) error { @@ -97,13 +105,46 @@ func runProxy(cmd *cobra.Command, args []string) error { chainID = args[0] logger.Info("Creating client...", "chainID", chainID) - witnessesAddrs := strings.Split(witnessAddrsJoined, ",") + witnessesAddrs := []string{} + if witnessAddrsJoined != "" { + witnessesAddrs = strings.Split(witnessAddrsJoined, ",") + } db, err := dbm.NewGoLevelDB("light-client-db", home) if err != nil { return fmt.Errorf("can't create a db: %w", err) } + 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) + } + } + + trustLevel, err := tmmath.ParseFraction(trustLevelStr) + if err != nil { + return fmt.Errorf("can't parse trust level: %w", err) + } + + options := []light.Option{light.Logger(logger)} + + if sequential { + options = append(options, light.SequentialVerification()) + } else { + options = append(options, light.SkippingVerification(trustLevel)) + } + var c *light.Client if trustedHeight > 0 && len(trustedHash) > 0 { // fresh installation c, err = light.NewHTTPClient( @@ -116,7 +157,7 @@ func runProxy(cmd *cobra.Command, args []string) error { primaryAddr, witnessesAddrs, dbs.New(db, chainID), - light.Logger(logger), + options..., ) } else { // continue from latest state c, err = light.NewHTTPClientFromTrustedStore( @@ -125,7 +166,7 @@ func runProxy(cmd *cobra.Command, args []string) error { primaryAddr, witnessesAddrs, dbs.New(db, chainID), - light.Logger(logger), + options..., ) } if err != nil { @@ -167,3 +208,28 @@ func runProxy(cmd *cobra.Command, args []string) error { return nil } + +func checkForExistingProviders(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 +} + +func saveProviders(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 +} diff --git a/libs/math/fraction.go b/libs/math/fraction.go index 6cc2ad23e..4883c0d3d 100644 --- a/libs/math/fraction.go +++ b/libs/math/fraction.go @@ -1,6 +1,11 @@ package math -import "fmt" +import ( + "errors" + "fmt" + "strconv" + "strings" +) // Fraction defined in terms of a numerator divided by a denominator in int64 // format. @@ -15,3 +20,23 @@ type Fraction struct { func (fr Fraction) String() string { return fmt.Sprintf("%d/%d", fr.Numerator, fr.Denominator) } + +// ParseFractions takes the string of a fraction as input i.e "2/3" and converts this +// to the equivalent fraction else returns an error. The format of the string must be +// one number followed by a slash (/) and then the other number. +func ParseFraction(f string) (Fraction, error) { + o := strings.SplitN(f, "/", -1) + if len(o) != 2 { + return Fraction{}, errors.New("incorrect formating: should be like \"1/3\"") + } + numerator, err := strconv.ParseInt(o[0], 10, 64) + if err != nil { + return Fraction{}, fmt.Errorf("incorrect formatting, err: %w", err) + } + + denominator, err := strconv.ParseInt(o[1], 10, 64) + if err != nil { + return Fraction{}, fmt.Errorf("incorrect formatting, err: %w", err) + } + return Fraction{Numerator: numerator, Denominator: denominator}, nil +} diff --git a/libs/math/fraction_test.go b/libs/math/fraction_test.go new file mode 100644 index 000000000..e4cabd32d --- /dev/null +++ b/libs/math/fraction_test.go @@ -0,0 +1,68 @@ +package math + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestParseFraction(t *testing.T) { + + testCases := []struct { + f string + exp Fraction + err bool + }{ + { + f: "2/3", + exp: Fraction{2, 3}, + err: false, + }, + { + f: "15/5", + exp: Fraction{15, 5}, + err: false, + }, + { + f: "-1/2", + exp: Fraction{-1, 2}, + err: false, + }, + { + f: "1/-2", + exp: Fraction{1, -2}, + err: false, + }, + { + f: "2/3/4", + exp: Fraction{}, + err: true, + }, + { + f: "123", + exp: Fraction{}, + err: true, + }, + { + f: "1a2/4", + exp: Fraction{}, + err: true, + }, + { + f: "1/3bc4", + exp: Fraction{}, + err: true, + }, + } + + for idx, tc := range testCases { + output, err := ParseFraction(tc.f) + if tc.err { + assert.Error(t, err, idx) + } else { + assert.NoError(t, err, idx) + } + assert.Equal(t, tc.exp, output, idx) + } + +}