diff --git a/cmd/tendermint/commands/lite.go b/cmd/tendermint/commands/lite.go index 52d253d77..f27c34cf1 100644 --- a/cmd/tendermint/commands/lite.go +++ b/cmd/tendermint/commands/lite.go @@ -2,6 +2,7 @@ package commands import ( "net/http" + "strings" "time" "github.com/pkg/errors" @@ -12,6 +13,7 @@ import ( tmos "github.com/tendermint/tendermint/libs/os" lite "github.com/tendermint/tendermint/lite2" + "github.com/tendermint/tendermint/lite2/provider" httpp "github.com/tendermint/tendermint/lite2/provider/http" lproxy "github.com/tendermint/tendermint/lite2/proxy" lrpc "github.com/tendermint/tendermint/lite2/rpc" @@ -36,9 +38,10 @@ just with added trust and running locally.`, var ( listenAddr string - nodeAddr string + primaryAddr string chainID string home string + witnessesAddrs string maxOpenConnections int trustingPeriod time.Duration @@ -47,9 +50,17 @@ var ( ) func init() { - LiteCmd.Flags().StringVar(&listenAddr, "laddr", "tcp://localhost:8888", "Serve the proxy on the given address") - LiteCmd.Flags().StringVar(&nodeAddr, "node", "tcp://localhost:26657", "Connect to a Tendermint node at this address") + LiteCmd.Flags().StringVar(&listenAddr, "laddr", "tcp://localhost:8888", + "Serve the proxy on the given address") + + LiteCmd.Flags().StringVar(&primaryAddr, "primary", "tcp://localhost:26657", + "Connect to a Tendermint node at this address") + + LiteCmd.Flags().StringVar(&witnessesAddrs, "witnesses", "", + "Tendermint nodes to cross-check the primary node, comma-separated") + LiteCmd.Flags().StringVar(&chainID, "chain-id", "tendermint", "Specify the Tendermint chain ID") + LiteCmd.Flags().StringVar(&home, "home-dir", ".tendermint-lite", "Specify the home directory") LiteCmd.Flags().IntVar( &maxOpenConnections, @@ -57,19 +68,33 @@ func init() { 900, "Maximum number of simultaneous connections (including WebSocket).") - LiteCmd.Flags().DurationVar(&trustingPeriod, "trusting-period", 168*time.Hour, "Trusting period. Should be significantly less than the unbonding period") + LiteCmd.Flags().DurationVar(&trustingPeriod, "trusting-period", 168*time.Hour, + "Trusting period. Should be significantly less than the unbonding period") + LiteCmd.Flags().Int64Var(&trustedHeight, "trusted-height", 1, "Trusted header's height") + LiteCmd.Flags().BytesHexVar(&trustedHash, "trusted-hash", []byte{}, "Trusted header's hash") } func runProxy(cmd *cobra.Command, args []string) error { liteLogger := logger.With("module", "lite") - logger.Info("Connecting to Tendermint node...") - // First, connect a client - node, err := rpcclient.NewHTTP(nodeAddr, "/websocket") + logger.Info("Connecting to the primary node...") + rpcClient, err := rpcclient.NewHTTP(chainID, primaryAddr) if err != nil { - return errors.Wrap(err, "new HTTP client") + return errors.Wrapf(err, "http client for %s", primaryAddr) + } + primary := httpp.NewWithClient(chainID, rpcClient) + + logger.Info("Connecting to the witness nodes...") + addrs := strings.Split(witnessesAddrs, ",") + witnesses := make([]provider.Provider, len(addrs)) + for i, addr := range addrs { + p, err := httpp.New(chainID, addr) + if err != nil { + return errors.Wrapf(err, "http provider for %s", addr) + } + witnesses[i] = p } logger.Info("Creating client...") @@ -84,7 +109,8 @@ func runProxy(cmd *cobra.Command, args []string) error { Height: trustedHeight, Hash: trustedHash, }, - httpp.NewWithClient(chainID, node), + primary, + witnesses, dbs.New(db, chainID), lite.Logger(liteLogger), ) @@ -96,7 +122,7 @@ func runProxy(cmd *cobra.Command, args []string) error { Addr: listenAddr, Config: &rpcserver.Config{MaxOpenConnections: maxOpenConnections}, Codec: amino.NewCodec(), - Client: lrpc.NewClient(node, c), + Client: lrpc.NewClient(rpcClient, c), Logger: liteLogger, } // Stop upon receiving SIGTERM or CTRL-C. diff --git a/lite2/client.go b/lite2/client.go index 4d49c9cac..4248d5eb8 100644 --- a/lite2/client.go +++ b/lite2/client.go @@ -75,29 +75,12 @@ func SequentialVerification() Option { // applies to non-adjacent headers. For adjacent headers, sequential // verification is used. func SkippingVerification(trustLevel tmmath.Fraction) Option { - if err := ValidateTrustLevel(trustLevel); err != nil { - panic(err) - } return func(c *Client) { c.verificationMode = skipping c.trustLevel = trustLevel } } -// Witnesses option can be used to supply providers, which will be used for -// cross-checking the primary provider. A witness can become a primary iff the -// current primary is unavailable. -func Witnesses(providers []provider.Provider) Option { - return func(c *Client) { - for _, witness := range providers { - if witness.ChainID() != c.ChainID() { - panic("Witness chainID is not equal to the Lite Client chainID") - } - } - c.witnesses = providers - } -} - // UpdatePeriod option can be used to change default polling period (5s). func UpdatePeriod(d time.Duration) Option { return func(c *Client) { @@ -175,11 +158,16 @@ type Client struct { // obtain the header & vals from the primary or they are invalid (e.g. trust // hash does not match with the one from the header). // +// Witnesses are providers, which will be used for cross-checking the primary +// provider. At least one witness must be given. A witness can become a primary +// iff the current primary is unavailable. +// // See all Option(s) for the additional configuration. func NewClient( chainID string, trustOptions TrustOptions, primary provider.Provider, + witnesses []provider.Provider, trustedStore store.Store, options ...Option) (*Client, error) { @@ -189,6 +177,7 @@ func NewClient( verificationMode: skipping, trustLevel: DefaultTrustLevel, primary: primary, + witnesses: witnesses, trustedStore: trustedStore, updatePeriod: defaultUpdatePeriod, removeNoLongerTrustedHeadersPeriod: defaultRemoveNoLongerTrustedHeadersPeriod, @@ -201,6 +190,24 @@ func NewClient( o(c) } + // Validate the number of witnesses. + if len(c.witnesses) < 1 { + return nil, errors.New("expected at least one witness") + } + + // Verify witnesses are all on the same chain. + for i, w := range witnesses { + if w.ChainID() != chainID { + return nil, errors.Errorf("witness #%d: %v is on another chain %s, expected %s", + i, w, w.ChainID(), chainID) + } + } + + // Validate trust level. + if err := ValidateTrustLevel(c.trustLevel); err != nil { + return nil, err + } + if err := c.restoreTrustedHeaderAndNextVals(); err != nil { return nil, err } @@ -523,11 +530,9 @@ func (c *Client) VerifyHeader(newHeader *types.SignedHeader, newVals *types.Vali return errors.Errorf("header at more recent height #%d exists", c.trustedHeader.Height) } - if len(c.witnesses) > 0 { - if err := c.compareNewHeaderWithRandomWitness(newHeader); err != nil { - c.logger.Error("Error when comparing new header with one from a witness", "err", err) - return err - } + if err := c.compareNewHeaderWithRandomWitness(newHeader); err != nil { + c.logger.Error("Error when comparing new header with one from a witness", "err", err) + return err } var err error @@ -733,6 +738,10 @@ func (c *Client) fetchHeaderAndValsAtHeight(height int64) (*types.SignedHeader, // compare header with one from a random witness. func (c *Client) compareNewHeaderWithRandomWitness(h *types.SignedHeader) error { + if len(c.witnesses) == 0 { + return errors.New("could not find any witnesses") + } + // 1. Pick a witness. witness := c.witnesses[tmrand.Intn(len(c.witnesses))] diff --git a/lite2/client_test.go b/lite2/client_test.go index d9f58347c..9ddab1ead 100644 --- a/lite2/client_test.go +++ b/lite2/client_test.go @@ -11,6 +11,7 @@ import ( dbm "github.com/tendermint/tm-db" "github.com/tendermint/tendermint/libs/log" + "github.com/tendermint/tendermint/lite2/provider" mockp "github.com/tendermint/tendermint/lite2/provider/mock" dbs "github.com/tendermint/tendermint/lite2/store/db" "github.com/tendermint/tendermint/types" @@ -127,6 +128,11 @@ func TestClient_SequentialVerification(t *testing.T) { tc.otherHeaders, tc.vals, ), + []provider.Provider{mockp.New( + chainID, + tc.otherHeaders, + tc.vals, + )}, dbs.New(dbm.NewMemDB(), chainID), SequentialVerification(), ) @@ -228,6 +234,11 @@ func TestClient_SkippingVerification(t *testing.T) { tc.otherHeaders, tc.vals, ), + []provider.Provider{mockp.New( + chainID, + tc.otherHeaders, + tc.vals, + )}, dbs.New(dbm.NewMemDB(), chainID), SkippingVerification(DefaultTrustLevel), ) @@ -264,6 +275,26 @@ func TestClientRemovesNoLongerTrustedHeaders(t *testing.T) { []byte("app_hash"), []byte("cons_hash"), []byte("results_hash"), 0, len(keys)) ) + primary := mockp.New( + chainID, + map[int64]*types.SignedHeader{ + // trusted header + 1: header, + // interim header (3/3 signed) + 2: keys.GenSignedHeader(chainID, 2, bTime.Add(2*time.Hour), nil, vals, vals, + []byte("app_hash"), []byte("cons_hash"), []byte("results_hash"), 0, len(keys)), + // last header (3/3 signed) + 3: keys.GenSignedHeader(chainID, 3, bTime.Add(4*time.Hour), nil, vals, vals, + []byte("app_hash"), []byte("cons_hash"), []byte("results_hash"), 0, len(keys)), + }, + map[int64]*types.ValidatorSet{ + 1: vals, + 2: vals, + 3: vals, + 4: vals, + }, + ) + c, err := NewClient( chainID, TrustOptions{ @@ -271,25 +302,8 @@ func TestClientRemovesNoLongerTrustedHeaders(t *testing.T) { Height: 1, Hash: header.Hash(), }, - mockp.New( - chainID, - map[int64]*types.SignedHeader{ - // trusted header - 1: header, - // interim header (3/3 signed) - 2: keys.GenSignedHeader(chainID, 2, bTime.Add(2*time.Hour), nil, vals, vals, - []byte("app_hash"), []byte("cons_hash"), []byte("results_hash"), 0, len(keys)), - // last header (3/3 signed) - 3: keys.GenSignedHeader(chainID, 3, bTime.Add(4*time.Hour), nil, vals, vals, - []byte("app_hash"), []byte("cons_hash"), []byte("results_hash"), 0, len(keys)), - }, - map[int64]*types.ValidatorSet{ - 1: vals, - 2: vals, - 3: vals, - 4: vals, - }, - ), + primary, + []provider.Provider{primary}, dbs.New(dbm.NewMemDB(), chainID), Logger(log.TestingLogger()), ) @@ -333,6 +347,18 @@ func TestClient_Cleanup(t *testing.T) { []byte("app_hash"), []byte("cons_hash"), []byte("results_hash"), 0, len(keys)) ) + primary := mockp.New( + chainID, + map[int64]*types.SignedHeader{ + // trusted header + 1: header, + }, + map[int64]*types.ValidatorSet{ + 1: vals, + 2: vals, + }, + ) + c, err := NewClient( chainID, TrustOptions{ @@ -340,17 +366,8 @@ func TestClient_Cleanup(t *testing.T) { Height: 1, Hash: header.Hash(), }, - mockp.New( - chainID, - map[int64]*types.SignedHeader{ - // trusted header - 1: header, - }, - map[int64]*types.ValidatorSet{ - 1: vals, - 2: vals, - }, - ), + primary, + []provider.Provider{primary}, dbs.New(dbm.NewMemDB(), chainID), Logger(log.TestingLogger()), ) @@ -388,6 +405,18 @@ func TestClientRestoreTrustedHeaderAfterStartup1(t *testing.T) { err := trustedStore.SaveSignedHeaderAndNextValidatorSet(header, vals) require.NoError(t, err) + primary := mockp.New( + chainID, + map[int64]*types.SignedHeader{ + // trusted header + 1: header, + }, + map[int64]*types.ValidatorSet{ + 1: vals, + 2: vals, + }, + ) + c, err := NewClient( chainID, TrustOptions{ @@ -395,17 +424,8 @@ func TestClientRestoreTrustedHeaderAfterStartup1(t *testing.T) { Height: 1, Hash: header.Hash(), }, - mockp.New( - chainID, - map[int64]*types.SignedHeader{ - // trusted header - 1: header, - }, - map[int64]*types.ValidatorSet{ - 1: vals, - 2: vals, - }, - ), + primary, + []provider.Provider{primary}, trustedStore, Logger(log.TestingLogger()), ) @@ -430,6 +450,18 @@ func TestClientRestoreTrustedHeaderAfterStartup1(t *testing.T) { header1 := keys.GenSignedHeader(chainID, 1, bTime.Add(1*time.Hour), nil, vals, vals, []byte("app_hash"), []byte("cons_hash"), []byte("results_hash"), 0, len(keys)) + primary := mockp.New( + chainID, + map[int64]*types.SignedHeader{ + // trusted header + 1: header1, + }, + map[int64]*types.ValidatorSet{ + 1: vals, + 2: vals, + }, + ) + c, err := NewClient( chainID, TrustOptions{ @@ -437,17 +469,8 @@ func TestClientRestoreTrustedHeaderAfterStartup1(t *testing.T) { Height: 1, Hash: header1.Hash(), }, - mockp.New( - chainID, - map[int64]*types.SignedHeader{ - // trusted header - 1: header1, - }, - map[int64]*types.ValidatorSet{ - 1: vals, - 2: vals, - }, - ), + primary, + []provider.Provider{primary}, trustedStore, Logger(log.TestingLogger()), ) @@ -487,6 +510,18 @@ func TestClientRestoreTrustedHeaderAfterStartup2(t *testing.T) { header2 := keys.GenSignedHeader(chainID, 2, bTime.Add(2*time.Hour), nil, vals, vals, []byte("app_hash"), []byte("cons_hash"), []byte("results_hash"), 0, len(keys)) + primary := mockp.New( + chainID, + map[int64]*types.SignedHeader{ + 1: header, + 2: header2, + }, + map[int64]*types.ValidatorSet{ + 1: vals, + 2: vals, + 3: vals, + }, + ) c, err := NewClient( chainID, TrustOptions{ @@ -494,18 +529,8 @@ func TestClientRestoreTrustedHeaderAfterStartup2(t *testing.T) { Height: 2, Hash: header2.Hash(), }, - mockp.New( - chainID, - map[int64]*types.SignedHeader{ - 1: header, - 2: header2, - }, - map[int64]*types.ValidatorSet{ - 1: vals, - 2: vals, - 3: vals, - }, - ), + primary, + []provider.Provider{primary}, trustedStore, Logger(log.TestingLogger()), ) @@ -535,6 +560,19 @@ func TestClientRestoreTrustedHeaderAfterStartup2(t *testing.T) { header2 := keys.GenSignedHeader(chainID, 2, bTime.Add(2*time.Hour), nil, vals, vals, []byte("app_hash"), []byte("cons_hash"), []byte("results_hash"), 0, len(keys)) + primary := mockp.New( + chainID, + map[int64]*types.SignedHeader{ + 1: header1, + 2: header2, + }, + map[int64]*types.ValidatorSet{ + 1: vals, + 2: vals, + 3: vals, + }, + ) + c, err := NewClient( chainID, TrustOptions{ @@ -542,18 +580,8 @@ func TestClientRestoreTrustedHeaderAfterStartup2(t *testing.T) { Height: 2, Hash: header2.Hash(), }, - mockp.New( - chainID, - map[int64]*types.SignedHeader{ - 1: header1, - 2: header2, - }, - map[int64]*types.ValidatorSet{ - 1: vals, - 2: vals, - 3: vals, - }, - ), + primary, + []provider.Provider{primary}, trustedStore, Logger(log.TestingLogger()), ) @@ -595,6 +623,19 @@ func TestClientRestoreTrustedHeaderAfterStartup3(t *testing.T) { err = trustedStore.SaveSignedHeaderAndNextValidatorSet(header2, vals) require.NoError(t, err) + primary := mockp.New( + chainID, + map[int64]*types.SignedHeader{ + 1: header, + 2: header2, + }, + map[int64]*types.ValidatorSet{ + 1: vals, + 2: vals, + 3: vals, + }, + ) + c, err := NewClient( chainID, TrustOptions{ @@ -602,18 +643,8 @@ func TestClientRestoreTrustedHeaderAfterStartup3(t *testing.T) { Height: 1, Hash: header.Hash(), }, - mockp.New( - chainID, - map[int64]*types.SignedHeader{ - 1: header, - 2: header2, - }, - map[int64]*types.ValidatorSet{ - 1: vals, - 2: vals, - 3: vals, - }, - ), + primary, + []provider.Provider{primary}, trustedStore, Logger(log.TestingLogger()), ) @@ -650,6 +681,17 @@ func TestClientRestoreTrustedHeaderAfterStartup3(t *testing.T) { err = trustedStore.SaveSignedHeaderAndNextValidatorSet(header2, vals) require.NoError(t, err) + primary := mockp.New( + chainID, + map[int64]*types.SignedHeader{ + 1: header1, + }, + map[int64]*types.ValidatorSet{ + 1: vals, + 2: vals, + }, + ) + c, err := NewClient( chainID, TrustOptions{ @@ -657,16 +699,8 @@ func TestClientRestoreTrustedHeaderAfterStartup3(t *testing.T) { Height: 1, Hash: header1.Hash(), }, - mockp.New( - chainID, - map[int64]*types.SignedHeader{ - 1: header1, - }, - map[int64]*types.ValidatorSet{ - 1: vals, - 2: vals, - }, - ), + primary, + []provider.Provider{primary}, trustedStore, Logger(log.TestingLogger()), ) @@ -702,6 +736,26 @@ func TestClient_Update(t *testing.T) { []byte("app_hash"), []byte("cons_hash"), []byte("results_hash"), 0, len(keys)) ) + primary := mockp.New( + chainID, + map[int64]*types.SignedHeader{ + // trusted header + 1: header, + // interim header (3/3 signed) + 2: keys.GenSignedHeader(chainID, 2, bTime.Add(30*time.Minute), nil, vals, vals, + []byte("app_hash"), []byte("cons_hash"), []byte("results_hash"), 0, len(keys)), + // last header (3/3 signed) + 3: keys.GenSignedHeader(chainID, 3, bTime.Add(1*time.Hour), nil, vals, vals, + []byte("app_hash"), []byte("cons_hash"), []byte("results_hash"), 0, len(keys)), + }, + map[int64]*types.ValidatorSet{ + 1: vals, + 2: vals, + 3: vals, + 4: vals, + }, + ) + c, err := NewClient( chainID, TrustOptions{ @@ -709,25 +763,8 @@ func TestClient_Update(t *testing.T) { Height: 1, Hash: header.Hash(), }, - mockp.New( - chainID, - map[int64]*types.SignedHeader{ - // trusted header - 1: header, - // interim header (3/3 signed) - 2: keys.GenSignedHeader(chainID, 2, bTime.Add(30*time.Minute), nil, vals, vals, - []byte("app_hash"), []byte("cons_hash"), []byte("results_hash"), 0, len(keys)), - // last header (3/3 signed) - 3: keys.GenSignedHeader(chainID, 3, bTime.Add(1*time.Hour), nil, vals, vals, - []byte("app_hash"), []byte("cons_hash"), []byte("results_hash"), 0, len(keys)), - }, - map[int64]*types.ValidatorSet{ - 1: vals, - 2: vals, - 3: vals, - 4: vals, - }, - ), + primary, + []provider.Provider{primary}, dbs.New(dbm.NewMemDB(), chainID), Logger(log.TestingLogger()), ) @@ -760,6 +797,26 @@ func TestClient_Concurrency(t *testing.T) { []byte("app_hash"), []byte("cons_hash"), []byte("results_hash"), 0, len(keys)) ) + primary := mockp.New( + chainID, + map[int64]*types.SignedHeader{ + // trusted header + 1: header, + // interim header (3/3 signed) + 2: keys.GenSignedHeader(chainID, 2, bTime.Add(30*time.Minute), nil, vals, vals, + []byte("app_hash"), []byte("cons_hash"), []byte("results_hash"), 0, len(keys)), + // last header (3/3 signed) + 3: keys.GenSignedHeader(chainID, 3, bTime.Add(1*time.Hour), nil, vals, vals, + []byte("app_hash"), []byte("cons_hash"), []byte("results_hash"), 0, len(keys)), + }, + map[int64]*types.ValidatorSet{ + 1: vals, + 2: vals, + 3: vals, + 4: vals, + }, + ) + c, err := NewClient( chainID, TrustOptions{ @@ -767,25 +824,8 @@ func TestClient_Concurrency(t *testing.T) { Height: 1, Hash: header.Hash(), }, - mockp.New( - chainID, - map[int64]*types.SignedHeader{ - // trusted header - 1: header, - // interim header (3/3 signed) - 2: keys.GenSignedHeader(chainID, 2, bTime.Add(30*time.Minute), nil, vals, vals, - []byte("app_hash"), []byte("cons_hash"), []byte("results_hash"), 0, len(keys)), - // last header (3/3 signed) - 3: keys.GenSignedHeader(chainID, 3, bTime.Add(1*time.Hour), nil, vals, vals, - []byte("app_hash"), []byte("cons_hash"), []byte("results_hash"), 0, len(keys)), - }, - map[int64]*types.ValidatorSet{ - 1: vals, - 2: vals, - 3: vals, - 4: vals, - }, - ), + primary, + []provider.Provider{primary}, dbs.New(dbm.NewMemDB(), chainID), UpdatePeriod(0), Logger(log.TestingLogger()), diff --git a/lite2/example_test.go b/lite2/example_test.go index 4a291236b..87e839d9b 100644 --- a/lite2/example_test.go +++ b/lite2/example_test.go @@ -12,6 +12,7 @@ import ( "github.com/tendermint/tendermint/abci/example/kvstore" "github.com/tendermint/tendermint/libs/log" + "github.com/tendermint/tendermint/lite2/provider" httpp "github.com/tendermint/tendermint/lite2/provider/http" dbs "github.com/tendermint/tendermint/lite2/store/db" rpctest "github.com/tendermint/tendermint/rpc/test" @@ -33,12 +34,12 @@ func TestExample_Client_AutoUpdate(t *testing.T) { chainID = config.ChainID() ) - provider, err := httpp.New(chainID, config.RPC.ListenAddress) + primary, err := httpp.New(chainID, config.RPC.ListenAddress) if err != nil { stdlog.Fatal(err) } - header, err := provider.SignedHeader(2) + header, err := primary.SignedHeader(2) if err != nil { stdlog.Fatal(err) } @@ -55,7 +56,8 @@ func TestExample_Client_AutoUpdate(t *testing.T) { Height: 2, Hash: header.Hash(), }, - provider, + primary, + []provider.Provider{primary}, // TODO: primary should not be used here dbs.New(db, chainID), UpdatePeriod(1*time.Second), Logger(log.TestingLogger()), @@ -99,12 +101,12 @@ func TestExample_Client_ManualUpdate(t *testing.T) { chainID = config.ChainID() ) - provider, err := httpp.New(chainID, config.RPC.ListenAddress) + primary, err := httpp.New(chainID, config.RPC.ListenAddress) if err != nil { stdlog.Fatal(err) } - header, err := provider.SignedHeader(2) + header, err := primary.SignedHeader(2) if err != nil { stdlog.Fatal(err) } @@ -121,7 +123,8 @@ func TestExample_Client_ManualUpdate(t *testing.T) { Height: 2, Hash: header.Hash(), }, - provider, + primary, + []provider.Provider{primary}, // TODO: primary should not be used here dbs.New(db, chainID), UpdatePeriod(0), Logger(log.TestingLogger()),