From c9cfddfe3a2229242f4633895667dc54469254c7 Mon Sep 17 00:00:00 2001 From: Omri Date: Mon, 26 Dec 2022 12:10:49 +0200 Subject: [PATCH] Initialized the settlement client with the proposer upon initialization (#168) * Initialized the settlement client with the proposer upon initialization and refactored settlement client to be more testable. * Updated to go.sum * Removed the local repalcement in the go.mod. * Udpated dymension version. * Fixed data race in access to latestHeight. * Fixed p2p client flaky gossiping test. --- .gitignore | 1 + block/manager_test.go | 11 +- cmd/commands/init.go | 81 ----- cmd/commands/root.go | 97 ----- cmd/commands/start.go | 203 ----------- cmd/main.go | 28 -- go.mod | 16 +- go.sum | 26 +- mocks/cosmosclient.go | 189 ++++++++++ mocks/settlement/hubclient.go | 152 ++++++++ mocks/settlement/rollapp_query_client.go | 363 +++++++++++++++++++ mocks/settlement/sequencer_query_client.go | 243 +++++++++++++ p2p/client_test.go | 3 +- settlement/base.go | 208 +++++++++++ settlement/dymension.go | 355 ------------------ settlement/dymension/cosmosclient.go | 62 ++++ settlement/dymension/dymension.go | 401 +++++++++++++++++++++ settlement/dymension/dymension_test.go | 79 ++++ settlement/errors.go | 4 +- settlement/events.go | 8 + settlement/mock/mock.go | 340 ++++++++--------- settlement/registry/registry.go | 9 +- settlement/registry/registry_test.go | 2 +- settlement/settlement.go | 40 +- settlement/settlement_test.go | 171 +++++---- tests.mk | 2 +- types/sequencer.go | 21 ++ 27 files changed, 2058 insertions(+), 1057 deletions(-) delete mode 100644 cmd/commands/init.go delete mode 100644 cmd/commands/root.go delete mode 100644 cmd/commands/start.go delete mode 100644 cmd/main.go create mode 100644 mocks/cosmosclient.go create mode 100644 mocks/settlement/hubclient.go create mode 100644 mocks/settlement/rollapp_query_client.go create mode 100644 mocks/settlement/sequencer_query_client.go create mode 100644 settlement/base.go delete mode 100644 settlement/dymension.go create mode 100644 settlement/dymension/cosmosclient.go create mode 100644 settlement/dymension/dymension.go create mode 100644 settlement/dymension/dymension_test.go create mode 100644 types/sequencer.go diff --git a/.gitignore b/.gitignore index b302b6717..a44fa04dc 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ proto/pb .DS_Store .vscode .idea +build diff --git a/block/manager_test.go b/block/manager_test.go index 497bd9da8..550d1cf69 100644 --- a/block/manager_test.go +++ b/block/manager_test.go @@ -42,7 +42,7 @@ func TestInitialState(t *testing.T) { logger := log.TestingLogger() pubsubServer := pubsub.NewServer() pubsubServer.Start() - settlementlc := slregistry.GetClient(slregistry.ClientMock) + settlementlc := slregistry.GetClient(slregistry.Mock) _ = settlementlc.Init(nil, pubsubServer, logger) // Init empty store and full store @@ -259,7 +259,7 @@ func getManager(settlementlc settlement.LayerClient, dalc da.DataAvailabilityLay pubsubServer.Start() if settlementlc == nil { - settlementlc = slregistry.GetClient(slregistry.ClientMock) + settlementlc = slregistry.GetClient(slregistry.Mock) } _ = initSettlementLayerMock(settlementlc, defaultBatchSize, uint64(state.LastBlockHeight), uint64(state.LastBlockHeight)+1, pubsubServer, logger) @@ -297,10 +297,9 @@ func initDALCMock(dalc da.DataAvailabilityLayerClient, daBlockTime time.Duration // TODO(omritoptix): Possible move out to a generic testutil func initSettlementLayerMock(settlementlc settlement.LayerClient, batchSize uint64, latestHeight uint64, batchOffsetHeight uint64, pubsubServer *pubsub.Server, logger log.Logger) error { conf := slmock.Config{ - AutoUpdateBatches: false, - BatchSize: batchSize, - LatestHeight: latestHeight, - BatchOffsetHeight: batchOffsetHeight, + Config: &settlement.Config{ + BatchSize: batchSize, + }, } byteconf, _ := json.Marshal(conf) return settlementlc.Init(byteconf, pubsubServer, logger) diff --git a/cmd/commands/init.go b/cmd/commands/init.go deleted file mode 100644 index 350852bba..000000000 --- a/cmd/commands/init.go +++ /dev/null @@ -1,81 +0,0 @@ -package commands - -import ( - "fmt" - - "github.com/spf13/cobra" - - cfg "github.com/tendermint/tendermint/config" - tmos "github.com/tendermint/tendermint/libs/os" - tmrand "github.com/tendermint/tendermint/libs/rand" - "github.com/tendermint/tendermint/p2p" - "github.com/tendermint/tendermint/privval" - "github.com/tendermint/tendermint/types" - tmtime "github.com/tendermint/tendermint/types/time" -) - -// InitFilesCmd initialises a fresh Dymint Core instance. -var InitFilesCmd = &cobra.Command{ - Use: "init", - Short: "Initialize Dymint", - RunE: initFiles, -} - -func initFiles(cmd *cobra.Command, args []string) error { - return initFilesWithConfig(tmconfig) -} - -func initFilesWithConfig(config *cfg.Config) error { - // private validator - privValKeyFile := config.PrivValidatorKeyFile() - privValStateFile := config.PrivValidatorStateFile() - var pv *privval.FilePV - if tmos.FileExists(privValKeyFile) { - pv = privval.LoadFilePV(privValKeyFile, privValStateFile) - logger.Info("Found private validator", "keyFile", privValKeyFile, - "stateFile", privValStateFile) - } else { - pv = privval.GenFilePV(privValKeyFile, privValStateFile) - pv.Save() - logger.Info("Generated private validator", "keyFile", privValKeyFile, - "stateFile", privValStateFile) - } - - nodeKeyFile := config.NodeKeyFile() - if tmos.FileExists(nodeKeyFile) { - logger.Info("Found node key", "path", nodeKeyFile) - } else { - if _, err := p2p.LoadOrGenNodeKey(nodeKeyFile); err != nil { - return err - } - logger.Info("Generated node key", "path", nodeKeyFile) - } - - // genesis file - genFile := config.GenesisFile() - if tmos.FileExists(genFile) { - logger.Info("Found genesis file", "path", genFile) - } else { - genDoc := types.GenesisDoc{ - ChainID: fmt.Sprintf("test-chain-%v", tmrand.Str(6)), - GenesisTime: tmtime.Now(), - ConsensusParams: types.DefaultConsensusParams(), - } - pubKey, err := pv.GetPubKey() - if err != nil { - return fmt.Errorf("can't get pubkey: %w", err) - } - genDoc.Validators = []types.GenesisValidator{{ - Address: pubKey.Address(), - PubKey: pubKey, - Power: 10, - }} - - if err := genDoc.SaveAs(genFile); err != nil { - return err - } - logger.Info("Generated genesis file", "path", genFile) - } - - return nil -} diff --git a/cmd/commands/root.go b/cmd/commands/root.go deleted file mode 100644 index 3f9b427f5..000000000 --- a/cmd/commands/root.go +++ /dev/null @@ -1,97 +0,0 @@ -package commands - -import ( - "fmt" - "os" - - "github.com/spf13/cobra" - "github.com/spf13/viper" - - "github.com/dymensionxyz/dymint/config" - cfg "github.com/tendermint/tendermint/config" - "github.com/tendermint/tendermint/libs/cli" - tmflags "github.com/tendermint/tendermint/libs/cli/flags" - "github.com/tendermint/tendermint/libs/log" -) - -var ( - tmconfig = cfg.DefaultConfig() - dymconfig = config.DefaultNodeConfig - logger = log.NewTMLogger(log.NewSyncWriter(os.Stdout)) -) - -func init() { - registerFlagsRootCmd(RootCmd) -} - -func registerFlagsRootCmd(cmd *cobra.Command) { - cmd.PersistentFlags().String("log_level", tmconfig.LogLevel, "log level") -} - -// ParseConfig retrieves the default environment configuration, -// sets up the Dymint root and ensures that the root exists -func ParseConfig(cmd *cobra.Command) (*cfg.Config, error) { - conf := cfg.DefaultConfig() - err := viper.Unmarshal(conf) - if err != nil { - return nil, err - } - - var home string - if os.Getenv("DYMINTHOME") != "" { - home = os.Getenv("DYMINTHOME") - } else { - home, err = cmd.Flags().GetString(cli.HomeFlag) - if err != nil { - return nil, err - } - } - - conf.RootDir = home - - conf.SetRoot(conf.RootDir) - cfg.EnsureRoot(conf.RootDir) - if err := conf.ValidateBasic(); err != nil { - return nil, fmt.Errorf("error in config file: %v", err) - } - - return conf, nil -} - -// RootCmd is the root command for Dymint core. -var RootCmd = &cobra.Command{ - Use: "dymint", - Short: "ABCI-client implementation for dYmenion's autonomous rollapps", - PersistentPreRunE: func(cmd *cobra.Command, args []string) (err error) { - v := viper.GetViper() - err = v.BindPFlags(cmd.Flags()) - if err != nil { - return err - } - err = dymconfig.GetViperConfig(v) - if err != nil { - return err - } - - tmconfig, err = ParseConfig(cmd) - if err != nil { - return err - } - - if tmconfig.LogFormat == cfg.LogFormatJSON { - logger = log.NewTMJSONLogger(log.NewSyncWriter(os.Stdout)) - } - - logger, err = tmflags.ParseLogLevel(tmconfig.LogLevel, logger, cfg.DefaultLogLevel) - if err != nil { - return err - } - - if viper.GetBool(cli.TraceFlag) { - logger = log.NewTracingLogger(logger) - } - - logger = logger.With("module", "main") - return nil - }, -} diff --git a/cmd/commands/start.go b/cmd/commands/start.go deleted file mode 100644 index f8698d681..000000000 --- a/cmd/commands/start.go +++ /dev/null @@ -1,203 +0,0 @@ -package commands - -import ( - "bytes" - "context" - "crypto/sha256" - "fmt" - "io" - "os" - - cfg "github.com/dymensionxyz/dymint/config" - "github.com/dymensionxyz/dymint/conv" - "github.com/dymensionxyz/dymint/node" - "github.com/dymensionxyz/dymint/rpc" - "github.com/spf13/cobra" - tmcfg "github.com/tendermint/tendermint/config" - "github.com/tendermint/tendermint/libs/log" - tmos "github.com/tendermint/tendermint/libs/os" - tmnode "github.com/tendermint/tendermint/node" - tmp2p "github.com/tendermint/tendermint/p2p" - "github.com/tendermint/tendermint/proxy" -) - -var ( - genesisHash []byte -) - -// AddNodeFlags exposes some common configuration options on the command-line -// These are exposed for convenience of commands embedding a dymint node -func AddNodeFlags(cmd *cobra.Command) { - // bind flags - cmd.Flags().String("moniker", tmconfig.Moniker, "node name") - - // priv val flags - cmd.Flags().String( - "priv_validator_laddr", - tmconfig.PrivValidatorListenAddr, - "socket address to listen on for connections from external priv_validator process") - - // node flags - cmd.Flags().BytesHexVar( - &genesisHash, - "genesis_hash", - []byte{}, - "optional SHA-256 hash of the genesis file") - - // abci flags - cmd.Flags().String( - "proxy_app", - tmconfig.ProxyApp, - "proxy app address, or one of: 'kvstore',"+ - " 'persistent_kvstore' or 'noop' for local testing.") - cmd.Flags().String("abci", tmconfig.ABCI, "specify abci transport (socket | grpc)") - - // rpc flags - cmd.Flags().String("rpc.laddr", tmconfig.RPC.ListenAddress, "RPC listen address. Port required") - cmd.Flags().String( - "rpc.grpc_laddr", - tmconfig.RPC.GRPCListenAddress, - "GRPC listen address (BroadcastTx only). Port required") - cmd.Flags().Bool("rpc.unsafe", tmconfig.RPC.Unsafe, "enabled unsafe rpc methods") - cmd.Flags().String("rpc.pprof_laddr", tmconfig.RPC.PprofListenAddress, "pprof listen address (https://golang.org/pkg/net/http/pprof)") - - // p2p flags - cmd.Flags().String( - "p2p.laddr", - tmconfig.P2P.ListenAddress, - "node listen address. (0.0.0.0:0 means any interface, any port)") - cmd.Flags().String("p2p.external-address", tmconfig.P2P.ExternalAddress, "ip:port address to advertise to peers for them to dial") - cmd.Flags().String("p2p.seeds", tmconfig.P2P.Seeds, "comma-delimited ID@host:port seed nodes") - cmd.Flags().String("p2p.persistent_peers", tmconfig.P2P.PersistentPeers, "comma-delimited ID@host:port persistent peers") - cmd.Flags().String("p2p.unconditional_peer_ids", - tmconfig.P2P.UnconditionalPeerIDs, "comma-delimited IDs of unconditional peers") - cmd.Flags().Bool("p2p.upnp", tmconfig.P2P.UPNP, "enable/disable UPNP port forwarding") - cmd.Flags().Bool("p2p.pex", tmconfig.P2P.PexReactor, "enable/disable Peer-Exchange") - cmd.Flags().Bool("p2p.seed_mode", tmconfig.P2P.SeedMode, "enable/disable seed mode") - cmd.Flags().String("p2p.private_peer_ids", tmconfig.P2P.PrivatePeerIDs, "comma-delimited private peer IDs") - -} - -// NewRunNodeCmd returns the command that allows the CLI to start a node. -// It can be used with a custom PrivValidator and in-process ABCI application. -func NewRunNodeCmd() *cobra.Command { - cmd := &cobra.Command{ - Use: "start", - Aliases: []string{"node", "run"}, - Short: "Run the dymint node", - RunE: func(cmd *cobra.Command, args []string) error { - if err := checkGenesisHash(tmconfig); err != nil { - return err - } - logger := log.NewTMLogger(log.NewSyncWriter(os.Stdout)) - err := startInProcess(&dymconfig, tmconfig, logger) - if err != nil { - return err - } - return nil - }, - } - - cfg.AddFlags(cmd) - AddNodeFlags(cmd) - return cmd -} - -func startInProcess(config *cfg.NodeConfig, tmConfig *tmcfg.Config, logger log.Logger) error { - nodeKey, err := tmp2p.LoadOrGenNodeKey(tmConfig.NodeKeyFile()) - if err != nil { - return fmt.Errorf("failed to load or gen node key %s: %w", tmConfig.NodeKeyFile(), err) - } - privValKey, err := tmp2p.LoadOrGenNodeKey(tmConfig.PrivValidatorKeyFile()) - if err != nil { - return err - } - genDocProvider := tmnode.DefaultGenesisDocProviderFunc(tmConfig) - logger.Info("starting node with ABCI dymint in-process") - p2pKey, err := conv.GetNodeKey(nodeKey) - if err != nil { - return err - } - signingKey, err := conv.GetNodeKey(privValKey) - if err != nil { - return err - } - genesis, err := genDocProvider() - if err != nil { - return err - } - conv.GetNodeConfig(config, tmConfig) - err = conv.TranslateAddresses(config) - if err != nil { - return err - } - - dymintNode, err := node.NewNode( - context.Background(), - *config, - p2pKey, - signingKey, - proxy.DefaultClientCreator(tmConfig.ProxyApp, tmConfig.ABCI, tmConfig.DBDir()), - genesis, - logger, - ) - if err != nil { - return err - } - - server := rpc.NewServer(dymintNode, tmConfig.RPC, logger) - err = server.Start() - if err != nil { - return err - } - - logger.Debug("initialization: dymint node created") - if err := dymintNode.Start(); err != nil { - return err - } - - logger.Info("Started dymint node") - - // Stop upon receiving SIGTERM or CTRL-C. - tmos.TrapSignal(logger, func() { - if dymintNode.IsRunning() { - if err := dymintNode.Stop(); err != nil { - logger.Error("unable to stop the node", "error", err) - } - } - }) - - // Run forever. - select {} -} - -func checkGenesisHash(config *tmcfg.Config) error { - if len(genesisHash) == 0 || config.Genesis == "" { - return nil - } - - // Calculate SHA-256 hash of the genesis file. - f, err := os.Open(config.GenesisFile()) - if err != nil { - return fmt.Errorf("can't open genesis file: %w", err) - } - defer func() { - if tempErr := f.Close(); tempErr != nil { - err = tempErr - } - }() - h := sha256.New() - if _, err := io.Copy(h, f); err != nil { - return fmt.Errorf("error when hashing genesis file: %w", err) - } - actualHash := h.Sum(nil) - - // Compare with the flag. - if !bytes.Equal(genesisHash, actualHash) { - return fmt.Errorf( - "--genesis_hash=%X does not match %s hash: %X", - genesisHash, config.GenesisFile(), actualHash) - } - - return nil -} diff --git a/cmd/main.go b/cmd/main.go deleted file mode 100644 index ff2992164..000000000 --- a/cmd/main.go +++ /dev/null @@ -1,28 +0,0 @@ -package main - -import ( - "os" - "path/filepath" - - "github.com/dymensionxyz/dymint/cmd/commands" - "github.com/dymensionxyz/dymint/config" - "github.com/tendermint/tendermint/cmd/tendermint/commands/debug" - "github.com/tendermint/tendermint/libs/cli" -) - -func main() { - rootCmd := commands.RootCmd - rootCmd.AddCommand( - commands.InitFilesCmd, - debug.DebugCmd, - cli.NewCompletionCmd(rootCmd, true), - ) - - // Create & start node - rootCmd.AddCommand(commands.NewRunNodeCmd()) - - cmd := cli.PrepareBaseCmd(rootCmd, "DM", os.ExpandEnv(filepath.Join("$HOME", config.DefaultDymintDir))) - if err := cmd.Execute(); err != nil { - panic(err) - } -} diff --git a/go.mod b/go.mod index fefc5c45b..394219af0 100644 --- a/go.mod +++ b/go.mod @@ -29,9 +29,9 @@ require ( github.com/stretchr/testify v1.8.1 github.com/tendermint/tendermint v0.34.21 go.uber.org/multierr v1.8.0 - golang.org/x/net v0.2.0 + golang.org/x/net v0.3.0 gonum.org/v1/gonum v0.12.0 - google.golang.org/grpc v1.50.1 + google.golang.org/grpc v1.51.0 google.golang.org/protobuf v1.28.1 ) @@ -77,7 +77,7 @@ require ( github.com/docker/go-units v0.4.0 // indirect github.com/dustin/go-humanize v1.0.1-0.20200219035652-afde56e7acac // indirect github.com/dvsekhvalnov/jose2go v1.5.0 // indirect - github.com/dymensionxyz/dymension v0.1.0-alpha.0.20221222094743-771d37d941db + github.com/dymensionxyz/dymension v0.1.0-alpha.0.20221225171439-68887865cf2f github.com/elastic/gosigar v0.14.2 // indirect github.com/flynn/noise v1.0.0 // indirect github.com/francoispqt/gojay v1.2.13 // indirect @@ -219,10 +219,10 @@ require ( golang.org/x/crypto v0.0.0-20220924013350-4ba4fb4dd9e7 // indirect golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 // indirect - golang.org/x/sys v0.2.0 // indirect - golang.org/x/term v0.2.0 // indirect - golang.org/x/text v0.4.0 // indirect - google.golang.org/genproto v0.0.0-20221114212237-e4508ebdbee1 // indirect + golang.org/x/sys v0.3.0 // indirect + golang.org/x/term v0.3.0 // indirect + golang.org/x/text v0.5.0 // indirect + google.golang.org/genproto v0.0.0-20221207170731-23e4bf6bdc37 // indirect gopkg.in/ini.v1 v1.66.6 // indirect gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect @@ -232,7 +232,7 @@ require ( require ( github.com/cenkalti/backoff v2.2.1+incompatible // indirect - github.com/cosmos/cosmos-sdk v0.45.5 // indirect + github.com/cosmos/cosmos-sdk v0.45.5 github.com/cosmos/ibc-go/v3 v3.0.1 // indirect github.com/dgraph-io/ristretto v0.1.1 // indirect github.com/golang/glog v1.0.0 // indirect diff --git a/go.sum b/go.sum index bee135d41..0a15bb1fb 100644 --- a/go.sum +++ b/go.sum @@ -277,8 +277,10 @@ github.com/dvsekhvalnov/jose2go v1.5.0 h1:3j8ya4Z4kMCwT5nXIKFSV84YS+HdqSSO0VsTQx github.com/dvsekhvalnov/jose2go v1.5.0/go.mod h1:QsHjhyTlD/lAVqn/NSbVZmSCGeDehTB/mPZadG+mhXU= github.com/dymensionxyz/cosmosclient v0.2.0-alpha h1:x8UCXtWyZc7TXw99VH83ld2YD21d5m4fJx5k8zbdFj4= github.com/dymensionxyz/cosmosclient v0.2.0-alpha/go.mod h1:Fxwqa+T2fG+Q1cK5niy+WE9L1ul1nf1fF6gi3nwCqVE= -github.com/dymensionxyz/dymension v0.1.0-alpha.0.20221222094743-771d37d941db h1:xuLQHpXwBI0sZKmYiZM1ZBWHKOsDHSJd2SvEgNJHOuU= -github.com/dymensionxyz/dymension v0.1.0-alpha.0.20221222094743-771d37d941db/go.mod h1:3ufLzwMgEhOeQN8O/lSXOjHft/06FofFAEVJoRW9QIE= +github.com/dymensionxyz/dymension v0.1.0-alpha.0.20221225142534-0e73a495b739 h1:x6dXyuofax+FAxWuYuikOMpCTU3VK4A7qM63EO43Xrs= +github.com/dymensionxyz/dymension v0.1.0-alpha.0.20221225142534-0e73a495b739/go.mod h1:Ss0oibs7QoFWlQjbb8R/HwKVpzh7AHI+0y3TShrf35g= +github.com/dymensionxyz/dymension v0.1.0-alpha.0.20221225171439-68887865cf2f h1:3kLjKxgH3a/Yz9zVzNoyvng4+hEViOwsPgftpXajV28= +github.com/dymensionxyz/dymension v0.1.0-alpha.0.20221225171439-68887865cf2f/go.mod h1:Ss0oibs7QoFWlQjbb8R/HwKVpzh7AHI+0y3TShrf35g= github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= @@ -1545,8 +1547,8 @@ golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220517181318-183a9ca12b87/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.2.0 h1:sZfSu1wtKLGlWI4ZZayP0ck9Y73K1ynO6gqzTdBVdPU= -golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= +golang.org/x/net v0.3.0 h1:VWL6FNY2bEEmsGVKabSlHu5Irp34xmMRoqb/9lF9lxk= +golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -1684,13 +1686,13 @@ golang.org/x/sys v0.0.0-20220517195934-5e4e11fc645e/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A= -golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ= +golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.2.0 h1:z85xZCsEl7bi/KwbNADeBYoOP0++7W1ipu+aGnpwzRM= -golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= +golang.org/x/term v0.3.0 h1:qoo4akIqOcDME5bhc/NgxUdovd6BSS2uMsVjB56q1xI= +golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1700,8 +1702,8 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg= -golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM= +golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -1872,8 +1874,8 @@ google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaE google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= google.golang.org/genproto v0.0.0-20210917145530-b395a37504d4/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= google.golang.org/genproto v0.0.0-20211101144312-62acf1d99145/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20221114212237-e4508ebdbee1 h1:jCw9YRd2s40X9Vxi4zKsPRvSPlHWNqadVkpbMsCPzPQ= -google.golang.org/genproto v0.0.0-20221114212237-e4508ebdbee1/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= +google.golang.org/genproto v0.0.0-20221207170731-23e4bf6bdc37 h1:jmIfw8+gSvXcZSgaFAGyInDXeWzUhvYH57G/5GKMn70= +google.golang.org/genproto v0.0.0-20221207170731-23e4bf6bdc37/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= google.golang.org/grpc v1.33.2 h1:EQyQC3sa8M+p6Ulc8yy9SWSS2GVwyRc83gAbG8lrl4o= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= diff --git a/mocks/cosmosclient.go b/mocks/cosmosclient.go new file mode 100644 index 000000000..c1c485e43 --- /dev/null +++ b/mocks/cosmosclient.go @@ -0,0 +1,189 @@ +// Code generated by mockery v2.15.0. DO NOT EDIT. + +package mocks + +import ( + context "context" + + client "github.com/cosmos/cosmos-sdk/client" + + coretypes "github.com/tendermint/tendermint/rpc/core/types" + + cosmosclient "github.com/dymensionxyz/cosmosclient/cosmosclient" + + mock "github.com/stretchr/testify/mock" + + rollapptypes "github.com/dymensionxyz/dymension/x/rollapp/types" + + sequencertypes "github.com/dymensionxyz/dymension/x/sequencer/types" + + types "github.com/cosmos/cosmos-sdk/types" +) + +// CosmosClient is an autogenerated mock type for the CosmosClient type +type CosmosClient struct { + mock.Mock +} + +// BroadcastTx provides a mock function with given fields: accountName, msgs +func (_m *CosmosClient) BroadcastTx(accountName string, msgs ...types.Msg) (cosmosclient.Response, error) { + _va := make([]interface{}, len(msgs)) + for _i := range msgs { + _va[_i] = msgs[_i] + } + var _ca []interface{} + _ca = append(_ca, accountName) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 cosmosclient.Response + if rf, ok := ret.Get(0).(func(string, ...types.Msg) cosmosclient.Response); ok { + r0 = rf(accountName, msgs...) + } else { + r0 = ret.Get(0).(cosmosclient.Response) + } + + var r1 error + if rf, ok := ret.Get(1).(func(string, ...types.Msg) error); ok { + r1 = rf(accountName, msgs...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Context provides a mock function with given fields: +func (_m *CosmosClient) Context() client.Context { + ret := _m.Called() + + var r0 client.Context + if rf, ok := ret.Get(0).(func() client.Context); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(client.Context) + } + + return r0 +} + +// EventListenerQuit provides a mock function with given fields: +func (_m *CosmosClient) EventListenerQuit() <-chan struct{} { + ret := _m.Called() + + var r0 <-chan struct{} + if rf, ok := ret.Get(0).(func() <-chan struct{}); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(<-chan struct{}) + } + } + + return r0 +} + +// GetRollappClient provides a mock function with given fields: +func (_m *CosmosClient) GetRollappClient() rollapptypes.QueryClient { + ret := _m.Called() + + var r0 rollapptypes.QueryClient + if rf, ok := ret.Get(0).(func() rollapptypes.QueryClient); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(rollapptypes.QueryClient) + } + } + + return r0 +} + +// GetSequencerClient provides a mock function with given fields: +func (_m *CosmosClient) GetSequencerClient() sequencertypes.QueryClient { + ret := _m.Called() + + var r0 sequencertypes.QueryClient + if rf, ok := ret.Get(0).(func() sequencertypes.QueryClient); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(sequencertypes.QueryClient) + } + } + + return r0 +} + +// StartEventListener provides a mock function with given fields: +func (_m *CosmosClient) StartEventListener() error { + ret := _m.Called() + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// StopEventListener provides a mock function with given fields: +func (_m *CosmosClient) StopEventListener() error { + ret := _m.Called() + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// SubscribeToEvents provides a mock function with given fields: ctx, subscriber, query, outCapacity +func (_m *CosmosClient) SubscribeToEvents(ctx context.Context, subscriber string, query string, outCapacity ...int) (<-chan coretypes.ResultEvent, error) { + _va := make([]interface{}, len(outCapacity)) + for _i := range outCapacity { + _va[_i] = outCapacity[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, subscriber, query) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 <-chan coretypes.ResultEvent + if rf, ok := ret.Get(0).(func(context.Context, string, string, ...int) <-chan coretypes.ResultEvent); ok { + r0 = rf(ctx, subscriber, query, outCapacity...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(<-chan coretypes.ResultEvent) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, string, string, ...int) error); ok { + r1 = rf(ctx, subscriber, query, outCapacity...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +type mockConstructorTestingTNewCosmosClient interface { + mock.TestingT + Cleanup(func()) +} + +// NewCosmosClient creates a new instance of CosmosClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewCosmosClient(t mockConstructorTestingTNewCosmosClient) *CosmosClient { + mock := &CosmosClient{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/mocks/settlement/hubclient.go b/mocks/settlement/hubclient.go new file mode 100644 index 000000000..f356d2ae8 --- /dev/null +++ b/mocks/settlement/hubclient.go @@ -0,0 +1,152 @@ +// Code generated by mockery v2.15.0. DO NOT EDIT. + +package mocks + +import ( + da "github.com/dymensionxyz/dymint/da" + mock "github.com/stretchr/testify/mock" + + settlement "github.com/dymensionxyz/dymint/settlement" + + types "github.com/dymensionxyz/dymint/types" +) + +// HubClient is an autogenerated mock type for the HubClient type +type HubClient struct { + mock.Mock +} + +// GetBatchAtIndex provides a mock function with given fields: rollappID, index +func (_m *HubClient) GetBatchAtIndex(rollappID string, index uint64) (*settlement.ResultRetrieveBatch, error) { + ret := _m.Called(rollappID, index) + + var r0 *settlement.ResultRetrieveBatch + if rf, ok := ret.Get(0).(func(string, uint64) *settlement.ResultRetrieveBatch); ok { + r0 = rf(rollappID, index) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*settlement.ResultRetrieveBatch) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(string, uint64) error); ok { + r1 = rf(rollappID, index) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetLatestBatch provides a mock function with given fields: rollappID +func (_m *HubClient) GetLatestBatch(rollappID string) (*settlement.ResultRetrieveBatch, error) { + ret := _m.Called(rollappID) + + var r0 *settlement.ResultRetrieveBatch + if rf, ok := ret.Get(0).(func(string) *settlement.ResultRetrieveBatch); ok { + r0 = rf(rollappID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*settlement.ResultRetrieveBatch) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(string) error); ok { + r1 = rf(rollappID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetSequencers provides a mock function with given fields: rollappID +func (_m *HubClient) GetSequencers(rollappID string) ([]*types.Sequencer, error) { + ret := _m.Called(rollappID) + + var r0 []*types.Sequencer + if rf, ok := ret.Get(0).(func(string) []*types.Sequencer); ok { + r0 = rf(rollappID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*types.Sequencer) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(string) error); ok { + r1 = rf(rollappID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// PostBatch provides a mock function with given fields: batch, daClient, daResult +func (_m *HubClient) PostBatch(batch *types.Batch, daClient da.Client, daResult *da.ResultSubmitBatch) (settlement.PostBatchResp, error) { + ret := _m.Called(batch, daClient, daResult) + + var r0 settlement.PostBatchResp + if rf, ok := ret.Get(0).(func(*types.Batch, da.Client, *da.ResultSubmitBatch) settlement.PostBatchResp); ok { + r0 = rf(batch, daClient, daResult) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(settlement.PostBatchResp) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(*types.Batch, da.Client, *da.ResultSubmitBatch) error); ok { + r1 = rf(batch, daClient, daResult) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Start provides a mock function with given fields: +func (_m *HubClient) Start() error { + ret := _m.Called() + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Stop provides a mock function with given fields: +func (_m *HubClient) Stop() error { + ret := _m.Called() + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +type mockConstructorTestingTNewHubClient interface { + mock.TestingT + Cleanup(func()) +} + +// NewHubClient creates a new instance of HubClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewHubClient(t mockConstructorTestingTNewHubClient) *HubClient { + mock := &HubClient{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/mocks/settlement/rollapp_query_client.go b/mocks/settlement/rollapp_query_client.go new file mode 100644 index 000000000..579030268 --- /dev/null +++ b/mocks/settlement/rollapp_query_client.go @@ -0,0 +1,363 @@ +// Code generated by mockery v2.15.0. DO NOT EDIT. + +package mocks + +import ( + context "context" + + grpc "google.golang.org/grpc" + + mock "github.com/stretchr/testify/mock" + + types "github.com/dymensionxyz/dymension/x/rollapp/types" +) + +// RollAppQueryClient is an autogenerated mock type for the QueryClient type +type RollAppQueryClient struct { + mock.Mock +} + +// BlockHeightToFinalizationQueue provides a mock function with given fields: ctx, in, opts +func (_m *RollAppQueryClient) BlockHeightToFinalizationQueue(ctx context.Context, in *types.QueryGetBlockHeightToFinalizationQueueRequest, opts ...grpc.CallOption) (*types.QueryGetBlockHeightToFinalizationQueueResponse, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, in) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 *types.QueryGetBlockHeightToFinalizationQueueResponse + if rf, ok := ret.Get(0).(func(context.Context, *types.QueryGetBlockHeightToFinalizationQueueRequest, ...grpc.CallOption) *types.QueryGetBlockHeightToFinalizationQueueResponse); ok { + r0 = rf(ctx, in, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*types.QueryGetBlockHeightToFinalizationQueueResponse) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *types.QueryGetBlockHeightToFinalizationQueueRequest, ...grpc.CallOption) error); ok { + r1 = rf(ctx, in, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// BlockHeightToFinalizationQueueAll provides a mock function with given fields: ctx, in, opts +func (_m *RollAppQueryClient) BlockHeightToFinalizationQueueAll(ctx context.Context, in *types.QueryAllBlockHeightToFinalizationQueueRequest, opts ...grpc.CallOption) (*types.QueryAllBlockHeightToFinalizationQueueResponse, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, in) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 *types.QueryAllBlockHeightToFinalizationQueueResponse + if rf, ok := ret.Get(0).(func(context.Context, *types.QueryAllBlockHeightToFinalizationQueueRequest, ...grpc.CallOption) *types.QueryAllBlockHeightToFinalizationQueueResponse); ok { + r0 = rf(ctx, in, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*types.QueryAllBlockHeightToFinalizationQueueResponse) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *types.QueryAllBlockHeightToFinalizationQueueRequest, ...grpc.CallOption) error); ok { + r1 = rf(ctx, in, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetStateInfoByHeight provides a mock function with given fields: ctx, in, opts +func (_m *RollAppQueryClient) GetStateInfoByHeight(ctx context.Context, in *types.QueryGetStateInfoByHeightRequest, opts ...grpc.CallOption) (*types.QueryGetStateInfoByHeightResponse, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, in) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 *types.QueryGetStateInfoByHeightResponse + if rf, ok := ret.Get(0).(func(context.Context, *types.QueryGetStateInfoByHeightRequest, ...grpc.CallOption) *types.QueryGetStateInfoByHeightResponse); ok { + r0 = rf(ctx, in, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*types.QueryGetStateInfoByHeightResponse) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *types.QueryGetStateInfoByHeightRequest, ...grpc.CallOption) error); ok { + r1 = rf(ctx, in, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// LatestFinalizedStateInfo provides a mock function with given fields: ctx, in, opts +func (_m *RollAppQueryClient) LatestFinalizedStateInfo(ctx context.Context, in *types.QueryGetLatestFinalizedStateInfoRequest, opts ...grpc.CallOption) (*types.QueryGetLatestFinalizedStateInfoResponse, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, in) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 *types.QueryGetLatestFinalizedStateInfoResponse + if rf, ok := ret.Get(0).(func(context.Context, *types.QueryGetLatestFinalizedStateInfoRequest, ...grpc.CallOption) *types.QueryGetLatestFinalizedStateInfoResponse); ok { + r0 = rf(ctx, in, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*types.QueryGetLatestFinalizedStateInfoResponse) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *types.QueryGetLatestFinalizedStateInfoRequest, ...grpc.CallOption) error); ok { + r1 = rf(ctx, in, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// LatestStateInfoIndex provides a mock function with given fields: ctx, in, opts +func (_m *RollAppQueryClient) LatestStateInfoIndex(ctx context.Context, in *types.QueryGetLatestStateInfoIndexRequest, opts ...grpc.CallOption) (*types.QueryGetLatestStateInfoIndexResponse, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, in) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 *types.QueryGetLatestStateInfoIndexResponse + if rf, ok := ret.Get(0).(func(context.Context, *types.QueryGetLatestStateInfoIndexRequest, ...grpc.CallOption) *types.QueryGetLatestStateInfoIndexResponse); ok { + r0 = rf(ctx, in, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*types.QueryGetLatestStateInfoIndexResponse) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *types.QueryGetLatestStateInfoIndexRequest, ...grpc.CallOption) error); ok { + r1 = rf(ctx, in, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// LatestStateInfoIndexAll provides a mock function with given fields: ctx, in, opts +func (_m *RollAppQueryClient) LatestStateInfoIndexAll(ctx context.Context, in *types.QueryAllLatestStateInfoIndexRequest, opts ...grpc.CallOption) (*types.QueryAllLatestStateInfoIndexResponse, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, in) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 *types.QueryAllLatestStateInfoIndexResponse + if rf, ok := ret.Get(0).(func(context.Context, *types.QueryAllLatestStateInfoIndexRequest, ...grpc.CallOption) *types.QueryAllLatestStateInfoIndexResponse); ok { + r0 = rf(ctx, in, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*types.QueryAllLatestStateInfoIndexResponse) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *types.QueryAllLatestStateInfoIndexRequest, ...grpc.CallOption) error); ok { + r1 = rf(ctx, in, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Params provides a mock function with given fields: ctx, in, opts +func (_m *RollAppQueryClient) Params(ctx context.Context, in *types.QueryParamsRequest, opts ...grpc.CallOption) (*types.QueryParamsResponse, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, in) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 *types.QueryParamsResponse + if rf, ok := ret.Get(0).(func(context.Context, *types.QueryParamsRequest, ...grpc.CallOption) *types.QueryParamsResponse); ok { + r0 = rf(ctx, in, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*types.QueryParamsResponse) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *types.QueryParamsRequest, ...grpc.CallOption) error); ok { + r1 = rf(ctx, in, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Rollapp provides a mock function with given fields: ctx, in, opts +func (_m *RollAppQueryClient) Rollapp(ctx context.Context, in *types.QueryGetRollappRequest, opts ...grpc.CallOption) (*types.QueryGetRollappResponse, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, in) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 *types.QueryGetRollappResponse + if rf, ok := ret.Get(0).(func(context.Context, *types.QueryGetRollappRequest, ...grpc.CallOption) *types.QueryGetRollappResponse); ok { + r0 = rf(ctx, in, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*types.QueryGetRollappResponse) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *types.QueryGetRollappRequest, ...grpc.CallOption) error); ok { + r1 = rf(ctx, in, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RollappAll provides a mock function with given fields: ctx, in, opts +func (_m *RollAppQueryClient) RollappAll(ctx context.Context, in *types.QueryAllRollappRequest, opts ...grpc.CallOption) (*types.QueryAllRollappResponse, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, in) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 *types.QueryAllRollappResponse + if rf, ok := ret.Get(0).(func(context.Context, *types.QueryAllRollappRequest, ...grpc.CallOption) *types.QueryAllRollappResponse); ok { + r0 = rf(ctx, in, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*types.QueryAllRollappResponse) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *types.QueryAllRollappRequest, ...grpc.CallOption) error); ok { + r1 = rf(ctx, in, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// StateInfo provides a mock function with given fields: ctx, in, opts +func (_m *RollAppQueryClient) StateInfo(ctx context.Context, in *types.QueryGetStateInfoRequest, opts ...grpc.CallOption) (*types.QueryGetStateInfoResponse, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, in) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 *types.QueryGetStateInfoResponse + if rf, ok := ret.Get(0).(func(context.Context, *types.QueryGetStateInfoRequest, ...grpc.CallOption) *types.QueryGetStateInfoResponse); ok { + r0 = rf(ctx, in, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*types.QueryGetStateInfoResponse) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *types.QueryGetStateInfoRequest, ...grpc.CallOption) error); ok { + r1 = rf(ctx, in, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// StateInfoAll provides a mock function with given fields: ctx, in, opts +func (_m *RollAppQueryClient) StateInfoAll(ctx context.Context, in *types.QueryAllStateInfoRequest, opts ...grpc.CallOption) (*types.QueryAllStateInfoResponse, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, in) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 *types.QueryAllStateInfoResponse + if rf, ok := ret.Get(0).(func(context.Context, *types.QueryAllStateInfoRequest, ...grpc.CallOption) *types.QueryAllStateInfoResponse); ok { + r0 = rf(ctx, in, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*types.QueryAllStateInfoResponse) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *types.QueryAllStateInfoRequest, ...grpc.CallOption) error); ok { + r1 = rf(ctx, in, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +type mockConstructorTestingTNewRollAppQueryClient interface { + mock.TestingT + Cleanup(func()) +} + +// NewRollAppQueryClient creates a new instance of RollAppQueryClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewRollAppQueryClient(t mockConstructorTestingTNewRollAppQueryClient) *RollAppQueryClient { + mock := &RollAppQueryClient{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/mocks/settlement/sequencer_query_client.go b/mocks/settlement/sequencer_query_client.go new file mode 100644 index 000000000..7c80fda50 --- /dev/null +++ b/mocks/settlement/sequencer_query_client.go @@ -0,0 +1,243 @@ +// Code generated by mockery v2.15.0. DO NOT EDIT. + +package mocks + +import ( + context "context" + + grpc "google.golang.org/grpc" + + mock "github.com/stretchr/testify/mock" + + types "github.com/dymensionxyz/dymension/x/sequencer/types" +) + +// SequencerQueryClient is an autogenerated mock type for the QueryClient type +type SequencerQueryClient struct { + mock.Mock +} + +// Params provides a mock function with given fields: ctx, in, opts +func (_m *SequencerQueryClient) Params(ctx context.Context, in *types.QueryParamsRequest, opts ...grpc.CallOption) (*types.QueryParamsResponse, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, in) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 *types.QueryParamsResponse + if rf, ok := ret.Get(0).(func(context.Context, *types.QueryParamsRequest, ...grpc.CallOption) *types.QueryParamsResponse); ok { + r0 = rf(ctx, in, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*types.QueryParamsResponse) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *types.QueryParamsRequest, ...grpc.CallOption) error); ok { + r1 = rf(ctx, in, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Scheduler provides a mock function with given fields: ctx, in, opts +func (_m *SequencerQueryClient) Scheduler(ctx context.Context, in *types.QueryGetSchedulerRequest, opts ...grpc.CallOption) (*types.QueryGetSchedulerResponse, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, in) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 *types.QueryGetSchedulerResponse + if rf, ok := ret.Get(0).(func(context.Context, *types.QueryGetSchedulerRequest, ...grpc.CallOption) *types.QueryGetSchedulerResponse); ok { + r0 = rf(ctx, in, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*types.QueryGetSchedulerResponse) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *types.QueryGetSchedulerRequest, ...grpc.CallOption) error); ok { + r1 = rf(ctx, in, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// SchedulerAll provides a mock function with given fields: ctx, in, opts +func (_m *SequencerQueryClient) SchedulerAll(ctx context.Context, in *types.QueryAllSchedulerRequest, opts ...grpc.CallOption) (*types.QueryAllSchedulerResponse, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, in) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 *types.QueryAllSchedulerResponse + if rf, ok := ret.Get(0).(func(context.Context, *types.QueryAllSchedulerRequest, ...grpc.CallOption) *types.QueryAllSchedulerResponse); ok { + r0 = rf(ctx, in, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*types.QueryAllSchedulerResponse) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *types.QueryAllSchedulerRequest, ...grpc.CallOption) error); ok { + r1 = rf(ctx, in, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Sequencer provides a mock function with given fields: ctx, in, opts +func (_m *SequencerQueryClient) Sequencer(ctx context.Context, in *types.QueryGetSequencerRequest, opts ...grpc.CallOption) (*types.QueryGetSequencerResponse, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, in) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 *types.QueryGetSequencerResponse + if rf, ok := ret.Get(0).(func(context.Context, *types.QueryGetSequencerRequest, ...grpc.CallOption) *types.QueryGetSequencerResponse); ok { + r0 = rf(ctx, in, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*types.QueryGetSequencerResponse) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *types.QueryGetSequencerRequest, ...grpc.CallOption) error); ok { + r1 = rf(ctx, in, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// SequencerAll provides a mock function with given fields: ctx, in, opts +func (_m *SequencerQueryClient) SequencerAll(ctx context.Context, in *types.QueryAllSequencerRequest, opts ...grpc.CallOption) (*types.QueryAllSequencerResponse, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, in) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 *types.QueryAllSequencerResponse + if rf, ok := ret.Get(0).(func(context.Context, *types.QueryAllSequencerRequest, ...grpc.CallOption) *types.QueryAllSequencerResponse); ok { + r0 = rf(ctx, in, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*types.QueryAllSequencerResponse) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *types.QueryAllSequencerRequest, ...grpc.CallOption) error); ok { + r1 = rf(ctx, in, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// SequencersByRollapp provides a mock function with given fields: ctx, in, opts +func (_m *SequencerQueryClient) SequencersByRollapp(ctx context.Context, in *types.QueryGetSequencersByRollappRequest, opts ...grpc.CallOption) (*types.QueryGetSequencersByRollappResponse, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, in) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 *types.QueryGetSequencersByRollappResponse + if rf, ok := ret.Get(0).(func(context.Context, *types.QueryGetSequencersByRollappRequest, ...grpc.CallOption) *types.QueryGetSequencersByRollappResponse); ok { + r0 = rf(ctx, in, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*types.QueryGetSequencersByRollappResponse) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *types.QueryGetSequencersByRollappRequest, ...grpc.CallOption) error); ok { + r1 = rf(ctx, in, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// SequencersByRollappAll provides a mock function with given fields: ctx, in, opts +func (_m *SequencerQueryClient) SequencersByRollappAll(ctx context.Context, in *types.QueryAllSequencersByRollappRequest, opts ...grpc.CallOption) (*types.QueryAllSequencersByRollappResponse, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, in) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 *types.QueryAllSequencersByRollappResponse + if rf, ok := ret.Get(0).(func(context.Context, *types.QueryAllSequencersByRollappRequest, ...grpc.CallOption) *types.QueryAllSequencersByRollappResponse); ok { + r0 = rf(ctx, in, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*types.QueryAllSequencersByRollappResponse) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *types.QueryAllSequencersByRollappRequest, ...grpc.CallOption) error); ok { + r1 = rf(ctx, in, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +type mockConstructorTestingTNewSequencerQueryClient interface { + mock.TestingT + Cleanup(func()) +} + +// NewSequencerQueryClient creates a new instance of SequencerQueryClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewSequencerQueryClient(t mockConstructorTestingTNewSequencerQueryClient) *SequencerQueryClient { + mock := &SequencerQueryClient{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/p2p/client_test.go b/p2p/client_test.go index 8e077e9c5..383c674a9 100644 --- a/p2p/client_test.go +++ b/p2p/client_test.go @@ -79,8 +79,7 @@ func TestGossiping(t *testing.T) { assert := assert.New(t) logger := test.NewLogger(t) - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + ctx := context.Background() var expectedMsg = []byte("foobar") var wg sync.WaitGroup diff --git a/settlement/base.go b/settlement/base.go new file mode 100644 index 000000000..e623dc179 --- /dev/null +++ b/settlement/base.go @@ -0,0 +1,208 @@ +package settlement + +import ( + "context" + "encoding/json" + "errors" + "sync/atomic" + + "github.com/dymensionxyz/dymint/da" + "github.com/dymensionxyz/dymint/log" + "github.com/dymensionxyz/dymint/types" + "github.com/tendermint/tendermint/libs/pubsub" +) + +const ( + defaultBatchSize = 5 +) + +// BaseLayerClient is intended only for usage in tests. +type BaseLayerClient struct { + logger log.Logger + pubsub *pubsub.Server + latestHeight uint64 + sequencersList []*types.Sequencer + config Config + ctx context.Context + cancel context.CancelFunc + client HubClient + // eventMap map[string]string +} + +// Config for the BaseLayerClient +type Config struct { + BatchSize uint64 `json:"batch_size"` + RollappID string `json:"rollapp_id"` +} + +var _ LayerClient = &BaseLayerClient{} + +// WithHubClient is an option which sets the hub client. +func WithHubClient(hubClient HubClient) Option { + return func(settlementClient LayerClient) { + settlementClient.(*BaseLayerClient).client = hubClient + } +} + +// Init is called once. it initializes the struct members. +func (b *BaseLayerClient) Init(config []byte, pubsub *pubsub.Server, logger log.Logger, options ...Option) error { + c, err := b.getConfig(config) + if err != nil { + return err + } + b.config = *c + b.pubsub = pubsub + b.logger = logger + b.ctx, b.cancel = context.WithCancel(context.Background()) + // Apply options + for _, apply := range options { + apply(b) + } + return nil +} + +// Start is called once, after init. It initializes the query client. +func (b *BaseLayerClient) Start() error { + b.logger.Debug("settlement Layer Client starting.") + latestBatch, err := b.RetrieveBatch() + var endHeight uint64 + if err != nil { + if err == ErrBatchNotFound { + endHeight = 0 + } else { + return err + } + } else { + endHeight = latestBatch.EndHeight + } + b.latestHeight = endHeight + b.logger.Info("Updated latest height from settlement layer", "latestHeight", b.latestHeight) + b.sequencersList, err = b.fetchSequencersList() + if err != nil { + if err == ErrNoSequencerForRollapp { + panic(err) + } + return err + } + b.logger.Info("Updated sequencers list from settlement layer", "sequencersList", b.sequencersList) + + err = b.client.Start() + if err != nil { + return err + } + + return nil +} + +// Stop is called once, after Start. +func (b *BaseLayerClient) Stop() error { + b.logger.Info("Settlement Layer Client stopping") + err := b.client.Stop() + if err != nil { + return err + } + b.cancel() + return nil +} + +// SubmitBatch submits the batch to the settlement layer. This should create a transaction which (potentially) +// triggers a state transition in the settlement layer. +func (b *BaseLayerClient) SubmitBatch(batch *types.Batch, daClient da.Client, daResult *da.ResultSubmitBatch) *ResultSubmitBatch { + b.logger.Debug("Submitting batch to settlement layer", "start height", batch.StartHeight, "end height", batch.EndHeight) + err := b.validateBatch(batch) + if err != nil { + return &ResultSubmitBatch{ + BaseResult: BaseResult{Code: StatusError, Message: err.Error()}, + } + } + txResp, err := b.client.PostBatch(batch, daClient, daResult) + if err != nil || txResp.GetCode() != 0 { + b.logger.Error("Error sending batch to settlement layer", "error", err) + return &ResultSubmitBatch{ + BaseResult: BaseResult{Code: StatusError, Message: err.Error()}, + } + } + b.logger.Info("Successfully submitted batch to settlement layer", "tx hash", txResp.GetTxHash()) + atomic.StoreUint64(&b.latestHeight, batch.EndHeight) + return &ResultSubmitBatch{ + BaseResult: BaseResult{Code: StatusSuccess}, + } +} + +// RetrieveBatch Gets the batch which contains the given slHeight. Empty slHeight returns the latest batch. +func (b *BaseLayerClient) RetrieveBatch(stateIndex ...uint64) (*ResultRetrieveBatch, error) { + var resultRetrieveBatch *ResultRetrieveBatch + var err error + if len(stateIndex) == 0 { + b.logger.Debug("Getting latest batch from settlement layer") + resultRetrieveBatch, err = b.client.GetLatestBatch(b.config.RollappID) + if err != nil { + return nil, err + } + } else if len(stateIndex) == 1 { + b.logger.Debug("Getting batch from settlement layer", "state index", stateIndex) + resultRetrieveBatch, err = b.client.GetBatchAtIndex(b.config.RollappID, stateIndex[0]) + if err != nil { + return nil, err + } + } + return resultRetrieveBatch, nil +} + +// GetSequencersList returns the current list of sequencers from the settlement layer +func (b *BaseLayerClient) GetSequencersList() []*types.Sequencer { + return b.sequencersList +} + +// GetProposer returns the sequencer which is currently the proposer +func (b *BaseLayerClient) GetProposer() *types.Sequencer { + for _, sequencer := range b.sequencersList { + if sequencer.Status == types.Proposer { + return sequencer + } + } + return nil +} + +func (b *BaseLayerClient) fetchSequencersList() ([]*types.Sequencer, error) { + sequencers, err := b.client.GetSequencers(b.config.RollappID) + if err != nil { + return nil, err + } + return sequencers, nil +} + +func (b *BaseLayerClient) decodeConfig(config []byte) (*Config, error) { + var c Config + err := json.Unmarshal(config, &c) + return &c, err +} + +func (b *BaseLayerClient) getConfig(config []byte) (*Config, error) { + var c *Config + if len(config) > 0 { + var err error + c, err = b.decodeConfig(config) + if err != nil { + return nil, err + } + if c.BatchSize == 0 { + c.BatchSize = defaultBatchSize + } + } else { + c = &Config{ + BatchSize: defaultBatchSize, + } + } + return c, nil +} + +func (b *BaseLayerClient) validateBatch(batch *types.Batch) error { + if batch.StartHeight != atomic.LoadUint64(&b.latestHeight)+1 { + return errors.New("batch start height must be last height + 1") + } + if batch.EndHeight < batch.StartHeight { + return errors.New("batch end height must be greater or equal to start height") + } + return nil +} diff --git a/settlement/dymension.go b/settlement/dymension.go deleted file mode 100644 index 3cfced90d..000000000 --- a/settlement/dymension.go +++ /dev/null @@ -1,355 +0,0 @@ -package settlement - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "strconv" - - "github.com/dymensionxyz/cosmosclient/cosmosclient" - rollapptypes "github.com/dymensionxyz/dymension/x/rollapp/types" - "github.com/dymensionxyz/dymint/da" - "github.com/dymensionxyz/dymint/log" - "github.com/dymensionxyz/dymint/types" - "github.com/hashicorp/go-multierror" - "github.com/ignite/cli/ignite/pkg/cosmosaccount" - "github.com/tendermint/tendermint/libs/pubsub" - ctypes "github.com/tendermint/tendermint/rpc/core/types" -) - -const ( - defaultBatchSize = 5 - addressPrefix = "dym" - dymRollappVersion = 0 - defaultNodeAddress = "http://localhost:26657" -) - -const ( - eventStateUpdate = "state_update.rollapp_id='%s'" -) - -// DymensionLayerClient is intended only for usage in tests. -type DymensionLayerClient struct { - logger log.Logger - pubsub *pubsub.Server - latestHeight uint64 - config Config - ctx context.Context - cancel context.CancelFunc - client *cosmosclient.Client - rollappQueryClient rollapptypes.QueryClient - // eventsChannel is the channel that receives events from the dymension layer. - eventsChannel <-chan ctypes.ResultEvent - eventMap map[string]string -} - -// Config for the DymensionLayerClient -type Config struct { - BatchSize uint64 `json:"batch_size"` - KeyringBackend cosmosaccount.KeyringBackend `json:"keyring_backend"` - NodeAddress string `json:"node_address"` - KeyRingHomeDir string `json:"keyring_home_dir"` - DymAccountName string `json:"dym_account_name"` - RollappID string `json:"rollapp_id"` -} - -var _ LayerClient = &DymensionLayerClient{} - -// Init is called once. it initializes the struct members. -func (d *DymensionLayerClient) Init(config []byte, pubsub *pubsub.Server, logger log.Logger) error { - c, err := d.getConfig(config) - if err != nil { - return err - } - d.config = *c - d.pubsub = pubsub - d.logger = logger - d.ctx, d.cancel = context.WithCancel(context.Background()) - client, err := cosmosclient.New( - d.ctx, - d.getCosmosClientOptions(d.config)..., - ) - if err != nil { - return err - } - d.client = &client - d.rollappQueryClient = rollapptypes.NewQueryClient(d.client.Context()) - // TODO(omritoptix): Build this map dynamically from our events. - d.eventMap = map[string]string{ - fmt.Sprintf(eventStateUpdate, d.config.RollappID): EventNewSettlementBatchAccepted, - } - return nil -} - -func (d *DymensionLayerClient) decodeConfig(config []byte) (*Config, error) { - var c Config - err := json.Unmarshal(config, &c) - return &c, err -} - -func (d *DymensionLayerClient) getConfig(config []byte) (*Config, error) { - var c *Config - if len(config) > 0 { - var err error - c, err = d.decodeConfig(config) - if err != nil { - return nil, err - } - if c.BatchSize == 0 { - c.BatchSize = defaultBatchSize - } - } else { - c = &Config{ - BatchSize: defaultBatchSize, - KeyringBackend: cosmosaccount.KeyringTest, - NodeAddress: defaultNodeAddress, - } - } - return c, nil -} - -func (d *DymensionLayerClient) getCosmosClientOptions(c Config) []cosmosclient.Option { - options := []cosmosclient.Option{ - cosmosclient.WithAddressPrefix(addressPrefix), - cosmosclient.WithNodeAddress(d.config.NodeAddress), - } - if d.config.KeyringBackend != "" { - options = append(options, - cosmosclient.WithKeyringBackend(d.config.KeyringBackend), - cosmosclient.WithHome(d.config.KeyRingHomeDir)) - } - return options -} - -// Start is called once, after init. It initializes the query client. -func (d *DymensionLayerClient) Start() error { - d.logger.Debug("settlement Layer Client starting.") - latestBatch, err := d.RetrieveBatch() - var endHeight uint64 - if err != nil { - if err == ErrBatchNotFound { - endHeight = 0 - } else { - return err - } - } else { - endHeight = latestBatch.EndHeight - } - d.latestHeight = endHeight - d.logger.Info("Updated latest height from settlement layer", "latestHeight", d.latestHeight) - - // Subscribe to relevant events - err = d.client.RPC.WSEvents.Start() - if err != nil { - return err - } - // TODO(omritoptix): eventsChannel should be a generic channel which is later filtered by the event type. - d.eventsChannel, err = d.client.RPC.WSEvents.Subscribe(context.Background(), "dymension-client", fmt.Sprintf(eventStateUpdate, d.config.RollappID)) - if err != nil { - return err - } - go d.eventHandler() - - return nil -} - -// Stop is called once, after Start. -func (d *DymensionLayerClient) Stop() error { - d.logger.Info("Settlement Layer Client stopping") - err := d.client.RPC.WSEvents.Stop() - if err != nil { - return err - } - d.cancel() - return nil -} - -func (d *DymensionLayerClient) eventHandler() { - for { - select { - case <-d.ctx.Done(): - return - case <-d.client.RPC.WSEvents.GetWSClient().Quit(): - panic("Settlement WS disconnected") - case event := <-d.eventsChannel: - // Assert value is in map and publish it to the event bus - d.logger.Debug("Received event from settlement layer", "event", event) - _, ok := d.eventMap[event.Query] - if !ok { - d.logger.Debug("Ignoring event. Type not supported", "event", event) - continue - } - eventData, err := d.getEventData(d.eventMap[event.Query], event) - if err != nil { - d.logger.Error("Failed to get event data", "err", err) - continue - } - err = d.pubsub.PublishWithEvents(d.ctx, eventData, map[string][]string{EventTypeKey: {d.eventMap[event.Query]}}) - if err != nil { - d.logger.Error("Error publishing event", "err", err) - } - } - } -} - -func (d *DymensionLayerClient) getEventData(eventType string, rawEventData ctypes.ResultEvent) (interface{}, error) { - switch eventType { - case EventNewSettlementBatchAccepted: - var multiErr *multierror.Error - numBlocks, err := strconv.ParseInt(rawEventData.Events["state_update.num_blocks"][0], 10, 64) - multiErr = multierror.Append(multiErr, err) - startHeight, err := strconv.ParseInt(rawEventData.Events["state_update.start_height"][0], 10, 64) - multiErr = multierror.Append(multiErr, err) - stateIndex, err := strconv.ParseInt(rawEventData.Events["state_update.state_info_index"][0], 10, 64) - multiErr = multierror.Append(multiErr, err) - err = multiErr.ErrorOrNil() - if err != nil { - return nil, multiErr - } - endHeight := uint64(startHeight + numBlocks - 1) - NewBatchEvent := &EventDataNewSettlementBatchAccepted{ - EndHeight: endHeight, - StateIndex: uint64(stateIndex), - } - return NewBatchEvent, nil - } - return nil, fmt.Errorf("event type %s not recognized", eventType) -} - -func (d *DymensionLayerClient) validateBatch(batch *types.Batch) error { - if batch.StartHeight != d.latestHeight+1 { - return errors.New("batch start height must be last height + 1") - } - if batch.EndHeight < batch.StartHeight { - return errors.New("batch end height must be greater or equal to start height") - } - return nil -} - -func (d *DymensionLayerClient) convertBatchtoSettlementBatch(batch *types.Batch, daClient da.Client, daResult *da.ResultSubmitBatch) (*rollapptypes.MsgUpdateState, error) { - account, err := d.client.Account(d.config.DymAccountName) - if err != nil { - return nil, err - } - addr := account.Address(addressPrefix) - DAMetaData := &DAMetaData{ - Height: daResult.DAHeight, - Client: daClient, - } - blockDescriptors := make([]rollapptypes.BlockDescriptor, len(batch.Blocks)) - for index, block := range batch.Blocks { - blockDescriptor := rollapptypes.BlockDescriptor{ - Height: block.Header.Height, - StateRoot: block.Header.AppHash[:], - // TODO(omritoptix): Change to a real ISR once supported - IntermediateStatesRoot: make([]byte, 32), - } - blockDescriptors[index] = blockDescriptor - } - settlementBatch := &rollapptypes.MsgUpdateState{ - Creator: addr, - RollappId: d.config.RollappID, - StartHeight: batch.StartHeight, - NumBlocks: batch.EndHeight - batch.StartHeight + 1, - DAPath: DAMetaData.toPath(), - Version: dymRollappVersion, - BDs: rollapptypes.BlockDescriptors{BD: blockDescriptors}, - } - return settlementBatch, nil -} - -// SubmitBatch submits the batch to the settlement layer. This should create a transaction which (potentially) -// triggers a state transition in the settlement layer. -func (d *DymensionLayerClient) SubmitBatch(batch *types.Batch, daClient da.Client, daResult *da.ResultSubmitBatch) *ResultSubmitBatch { - d.logger.Debug("Submitting batch to settlement layer", "start height", batch.StartHeight, "end height", batch.EndHeight) - // validate batch - err := d.validateBatch(batch) - if err != nil { - return &ResultSubmitBatch{ - BaseResult: BaseResult{Code: StatusError, Message: err.Error()}, - } - } - // Build the result to save in the settlement layer. - settlementBatch, err := d.convertBatchtoSettlementBatch(batch, daClient, daResult) - if err != nil { - return &ResultSubmitBatch{ - BaseResult: BaseResult{Code: StatusError, Message: err.Error()}, - } - } - // Send the batch to the settlement layer. stateIndex will be updated by an event. - txResp, err := d.client.BroadcastTx(d.config.DymAccountName, settlementBatch) - if err != nil || txResp.Code != 0 { - d.logger.Error("Error sending batch to settlement layer", "error", err) - return &ResultSubmitBatch{ - BaseResult: BaseResult{Code: StatusError, Message: err.Error()}, - } - } - d.logger.Info("Successfully submitted batch to settlement layer", "tx hash", txResp.TxHash) - d.latestHeight = batch.EndHeight - return &ResultSubmitBatch{ - BaseResult: BaseResult{Code: StatusSuccess}, - } -} - -// RetrieveBatch Gets the batch which contains the given slHeight . Empty slHeight returns the latest batch. -func (d *DymensionLayerClient) RetrieveBatch(stateIndex ...uint64) (*ResultRetrieveBatch, error) { - var stateInfo rollapptypes.StateInfo - if len(stateIndex) == 0 { - d.logger.Debug("Getting latest batch from settlement layer") - latestStateInfoIndexResp, err := d.rollappQueryClient.LatestStateInfoIndex(d.ctx, - &rollapptypes.QueryGetLatestStateInfoIndexRequest{RollappId: d.config.RollappID}) - if latestStateInfoIndexResp == nil { - return nil, ErrBatchNotFound - } - if err != nil { - return nil, err - } - stateInfoResp, err := d.rollappQueryClient.StateInfo(d.ctx, - &rollapptypes.QueryGetStateInfoRequest{ - RollappId: d.config.RollappID, Index: latestStateInfoIndexResp.LatestStateInfoIndex.Index}, - ) - if err != nil { - return nil, err - } - stateInfo = stateInfoResp.StateInfo - } else if len(stateIndex) == 1 { - d.logger.Debug("Getting batch from settlement layer", "state index", stateIndex) - queryResp, err := d.rollappQueryClient.StateInfo(context.Background(), &rollapptypes.QueryGetStateInfoRequest{RollappId: d.config.RollappID, Index: stateIndex[0]}) - if err != nil { - return nil, err - } - if queryResp == nil { - return nil, ErrBatchNotFound - } - stateInfo = queryResp.StateInfo - } - daMetaData := &DAMetaData{} - daMetaData, err := daMetaData.fromPath(stateInfo.DAPath) - if err != nil { - return nil, err - } - batchResult := &Batch{ - StartHeight: stateInfo.StartHeight, - EndHeight: stateInfo.StartHeight + stateInfo.NumBlocks - 1, - MetaData: &BatchMetaData{ - DA: daMetaData, - }, - } - return &ResultRetrieveBatch{ - BaseResult: BaseResult{Code: StatusSuccess, StateIndex: stateInfo.StateInfoIndex.Index}, - Batch: batchResult}, nil -} - -// GetLatestFinalizedStateHeight returns the latest-finalized-state height of the active rollapp -func (d *DymensionLayerClient) GetLatestFinalizedStateHeight(rollapID string) (int64, error) { - latestFinalizedStateInfoResponse, err := d.rollappQueryClient.LatestFinalizedStateInfo(d.ctx, - &rollapptypes.QueryGetLatestFinalizedStateInfoRequest{RollappId: rollapID}) - if err != nil { - return -1, err - } - if latestFinalizedStateInfoResponse == nil { - return -1, fmt.Errorf("can't get latest-finalized-state info") - } - return int64(latestFinalizedStateInfoResponse.StateInfo.StartHeight), nil -} diff --git a/settlement/dymension/cosmosclient.go b/settlement/dymension/cosmosclient.go new file mode 100644 index 000000000..53456579e --- /dev/null +++ b/settlement/dymension/cosmosclient.go @@ -0,0 +1,62 @@ +package dymension + +import ( + "context" + + sdkclient "github.com/cosmos/cosmos-sdk/client" + sdktypes "github.com/cosmos/cosmos-sdk/types" + "github.com/dymensionxyz/cosmosclient/cosmosclient" + rollapptypes "github.com/dymensionxyz/dymension/x/rollapp/types" + sequencertypes "github.com/dymensionxyz/dymension/x/sequencer/types" + ctypes "github.com/tendermint/tendermint/rpc/core/types" +) + +// CosmosClient is an interface for interacting with cosmos client chains. +// It is a wrapper around the cosmos client in order to provide with an interface which can be implemented by +// other clients and can easily be mocked for testing purposes. +// Currently it contains only the methods that are used by the dymension hub client. +type CosmosClient interface { + Context() sdkclient.Context + StartEventListener() error + StopEventListener() error + EventListenerQuit() <-chan struct{} + SubscribeToEvents(ctx context.Context, subscriber string, query string, outCapacity ...int) (out <-chan ctypes.ResultEvent, err error) + BroadcastTx(accountName string, msgs ...sdktypes.Msg) (cosmosclient.Response, error) + GetRollappClient() rollapptypes.QueryClient + GetSequencerClient() sequencertypes.QueryClient +} + +type cosmosClient struct { + cosmosclient.Client +} + +var _ CosmosClient = &cosmosClient{} + +// NewCosmosClient creates a new cosmos client +func NewCosmosClient(client cosmosclient.Client) CosmosClient { + return &cosmosClient{client} +} + +func (c *cosmosClient) StartEventListener() error { + return c.Client.RPC.WSEvents.Start() +} + +func (c *cosmosClient) StopEventListener() error { + return c.Client.RPC.WSEvents.Stop() +} + +func (c *cosmosClient) EventListenerQuit() <-chan struct{} { + return c.Client.RPC.GetWSClient().Quit() +} + +func (c *cosmosClient) SubscribeToEvents(ctx context.Context, subscriber string, query string, outCapacity ...int) (out <-chan ctypes.ResultEvent, err error) { + return c.Client.RPC.WSEvents.Subscribe(ctx, subscriber, query, outCapacity...) +} + +func (c *cosmosClient) GetRollappClient() rollapptypes.QueryClient { + return rollapptypes.NewQueryClient(c.Context()) +} + +func (c *cosmosClient) GetSequencerClient() sequencertypes.QueryClient { + return sequencertypes.NewQueryClient(c.Context()) +} diff --git a/settlement/dymension/dymension.go b/settlement/dymension/dymension.go new file mode 100644 index 000000000..6a9b497e0 --- /dev/null +++ b/settlement/dymension/dymension.go @@ -0,0 +1,401 @@ +package dymension + +import ( + "context" + "encoding/json" + "fmt" + "strconv" + + "github.com/cosmos/cosmos-sdk/codec" + cdctypes "github.com/cosmos/cosmos-sdk/codec/types" + cryptocodec "github.com/cosmos/cosmos-sdk/crypto/codec" + "github.com/dymensionxyz/cosmosclient/cosmosclient" + rollapptypes "github.com/dymensionxyz/dymension/x/rollapp/types" + + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + sequencertypes "github.com/dymensionxyz/dymension/x/sequencer/types" + "github.com/dymensionxyz/dymint/da" + "github.com/dymensionxyz/dymint/log" + "github.com/dymensionxyz/dymint/settlement" + "github.com/dymensionxyz/dymint/types" + "github.com/hashicorp/go-multierror" + "github.com/ignite/cli/ignite/pkg/cosmosaccount" + "github.com/tendermint/tendermint/libs/pubsub" + ctypes "github.com/tendermint/tendermint/rpc/core/types" +) + +const ( + addressPrefix = "dym" + dymRollappVersion = 0 + defaultNodeAddress = "http://localhost:26657" +) + +const ( + eventStateUpdate = "state_update.rollapp_id='%s'" + eventSequencersListUpdate = "sequencers_list_update.rollapp_id='%s'" +) + +// LayerClient is intended only for usage in tests. +type LayerClient struct { + *settlement.BaseLayerClient +} + +// Config for the DymensionLayerClient +type Config struct { + KeyringBackend cosmosaccount.KeyringBackend `json:"keyring_backend"` + NodeAddress string `json:"node_address"` + KeyRingHomeDir string `json:"keyring_home_dir"` + DymAccountName string `json:"dym_account_name"` + RollappID string `json:"rollapp_id"` +} + +var _ settlement.LayerClient = &LayerClient{} + +// Init is called once. it initializes the struct members. +func (dlc *LayerClient) Init(config []byte, pubsub *pubsub.Server, logger log.Logger, options ...settlement.Option) error { + DymensionCosmosClient, err := newDymensionHubClient(config, pubsub, logger) + if err != nil { + return err + } + baseOptions := []settlement.Option{ + settlement.WithHubClient(DymensionCosmosClient), + } + if options == nil { + options = baseOptions + } else { + options = append(baseOptions, options...) + } + dlc.BaseLayerClient = &settlement.BaseLayerClient{} + err = dlc.BaseLayerClient.Init(config, pubsub, logger, options...) + if err != nil { + return err + } + return nil +} + +// PostBatchResp is the response after posting a batch to the Dymension Hub. +type PostBatchResp struct { + resp cosmosclient.Response +} + +// GetTxHash returns the transaction hash. +func (d PostBatchResp) GetTxHash() string { + return d.resp.TxHash +} + +// GetCode returns the response code. +func (d PostBatchResp) GetCode() uint32 { + return uint32(d.resp.Code) +} + +// HubClient is the client for the Dymension Hub. +type HubClient struct { + config *Config + logger log.Logger + pubsub *pubsub.Server + client CosmosClient + ctx context.Context + cancel context.CancelFunc + rollappQueryClient rollapptypes.QueryClient + sequencerQueryClient sequencertypes.QueryClient + protoCodec *codec.ProtoCodec + eventMap map[string]string +} + +var _ settlement.HubClient = &HubClient{} + +// Option is a function that configures the HubClient. +type Option func(*HubClient) + +// WithCosmosClient is an option that sets the CosmosClient. +func WithCosmosClient(cosmosClient CosmosClient) Option { + return func(d *HubClient) { + d.client = cosmosClient + } +} + +func newDymensionHubClient(config []byte, pubsub *pubsub.Server, logger log.Logger, options ...Option) (*HubClient, error) { + conf, err := getConfig(config) + if err != nil { + return nil, err + } + + ctx, cancel := context.WithCancel(context.Background()) + eventMap := map[string]string{ + fmt.Sprintf(eventStateUpdate, conf.RollappID): settlement.EventNewSettlementBatchAccepted, + fmt.Sprintf(eventSequencersListUpdate, conf.RollappID): settlement.EventSequencersListUpdated, + } + + interfaceRegistry := cdctypes.NewInterfaceRegistry() + cryptocodec.RegisterInterfaces(interfaceRegistry) + protoCodec := codec.NewProtoCodec(interfaceRegistry) + + dymesionHubClient := &HubClient{ + config: conf, + logger: logger, + pubsub: pubsub, + ctx: ctx, + cancel: cancel, + eventMap: eventMap, + protoCodec: protoCodec, + } + + for _, option := range options { + option(dymesionHubClient) + } + + if dymesionHubClient.client == nil { + client, err := cosmosclient.New( + ctx, + getCosmosClientOptions(conf)..., + ) + if err != nil { + return nil, err + } + dymesionHubClient.client = NewCosmosClient(client) + } + dymesionHubClient.rollappQueryClient = dymesionHubClient.client.GetRollappClient() + dymesionHubClient.sequencerQueryClient = dymesionHubClient.client.GetSequencerClient() + + return dymesionHubClient, nil +} + +func decodeConfig(config []byte) (*Config, error) { + var c Config + err := json.Unmarshal(config, &c) + return &c, err +} + +func getConfig(config []byte) (*Config, error) { + var c *Config + if len(config) > 0 { + var err error + c, err = decodeConfig(config) + if err != nil { + return nil, err + } + } else { + c = &Config{ + KeyringBackend: cosmosaccount.KeyringTest, + NodeAddress: defaultNodeAddress, + } + } + return c, nil +} + +// Start starts the HubClient. +func (d *HubClient) Start() error { + err := d.client.StartEventListener() + if err != nil { + return err + } + go d.eventHandler() + return nil + +} + +// Stop stops the HubClient. +func (d *HubClient) Stop() error { + err := d.client.StopEventListener() + if err != nil { + return err + } + d.cancel() + return nil +} + +// PostBatch posts a batch to the Dymension Hub. +func (d *HubClient) PostBatch(batch *types.Batch, daClient da.Client, daResult *da.ResultSubmitBatch) (settlement.PostBatchResp, error) { + msgUpdateState, err := d.convertBatchToMsgUpdateState(batch, daClient, daResult) + if err != nil { + return nil, err + } + txResp, err := d.client.BroadcastTx(d.config.DymAccountName, msgUpdateState) + if err != nil || txResp.Code != 0 { + d.logger.Error("Error sending batch to settlement layer", "error", err) + return PostBatchResp{resp: txResp}, err + } + return PostBatchResp{resp: txResp}, nil +} + +// GetLatestBatch returns the latest batch from the Dymension Hub. +func (d *HubClient) GetLatestBatch(rollappID string) (*settlement.ResultRetrieveBatch, error) { + latestStateInfoIndexResp, err := d.rollappQueryClient.LatestStateInfoIndex(d.ctx, + &rollapptypes.QueryGetLatestStateInfoIndexRequest{RollappId: d.config.RollappID}) + if latestStateInfoIndexResp == nil { + return nil, settlement.ErrBatchNotFound + } + if err != nil { + return nil, err + } + latestBatch, err := d.GetBatchAtIndex(rollappID, latestStateInfoIndexResp.LatestStateInfoIndex.Index) + if err != nil { + return nil, err + } + return latestBatch, nil +} + +// GetBatchAtIndex returns the batch at the given index from the Dymension Hub. +func (d *HubClient) GetBatchAtIndex(rollappID string, index uint64) (*settlement.ResultRetrieveBatch, error) { + stateInfoResp, err := d.rollappQueryClient.StateInfo(d.ctx, + &rollapptypes.QueryGetStateInfoRequest{RollappId: d.config.RollappID, Index: index}) + if stateInfoResp == nil { + return nil, settlement.ErrBatchNotFound + } + if err != nil { + return nil, err + } + return d.convertStateInfoToResultRetrieveBatch(&stateInfoResp.StateInfo) +} + +// GetSequencers returns the sequence of the given rollapp. +func (d *HubClient) GetSequencers(rollappID string) ([]*types.Sequencer, error) { + sequencers, err := d.sequencerQueryClient.SequencersByRollapp(d.ctx, &sequencertypes.QueryGetSequencersByRollappRequest{RollappId: d.config.RollappID}) + if err != nil { + return nil, err + } + if sequencers == nil { + return nil, settlement.ErrNoSequencerForRollapp + } + sequencersList := []*types.Sequencer{} + for _, sequencer := range sequencers.SequencerInfoList { + var pubKey cryptotypes.PubKey + err := d.protoCodec.UnpackAny(sequencer.Sequencer.DymintPubKey, &pubKey) + if err != nil { + return nil, err + } + var status types.SequencerStatus + if sequencer.Status == sequencertypes.Proposer { + status = types.Proposer + } else { + status = types.Inactive + } + if err != nil { + return nil, err + } + sequencersList = append(sequencersList, &types.Sequencer{ + PublicKey: pubKey, + Status: status, + }) + } + return sequencersList, nil +} + +func (d *HubClient) eventHandler() { + // TODO(omritoptix): eventsChannel should be a generic channel which is later filtered by the event type. + eventsChannel, err := d.client.SubscribeToEvents(context.Background(), "dymension-client", fmt.Sprintf(eventStateUpdate, d.config.RollappID)) + if err != nil { + panic("Error subscribing to events") + } + for { + select { + case <-d.ctx.Done(): + return + case <-d.client.EventListenerQuit(): + panic("Settlement WS disconnected") + case event := <-eventsChannel: + // Assert value is in map and publish it to the event bus + d.logger.Debug("Received event from settlement layer", "event", event) + _, ok := d.eventMap[event.Query] + if !ok { + d.logger.Debug("Ignoring event. Type not supported", "event", event) + continue + } + eventData, err := d.getEventData(d.eventMap[event.Query], event) + if err != nil { + d.logger.Error("Failed to get event data", "err", err) + continue + } + err = d.pubsub.PublishWithEvents(d.ctx, eventData, map[string][]string{settlement.EventTypeKey: {d.eventMap[event.Query]}}) + if err != nil { + d.logger.Error("Error publishing event", "err", err) + } + } + } +} + +func (d *HubClient) convertBatchToMsgUpdateState(batch *types.Batch, daClient da.Client, daResult *da.ResultSubmitBatch) (*rollapptypes.MsgUpdateState, error) { + DAMetaData := &settlement.DAMetaData{ + Height: daResult.DAHeight, + Client: daClient, + } + blockDescriptors := make([]rollapptypes.BlockDescriptor, len(batch.Blocks)) + for index, block := range batch.Blocks { + blockDescriptor := rollapptypes.BlockDescriptor{ + Height: block.Header.Height, + StateRoot: block.Header.AppHash[:], + // TODO(omritoptix): Change to a real ISR once supported + IntermediateStatesRoot: make([]byte, 32), + } + blockDescriptors[index] = blockDescriptor + } + settlementBatch := &rollapptypes.MsgUpdateState{ + RollappId: d.config.RollappID, + StartHeight: batch.StartHeight, + NumBlocks: batch.EndHeight - batch.StartHeight + 1, + DAPath: DAMetaData.ToPath(), + Version: dymRollappVersion, + BDs: rollapptypes.BlockDescriptors{BD: blockDescriptors}, + } + return settlementBatch, nil + +} + +func getCosmosClientOptions(config *Config) []cosmosclient.Option { + options := []cosmosclient.Option{ + cosmosclient.WithAddressPrefix(addressPrefix), + cosmosclient.WithNodeAddress(config.NodeAddress), + } + if config.KeyringBackend != "" { + options = append(options, + cosmosclient.WithKeyringBackend(config.KeyringBackend), + cosmosclient.WithHome(config.KeyRingHomeDir)) + } + return options +} + +func (d *HubClient) getEventData(eventType string, rawEventData ctypes.ResultEvent) (interface{}, error) { + switch eventType { + case settlement.EventNewSettlementBatchAccepted: + return d.convertToNewBatchEvent(rawEventData) + } + return nil, fmt.Errorf("event type %s not recognized", eventType) +} + +func (d *HubClient) convertToNewBatchEvent(rawEventData ctypes.ResultEvent) (*settlement.EventDataNewSettlementBatchAccepted, error) { + var multiErr *multierror.Error + numBlocks, err := strconv.ParseInt(rawEventData.Events["state_update.num_blocks"][0], 10, 64) + multiErr = multierror.Append(multiErr, err) + startHeight, err := strconv.ParseInt(rawEventData.Events["state_update.start_height"][0], 10, 64) + multiErr = multierror.Append(multiErr, err) + stateIndex, err := strconv.ParseInt(rawEventData.Events["state_update.state_info_index"][0], 10, 64) + multiErr = multierror.Append(multiErr, err) + err = multiErr.ErrorOrNil() + if err != nil { + return nil, multiErr + } + endHeight := uint64(startHeight + numBlocks - 1) + NewBatchEvent := &settlement.EventDataNewSettlementBatchAccepted{ + EndHeight: endHeight, + StateIndex: uint64(stateIndex), + } + return NewBatchEvent, nil +} + +func (d *HubClient) convertStateInfoToResultRetrieveBatch(stateInfo *rollapptypes.StateInfo) (*settlement.ResultRetrieveBatch, error) { + daMetaData := &settlement.DAMetaData{} + daMetaData, err := daMetaData.FromPath(stateInfo.DAPath) + if err != nil { + return nil, err + } + batchResult := &settlement.Batch{ + StartHeight: stateInfo.StartHeight, + EndHeight: stateInfo.StartHeight + stateInfo.NumBlocks - 1, + MetaData: &settlement.BatchMetaData{ + DA: daMetaData, + }, + } + return &settlement.ResultRetrieveBatch{ + BaseResult: settlement.BaseResult{Code: settlement.StatusSuccess, StateIndex: stateInfo.StateInfoIndex.Index}, + Batch: batchResult}, nil +} diff --git a/settlement/dymension/dymension_test.go b/settlement/dymension/dymension_test.go new file mode 100644 index 000000000..0b46bd057 --- /dev/null +++ b/settlement/dymension/dymension_test.go @@ -0,0 +1,79 @@ +package dymension + +import ( + "testing" + + "github.com/cosmos/cosmos-sdk/crypto/keys/ed25519" + "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + "github.com/tendermint/tendermint/libs/pubsub" + + sequencertypes "github.com/dymensionxyz/dymension/x/sequencer/types" + "github.com/dymensionxyz/dymint/log/test" + mocks "github.com/dymensionxyz/dymint/mocks" + settlementmocks "github.com/dymensionxyz/dymint/mocks/settlement" + + sdkcodectypes "github.com/cosmos/cosmos-sdk/codec/types" +) + +func TestGetSequencers(t *testing.T) { + require := require.New(t) + cosmosClientMock := mocks.NewCosmosClient(t) + + sequencerQueryClientMock := settlementmocks.NewSequencerQueryClient(t) + count := 5 + sequencersRollappResponse, _ := generateSequencerByRollappResponse(t, count) + sequencerQueryClientMock.On("SequencersByRollapp", mock.Anything, mock.Anything).Return(sequencersRollappResponse, nil) + + cosmosClientMock.On("GetRollappClient").Return(settlementmocks.NewRollAppQueryClient(t)) + cosmosClientMock.On("GetSequencerClient").Return(sequencerQueryClientMock) + + options := []Option{ + WithCosmosClient(cosmosClientMock), + } + + pubsubServer := pubsub.NewServer() + pubsubServer.Start() + + hubClient, err := newDymensionHubClient([]byte{}, pubsubServer, test.NewLogger(t), options...) + require.NoError(err) + + sequencers, err := hubClient.GetSequencers("mock-rollapp") + require.NoError(err) + require.Len(sequencers, count) +} + +/* -------------------------------------------------------------------------- */ +/* Utils */ +/* -------------------------------------------------------------------------- */ + +func generateSequencerByRollappResponse(t *testing.T, count int) (*sequencertypes.QueryGetSequencersByRollappResponse, sequencertypes.SequencerInfo) { + // Generate the proposer sequencer + proposerPubKeyAny, err := sdkcodectypes.NewAnyWithValue(ed25519.GenPrivKey().PubKey()) + require.NoError(t, err) + proposer := sequencertypes.SequencerInfo{ + Sequencer: sequencertypes.Sequencer{ + DymintPubKey: proposerPubKeyAny, + }, + Status: sequencertypes.Proposer, + } + squencerInfoList := []sequencertypes.SequencerInfo{ + proposer, + } + // Generate the inactive sequencers + for i := 0; i < count-1; i++ { + nonProposerPubKeyAny, err := sdkcodectypes.NewAnyWithValue(secp256k1.GenPrivKey().PubKey()) + require.NoError(t, err) + squencerInfoList = append(squencerInfoList, sequencertypes.SequencerInfo{ + Sequencer: sequencertypes.Sequencer{ + DymintPubKey: nonProposerPubKeyAny, + }, + Status: sequencertypes.Inactive, + }) + } + response := &sequencertypes.QueryGetSequencersByRollappResponse{ + SequencerInfoList: squencerInfoList, + } + return response, proposer +} diff --git a/settlement/errors.go b/settlement/errors.go index 7d60ac2fe..9cd406090 100644 --- a/settlement/errors.go +++ b/settlement/errors.go @@ -3,6 +3,8 @@ package settlement import "errors" var ( - // ErrBatchNotFound is returned when a batch is not found in the kvstore. + // ErrBatchNotFound is returned when a batch is not found for the rollapp. ErrBatchNotFound = errors.New("batch not found") + // ErrNoSequencerForRollapp is returned when a sequencer is not found for the rollapp. + ErrNoSequencerForRollapp = errors.New("no sequencer for rollapp") ) diff --git a/settlement/events.go b/settlement/events.go index 8629304bf..dd43bd00d 100644 --- a/settlement/events.go +++ b/settlement/events.go @@ -3,6 +3,7 @@ package settlement import ( "fmt" + "github.com/dymensionxyz/dymint/types" tmpubsub "github.com/tendermint/tendermint/libs/pubsub" tmquery "github.com/tendermint/tendermint/libs/pubsub/query" ) @@ -16,6 +17,7 @@ const ( // Define the event types const ( EventNewSettlementBatchAccepted = "NewSettlementBatchAccepted" + EventSequencersListUpdated = "SequencersListUpdated" ) // EventDataNewSettlementBatchAccepted defines the structure of the event data for the EventNewSettlementBatchAccepted @@ -26,6 +28,12 @@ type EventDataNewSettlementBatchAccepted struct { StateIndex uint64 } +// EventDataSequencersListUpdated defines the structure of the event data for the EventSequencersListUpdated +type EventDataSequencersListUpdated struct { + // Sequencers is the list of sequencers + Sequencers []types.Sequencer +} + // Define queries var ( EventQueryNewSettlementBatchAccepted = QueryForEvent(EventNewSettlementBatchAccepted) diff --git a/settlement/mock/mock.go b/settlement/mock/mock.go index bde413648..6f8d12214 100644 --- a/settlement/mock/mock.go +++ b/settlement/mock/mock.go @@ -6,207 +6,237 @@ import ( "encoding/json" "errors" "sync/atomic" - "time" + "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" + rollapptypes "github.com/dymensionxyz/dymension/x/rollapp/types" "github.com/dymensionxyz/dymint/da" "github.com/dymensionxyz/dymint/log" "github.com/dymensionxyz/dymint/settlement" "github.com/dymensionxyz/dymint/store" - "github.com/dymensionxyz/dymint/testutil" "github.com/dymensionxyz/dymint/types" + "github.com/tendermint/tendermint/libs/pubsub" ) -const defaultBatchSize = 5 const kvStoreDBName = "settlement" var settlementKVPrefix = []byte{0} -var latestHeightKey = []byte("latestHeight") var slStateIndexKey = []byte("slStateIndex") -// SettlementLayerClient is intended only for usage in tests. +// SettlementLayerClient is an extension of the base settlement layer client +// for usage in tests and local development. type SettlementLayerClient struct { + *settlement.BaseLayerClient +} + +var _ settlement.LayerClient = (*SettlementLayerClient)(nil) + +// Init initializes the mock layer client. +func (m *SettlementLayerClient) Init(config []byte, pubsub *pubsub.Server, logger log.Logger, options ...settlement.Option) error { + HubClientMock, err := newHubClient(config, pubsub, logger) + if err != nil { + return err + } + baseOptions := []settlement.Option{ + settlement.WithHubClient(HubClientMock), + } + if options == nil { + options = baseOptions + } else { + options = append(baseOptions, options...) + } + m.BaseLayerClient = &settlement.BaseLayerClient{} + err = m.BaseLayerClient.Init(config, pubsub, logger, options...) + if err != nil { + return err + } + return nil +} + +// Config for the HubClient +type Config struct { + *settlement.Config + DBPath string `json:"db_path"` + RootDir string `json:"root_dir"` + store store.KVStore +} + +// HubClient implements The HubClient interface +type HubClient struct { + config *Config + slStateIndex uint64 logger log.Logger - settlementKV store.KVStore pubsub *pubsub.Server latestHeight uint64 - slStateIndex uint64 - config Config - ctx context.Context - cancel context.CancelFunc + settlementKV store.KVStore } -// Config for the SettlementLayerClient mock -type Config struct { - AutoUpdateBatches bool `json:"auto_update_batches"` - AutoUpdateBatchInterval time.Duration `json:"auto_update_batch_interval"` - BatchSize uint64 `json:"batch_size"` - LatestHeight uint64 `json:"latest_height"` - // The height at which the new batchSize started - BatchOffsetHeight uint64 `json:"batch_offset_height"` - DBPath string `json:"db_path"` - RootDir string `json:"root_dir"` - store store.KVStore +var _ settlement.HubClient = &HubClient{} + +// PostBatchResp is the response from saving the batch +type PostBatchResp struct { + err error } -var _ settlement.LayerClient = &SettlementLayerClient{} +// GetTxHash returns the tx hash +func (s PostBatchResp) GetTxHash() string { + if s.err != nil { + return "" + } + return "mock-hash" +} -// Init is called once. it initializes the struct members. -func (s *SettlementLayerClient) Init(config []byte, pubsub *pubsub.Server, logger log.Logger) error { - c, err := s.getConfig(config) - if err != nil { - return err +// GetCode returns the code +func (s PostBatchResp) GetCode() uint32 { + if s.err != nil { + return 1 } - s.config = *c - s.pubsub = pubsub - s.logger = logger - s.ctx, s.cancel = context.WithCancel(context.Background()) - s.settlementKV = store.NewPrefixKV(s.config.store, settlementKVPrefix) - b, err := s.settlementKV.Get(latestHeightKey) + return 0 +} + +var _ settlement.PostBatchResp = PostBatchResp{} + +func newHubClient(config []byte, pubsub *pubsub.Server, logger log.Logger) (*HubClient, error) { + latestHeight := uint64(0) + slStateIndex := uint64(0) + conf, err := getConfig(config) if err != nil { - s.latestHeight = 0 - } else { - s.latestHeight = binary.BigEndian.Uint64(b) + return nil, err } - b, err = s.settlementKV.Get(slStateIndexKey) - if err != nil { - s.slStateIndex = 0 - s.latestHeight = 0 - } else { - s.slStateIndex = binary.BigEndian.Uint64(b) + settlementKV := store.NewPrefixKV(conf.store, settlementKVPrefix) + b, err := settlementKV.Get(slStateIndexKey) + if err == nil { + slStateIndex = binary.BigEndian.Uint64(b) // Get the latest height from the stateIndex - var settlementBatch settlement.Batch - b, err := s.settlementKV.Get(getKey(s.slStateIndex)) + var settlementBatch rollapptypes.MsgUpdateState + b, err := settlementKV.Get(getKey(slStateIndex)) if err != nil { - return err + return nil, err } err = json.Unmarshal(b, &settlementBatch) if err != nil { - return errors.New("error unmarshalling batch") + return nil, errors.New("error unmarshalling batch") } - s.latestHeight = settlementBatch.EndHeight - + latestHeight = settlementBatch.StartHeight + settlementBatch.NumBlocks - 1 } - return nil + return &HubClient{ + config: conf, + logger: logger, + pubsub: pubsub, + latestHeight: latestHeight, + slStateIndex: slStateIndex, + settlementKV: settlementKV, + }, nil } -func (s *SettlementLayerClient) decodeConfig(config []byte) (*Config, error) { - var c Config - err := json.Unmarshal(config, &c) - return &c, err -} - -func (s *SettlementLayerClient) getConfig(config []byte) (*Config, error) { - var c *Config +func getConfig(config []byte) (*Config, error) { + var conf *Config if len(config) > 0 { var err error - c, err = s.decodeConfig(config) + conf, err = decodeConfig(config) if err != nil { return nil, err } - if c.BatchSize == 0 { - c.BatchSize = defaultBatchSize - } - if c.RootDir != "" && c.DBPath != "" { - c.store = store.NewDefaultKVStore(c.RootDir, c.DBPath, kvStoreDBName) + if conf.RootDir != "" && conf.DBPath != "" { + conf.store = store.NewDefaultKVStore(conf.RootDir, conf.DBPath, kvStoreDBName) } else { - c.store = store.NewDefaultInMemoryKVStore() + conf.store = store.NewDefaultInMemoryKVStore() } } else { - c = &Config{ - BatchSize: defaultBatchSize, - store: store.NewDefaultInMemoryKVStore(), + conf = &Config{ + store: store.NewDefaultInMemoryKVStore(), } } - return c, nil + return conf, nil } -// Start is called once, after init. If configured so, it will start producing batches every interval. -func (s *SettlementLayerClient) Start() error { - s.logger.Debug("Mock settlement Layer Client starting") - if s.config.AutoUpdateBatches { - go func() { - timer := time.NewTimer(s.config.AutoUpdateBatchInterval) - for { - select { - case <-s.ctx.Done(): - return - case <-timer.C: - s.updateSettlementWithBatch() - } - } - }() - } +func decodeConfig(config []byte) (*Config, error) { + var conf Config + err := json.Unmarshal(config, &conf) + return &conf, err +} + +// Start starts the mock client +func (c *HubClient) Start() error { return nil } -// Stop is called once, after Start. it cancels the auto batches created, if such was started. -func (s *SettlementLayerClient) Stop() error { - s.logger.Debug("Mock settlement Layer Client stopping") - s.cancel() +// Stop stops the mock client +func (c *HubClient) Stop() error { return nil } -func (s *SettlementLayerClient) updateSettlementWithBatch() { - s.logger.Debug("Mock settlement Layer Client updating with batch") - batch := s.createBatch(s.latestHeight+1, s.latestHeight+1+s.config.BatchSize) - daResult := &da.ResultSubmitBatch{ - BaseResult: da.BaseResult{ - DAHeight: batch.EndHeight, - }, +// PostBatch saves the batch to the kv store +func (c *HubClient) PostBatch(batch *types.Batch, daClient da.Client, daResult *da.ResultSubmitBatch) (settlement.PostBatchResp, error) { + settlementBatch := c.convertBatchtoSettlementBatch(batch, daClient, daResult) + err := c.saveBatch(settlementBatch) + if err != nil { + return PostBatchResp{err}, err } - s.SubmitBatch(&batch, da.Mock, daResult) + err = c.pubsub.PublishWithEvents(context.Background(), &settlement.EventDataNewSettlementBatchAccepted{EndHeight: settlementBatch.EndHeight}, map[string][]string{settlement.EventTypeKey: {settlement.EventNewSettlementBatchAccepted}}) + if err != nil { + c.logger.Error("error publishing event", "error", err) + } + return PostBatchResp{nil}, nil +} + +// GetLatestBatch returns the latest batch from the kv store +func (c *HubClient) GetLatestBatch(rollappID string) (*settlement.ResultRetrieveBatch, error) { + batchResult, err := c.GetBatchAtIndex(rollappID, atomic.LoadUint64(&c.slStateIndex)) + if err != nil { + return nil, err + } + return batchResult, nil } -func (s *SettlementLayerClient) createBatch(startHeight uint64, endHeight uint64) types.Batch { - s.logger.Debug("Creating batch for settlement layer", "start height", startHeight, "end height", endHeight) - blocks := testutil.GenerateBlocks(startHeight, endHeight-startHeight) - batch := types.Batch{ - StartHeight: startHeight, - EndHeight: endHeight, - Blocks: blocks, +// GetBatchAtIndex returns the batch at the given index +func (c *HubClient) GetBatchAtIndex(rollappID string, index uint64) (*settlement.ResultRetrieveBatch, error) { + batchResult, err := c.retrieveBatchAtStateIndex(index) + if err != nil { + return &settlement.ResultRetrieveBatch{ + BaseResult: settlement.BaseResult{Code: settlement.StatusError, Message: err.Error()}, + }, err } - return batch + return batchResult, nil } -// saveBatch saves the data to the kvstore -func (s *SettlementLayerClient) saveBatch(batch *settlement.Batch) error { - s.logger.Debug("Saving batch to settlement layer", "start height", +// GetSequencers returns a list of sequencers. Currently only returns a single sequencer +func (c *HubClient) GetSequencers(rollappID string) ([]*types.Sequencer, error) { + return []*types.Sequencer{ + { + PublicKey: secp256k1.GenPrivKey().PubKey(), + Status: types.Proposer, + }, + }, nil +} + +func (c *HubClient) saveBatch(batch *settlement.Batch) error { + c.logger.Debug("Saving batch to settlement layer", "start height", batch.StartHeight, "end height", batch.EndHeight) b, err := json.Marshal(batch) if err != nil { return err } // Save the batch to the next state index - slStateIndex := atomic.LoadUint64(&s.slStateIndex) - err = s.settlementKV.Set(getKey(slStateIndex+1), b) + slStateIndex := atomic.LoadUint64(&c.slStateIndex) + err = c.settlementKV.Set(getKey(slStateIndex+1), b) if err != nil { return err } // Save SL state index in memory and in store - atomic.StoreUint64(&s.slStateIndex, slStateIndex+1) + atomic.StoreUint64(&c.slStateIndex, slStateIndex+1) b = make([]byte, 8) binary.BigEndian.PutUint64(b, slStateIndex+1) - err = s.settlementKV.Set(slStateIndexKey, b) + err = c.settlementKV.Set(slStateIndexKey, b) if err != nil { return err } // Save latest height in memory and in store - atomic.StoreUint64(&s.latestHeight, batch.EndHeight) + atomic.StoreUint64(&c.latestHeight, batch.EndHeight) return nil } -func (s *SettlementLayerClient) validateBatch(batch *types.Batch) error { - if batch.StartHeight != s.latestHeight+1 { - return errors.New("batch start height must be last height + 1") - } - if batch.EndHeight < batch.StartHeight { - return errors.New("batch end height must be greater or equal to start height") - } - return nil -} - -func (s *SettlementLayerClient) convertBatchtoSettlementBatch(batch *types.Batch, daClient da.Client, daResult *da.ResultSubmitBatch) *settlement.Batch { +func (c *HubClient) convertBatchtoSettlementBatch(batch *types.Batch, daClient da.Client, daResult *da.ResultSubmitBatch) *settlement.Batch { settlementBatch := &settlement.Batch{ StartHeight: batch.StartHeight, EndHeight: batch.EndHeight, @@ -223,40 +253,9 @@ func (s *SettlementLayerClient) convertBatchtoSettlementBatch(batch *types.Batch return settlementBatch } -// SubmitBatch submits the batch to the settlement layer. This should create a transaction which (potentially) -// triggers a state transition in the settlement layer. -func (s *SettlementLayerClient) SubmitBatch(batch *types.Batch, daClient da.Client, daResult *da.ResultSubmitBatch) *settlement.ResultSubmitBatch { - s.logger.Debug("Submitting batch to settlement layer", "start height", batch.StartHeight, "end height", batch.EndHeight) - // validate batch - err := s.validateBatch(batch) - if err != nil { - return &settlement.ResultSubmitBatch{ - BaseResult: settlement.BaseResult{Code: settlement.StatusError, Message: err.Error()}, - } - } - // Build the result to save in the settlement layer. - settlementBatch := s.convertBatchtoSettlementBatch(batch, daClient, daResult) - // Save to the settlement layer - err = s.saveBatch(settlementBatch) - if err != nil { - s.logger.Error("Error saving app hash to kv store", "error", err) - return &settlement.ResultSubmitBatch{ - BaseResult: settlement.BaseResult{Code: settlement.StatusError, Message: err.Error()}, - } - } - // Emit an event - err = s.pubsub.PublishWithEvents(context.Background(), &settlement.EventDataNewSettlementBatchAccepted{EndHeight: batch.EndHeight}, map[string][]string{settlement.EventTypeKey: {settlement.EventNewSettlementBatchAccepted}}) - if err != nil { - s.logger.Error("Error publishing event", "error", err) - } - return &settlement.ResultSubmitBatch{ - BaseResult: settlement.BaseResult{Code: settlement.StatusSuccess, StateIndex: atomic.LoadUint64(&s.slStateIndex)}, - } -} - -func (s *SettlementLayerClient) retrieveBatchAtStateIndex(slStateIndex uint64) (*settlement.ResultRetrieveBatch, error) { - b, err := s.settlementKV.Get(getKey(slStateIndex)) - s.logger.Debug("Retrieving batch from settlement layer", "SL state index", slStateIndex) +func (c *HubClient) retrieveBatchAtStateIndex(slStateIndex uint64) (*settlement.ResultRetrieveBatch, error) { + b, err := c.settlementKV.Get(getKey(slStateIndex)) + c.logger.Debug("Retrieving batch from settlement layer", "SL state index", slStateIndex) if err != nil { return nil, settlement.ErrBatchNotFound } @@ -272,33 +271,6 @@ func (s *SettlementLayerClient) retrieveBatchAtStateIndex(slStateIndex uint64) ( return &batchResult, nil } -// RetrieveBatch Gets the batch which contains the given stateIndex. Empty stateIndex returns the latest batch. -func (s *SettlementLayerClient) RetrieveBatch(slStateIndex ...uint64) (*settlement.ResultRetrieveBatch, error) { - var stateIndex uint64 - if len(slStateIndex) == 0 { - stateIndex = atomic.LoadUint64(&s.slStateIndex) - s.logger.Debug("Getting latest batch from settlement layer", "state index", stateIndex) - } else if len(slStateIndex) == 1 { - s.logger.Debug("Getting batch from settlement layer for SL state index", slStateIndex) - stateIndex = slStateIndex[0] - } else { - return &settlement.ResultRetrieveBatch{}, errors.New("height len must be 1 or 0") - } - // Get the batch from the settlement layer - batchResult, err := s.retrieveBatchAtStateIndex(stateIndex) - if err != nil { - return &settlement.ResultRetrieveBatch{ - BaseResult: settlement.BaseResult{Code: settlement.StatusError, Message: err.Error()}, - }, err - } - return batchResult, nil -} - -// GetLatestFinalizedStateHeight returns the latest-finalized-state height of the active rollapp -func (s *SettlementLayerClient) GetLatestFinalizedStateHeight(_ string) (int64, error) { - return int64(s.latestHeight), nil -} - func getKey(key uint64) []byte { b := make([]byte, 8) binary.BigEndian.PutUint64(b, key) diff --git a/settlement/registry/registry.go b/settlement/registry/registry.go index 2a5d03cab..bac54b569 100644 --- a/settlement/registry/registry.go +++ b/settlement/registry/registry.go @@ -2,6 +2,7 @@ package registry import ( "github.com/dymensionxyz/dymint/settlement" + "github.com/dymensionxyz/dymint/settlement/dymension" "github.com/dymensionxyz/dymint/settlement/mock" ) @@ -9,16 +10,16 @@ import ( type Client string const ( - // ClientMock is a mock client for the settlement layer - ClientMock Client = "mock" + // Mock is a mock client for the settlement layer + Mock Client = "mock" // Dymension is a client for interacting with dymension settlement layer Dymension Client = "dymension" ) // A central registry for all Settlement Layer Clients var clients = map[Client]func() settlement.LayerClient{ - ClientMock: func() settlement.LayerClient { return &mock.SettlementLayerClient{} }, - Dymension: func() settlement.LayerClient { return &settlement.DymensionLayerClient{} }, + Mock: func() settlement.LayerClient { return &mock.SettlementLayerClient{} }, + Dymension: func() settlement.LayerClient { return &dymension.LayerClient{} }, } // GetClient returns client identified by name. diff --git a/settlement/registry/registry_test.go b/settlement/registry/registry_test.go index 38e129ffa..04ae1b856 100644 --- a/settlement/registry/registry_test.go +++ b/settlement/registry/registry_test.go @@ -10,7 +10,7 @@ import ( func TestRegistery(t *testing.T) { assert := assert.New(t) - expected := []registry.Client{registry.ClientMock, registry.Dymension} + expected := []registry.Client{registry.Mock, registry.Dymension} actual := registry.RegisteredClients() assert.ElementsMatch(expected, actual) diff --git a/settlement/settlement.go b/settlement/settlement.go index 882b332fb..f26ef5e3a 100644 --- a/settlement/settlement.go +++ b/settlement/settlement.go @@ -39,13 +39,15 @@ type DAMetaData struct { Client da.Client } -func (d *DAMetaData) toPath() string { +// ToPath converts a DAMetaData to a path. +func (d *DAMetaData) ToPath() string { // convert uint64 to string path := []string{string(d.Client), ".", strconv.FormatUint(d.Height, 10)} return strings.Join(path, "") } -func (d *DAMetaData) fromPath(path string) (*DAMetaData, error) { +// FromPath parses a path to a DAMetaData. +func (d *DAMetaData) FromPath(path string) (*DAMetaData, error) { pathParts := strings.FieldsFunc(path, func(r rune) bool { return r == '.' }) height, err := strconv.ParseUint(pathParts[1], 10, 64) if err != nil { @@ -76,17 +78,20 @@ type ResultSubmitBatch struct { BaseResult } -// ResultRetrieveBatch contains information returned from settlement layer after batch retrieval. +// ResultRetrieveBatch contains information returned from settlement layer after batch retrieva type ResultRetrieveBatch struct { BaseResult *Batch } +// Option is a function that sets a parameter on the settlement layer. +type Option func(LayerClient) + // LayerClient defines generic interface for Settlement layer interaction. type LayerClient interface { // Init is called once for the client initialization - Init(config []byte, pubsub *pubsub.Server, logger log.Logger) error + Init(config []byte, pubsub *pubsub.Server, logger log.Logger, options ...Option) error // Start is called once, after Init. It's implementation should start the client service. Start() error @@ -101,8 +106,29 @@ type LayerClient interface { // RetrieveBatch Gets the batch which contains the given height. Empty height returns the latest batch. RetrieveBatch(stateIndex ...uint64) (*ResultRetrieveBatch, error) - // GetLatestFinalizedStateHeight returns the latest-finalized-state height of the active rollapp - GetLatestFinalizedStateHeight(rollapID string) (int64, error) + // GetSequencersList returns the list of the sequencers for this chain. + GetSequencersList() []*types.Sequencer + + // GetProposer returns the current proposer for this chain. + GetProposer() *types.Sequencer +} + +// HubClient is an helper interface for a more granualr interaction with the hub. +// Implementing a new settlement layer client basically requires embedding the base client +// and implementing the helper interfaces. +type HubClient interface { + Start() error + Stop() error + PostBatch(batch *types.Batch, daClient da.Client, daResult *da.ResultSubmitBatch) (PostBatchResp, error) + GetLatestBatch(rollappID string) (*ResultRetrieveBatch, error) + GetBatchAtIndex(rollappID string, index uint64) (*ResultRetrieveBatch, error) + GetSequencers(rollappID string) ([]*types.Sequencer, error) +} - // TODO(omritoptix): Support getting multiple batches and pagination +// PostBatchResp is an helper interface for a more granualr interaction with the hub. +// Implementing a new settlement layer client basically requires embedding the base client +// and implementing the helper interfaces. +type PostBatchResp interface { + GetCode() uint32 + GetTxHash() string } diff --git a/settlement/settlement_test.go b/settlement/settlement_test.go index bbcdc5d26..f3fe0c264 100644 --- a/settlement/settlement_test.go +++ b/settlement/settlement_test.go @@ -1,90 +1,48 @@ package settlement_test import ( - "encoding/json" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/tendermint/tendermint/libs/pubsub" + tmtypes "github.com/tendermint/tendermint/types" + "github.com/cosmos/cosmos-sdk/crypto/keys/ed25519" "github.com/dymensionxyz/dymint/da" "github.com/dymensionxyz/dymint/log/test" + mocks "github.com/dymensionxyz/dymint/mocks/settlement" "github.com/dymensionxyz/dymint/settlement" - "github.com/dymensionxyz/dymint/settlement/mock" "github.com/dymensionxyz/dymint/settlement/registry" "github.com/dymensionxyz/dymint/testutil" "github.com/dymensionxyz/dymint/types" + tsmock "github.com/stretchr/testify/mock" + ce "github.com/tendermint/tendermint/crypto/encoding" + pc "github.com/tendermint/tendermint/proto/tendermint/crypto" ) const batchSize = 5 -func TesClientsLifeCycle(t *testing.T) { - - for _, settlement := range registry.RegisteredClients() { - t.Run(string(settlement), func(t *testing.T) { - doTestLifecycle(t, registry.GetClient(settlement)) - }) - } -} - -func doTestLifecycle(t *testing.T, settlementClient settlement.LayerClient) { +func TestLifecycle(t *testing.T) { + client := registry.GetClient(registry.Mock) require := require.New(t) pubsubServer := pubsub.NewServer() pubsubServer.Start() - err := settlementClient.Init([]byte{}, pubsubServer, test.NewLogger(t)) + err := client.Init([]byte{}, pubsubServer, test.NewLogger(t)) require.NoError(err) - err = settlementClient.Start() - require.NoError(err) - - err = settlementClient.Stop() - require.NoError(err) -} - -func TestSubmitAndRetrieve(t *testing.T) { - for _, settlement := range registry.RegisteredClients() { - t.Run(string(settlement), func(t *testing.T) { - //TODO(omritoptix): Currently not testing dymension SL as part of this tests. - if settlement == registry.ClientMock { - doTestSubmitAndRetrieve(t, registry.GetClient(settlement)) - doTestInvalidSubmit(t, registry.GetClient(settlement)) - } - }) - } -} - -func getConfForClient(settlementlc settlement.LayerClient) []byte { - var config interface{} - conf := []byte{} - if _, ok := settlementlc.(*mock.SettlementLayerClient); ok { - config = mock.Config{ - AutoUpdateBatches: false, - BatchSize: batchSize, - } - } - conf, _ = json.Marshal(config) - - return conf -} - -func initClient(t *testing.T, settlementlc settlement.LayerClient) { - require := require.New(t) - conf := getConfForClient(settlementlc) - - pubsubServer := pubsub.NewServer() - pubsubServer.Start() - err := settlementlc.Init(conf, pubsubServer, test.NewLogger(t)) + err = client.Start() require.NoError(err) - err = settlementlc.Start() + err = client.Stop() require.NoError(err) } -func doTestInvalidSubmit(t *testing.T, settlementlc settlement.LayerClient) { +func TestInvalidSubmit(t *testing.T) { assert := assert.New(t) - initClient(t, settlementlc) + settlementClient := registry.GetClient(registry.Mock) + initClient(t, settlementClient) // Create cases cases := []struct { @@ -108,25 +66,27 @@ func doTestInvalidSubmit(t *testing.T, settlementlc settlement.LayerClient) { DAHeight: c.endHeight, }, } - resultSubmitBatch := settlementlc.SubmitBatch(batch, da.Mock, daResult) + resultSubmitBatch := settlementClient.SubmitBatch(batch, da.Mock, daResult) assert.Equal(resultSubmitBatch.Code, c.status) } } -func doTestSubmitAndRetrieve(t *testing.T, settlementlc settlement.LayerClient) { +func TestSubmitAndRetrieve(t *testing.T) { require := require.New(t) assert := assert.New(t) - initClient(t, settlementlc) + settlementClient := registry.GetClient(registry.Mock) + + initClient(t, settlementClient) - // Get settlement lastest batch and check there is an error as we haven't written anything yet. - _, err := settlementlc.RetrieveBatch() + // Get settlement lastest batch and check if there is an error as we haven't written anything yet. + _, err := settlementClient.RetrieveBatch() require.Error(err) assert.Equal(err, settlement.ErrBatchNotFound) // Get nonexisting stateIndex from the settlement layer - _, err = settlementlc.RetrieveBatch(uint64(100)) + _, err = settlementClient.RetrieveBatch(uint64(100)) require.Error(err) assert.Equal(err, settlement.ErrBatchNotFound) @@ -144,23 +104,100 @@ func doTestSubmitAndRetrieve(t *testing.T, settlementlc settlement.LayerClient) DAHeight: batch.EndHeight, }, } - resultSubmitBatch := settlementlc.SubmitBatch(batch, da.Mock, daResult) + resultSubmitBatch := settlementClient.SubmitBatch(batch, da.Mock, daResult) assert.Equal(resultSubmitBatch.Code, settlement.StatusSuccess) } // Retrieve the latest batch and make sure it matches latest batch submitted - lastestBatch, err := settlementlc.RetrieveBatch() + lastestBatch, err := settlementClient.RetrieveBatch() require.NoError(err) assert.Equal(batch.EndHeight, lastestBatch.EndHeight) // Retrieve one batch before last - batchResult, err := settlementlc.RetrieveBatch(lastestBatch.StateIndex - 1) + batchResult, err := settlementClient.RetrieveBatch(lastestBatch.StateIndex - 1) require.NoError(err) middleOfBatchHeight := uint64(numBatches-1)*(batchSize) - (batchSize / 2) assert.LessOrEqual(batchResult.StartHeight, middleOfBatchHeight) assert.GreaterOrEqual(batchResult.EndHeight, middleOfBatchHeight) - result, err := settlementlc.GetLatestFinalizedStateHeight("") +} + +func TestGetSequencersEmptyList(t *testing.T) { + settlementClient := registry.GetClient(registry.Mock) + hubClientMock := mocks.NewHubClient(t) + hubClientMock.On("GetLatestBatch", tsmock.Anything).Return(nil, settlement.ErrBatchNotFound) + hubClientMock.On("GetSequencers", tsmock.Anything, tsmock.Anything).Return(nil, settlement.ErrNoSequencerForRollapp) + options := []settlement.Option{ + settlement.WithHubClient(hubClientMock), + } + assert.Panics(t, func() { initClient(t, settlementClient, options...) }) + +} + +func TestGetSequencers(t *testing.T) { + hubClientMock := mocks.NewHubClient(t) + hubClientMock.On("Start", tsmock.Anything).Return(nil) + hubClientMock.On("GetLatestBatch", tsmock.Anything).Return(nil, settlement.ErrBatchNotFound) + // Mock a sequencer response by the sequencerByRollapp query + totalSequencers := 5 + sequencers, proposer := generateSequencers(totalSequencers) + hubClientMock.On("GetSequencers", tsmock.Anything, tsmock.Anything).Return(sequencers, nil) + options := []settlement.Option{ + settlement.WithHubClient(hubClientMock), + } + settlementClient := registry.GetClient(registry.Mock) + initClient(t, settlementClient, options...) + + sequencersList := settlementClient.GetSequencersList() + + assert.Len(t, sequencersList, len(sequencers)) + assert.Equal(t, settlementClient.GetProposer().PublicKey, proposer.PublicKey) + // Validate the amount of inactive sequencers + var inactiveSequencerAmount int + for _, sequencer := range sequencersList { + if sequencer.Status == types.Inactive { + inactiveSequencerAmount += 1 + } + } + assert.Equal(t, inactiveSequencerAmount, totalSequencers-1) + +} + +/* -------------------------------------------------------------------------- */ +/* Utils */ +/* -------------------------------------------------------------------------- */ + +func initClient(t *testing.T, settlementlc settlement.LayerClient, options ...settlement.Option) { + require := require.New(t) + + pubsubServer := pubsub.NewServer() + pubsubServer.Start() + err := settlementlc.Init([]byte{}, pubsubServer, test.NewLogger(t), options...) require.NoError(err) - assert.Equal(result, int64(numBatches*batchSize)) + + err = settlementlc.Start() + require.NoError(err) +} + +func generateProtoPubKey(t *testing.T) pc.PublicKey { + pubKey := tmtypes.NewMockPV().PrivKey.PubKey() + protoPubKey, err := ce.PubKeyToProto(pubKey) + require.NoError(t, err) + return protoPubKey +} + +func generateSequencers(count int) ([]*types.Sequencer, *types.Sequencer) { + sequencers := make([]*types.Sequencer, count) + proposer := &types.Sequencer{ + PublicKey: ed25519.GenPrivKey().PubKey(), + Status: types.Proposer, + } + sequencers[0] = proposer + for i := 1; i < count; i++ { + sequencers[i] = &types.Sequencer{ + PublicKey: ed25519.GenPrivKey().PubKey(), + Status: types.Inactive, + } + } + return sequencers, proposer } diff --git a/tests.mk b/tests.mk index b74086ce9..ecaf75d85 100644 --- a/tests.mk +++ b/tests.mk @@ -8,5 +8,5 @@ BINDIR ?= $(GOPATH)/bin ### go tests test: @echo "--> Running go test" - @go test $(PACKAGES) + @go test $(PACKAGES) -race .PHONY: test diff --git a/types/sequencer.go b/types/sequencer.go new file mode 100644 index 000000000..bedaabd1a --- /dev/null +++ b/types/sequencer.go @@ -0,0 +1,21 @@ +package types + +import crypto "github.com/cosmos/cosmos-sdk/crypto/types" + +// SequencerStatus defines the operating status of a sequencer +type SequencerStatus int32 + +const ( + // Proposer defines a sequencer that is currently the proposer + Proposer SequencerStatus = iota + // Inactive defines a sequencer that is currently inactive + Inactive +) + +// Sequencer represents a sequencer of the rollapp +type Sequencer struct { + // PublicKey is the public key of the sequencer + PublicKey crypto.PubKey + // Status is status of the sequencer + Status SequencerStatus +}