From 55db42e8b77ebc88fd7fc14d7eb5e80f54469441 Mon Sep 17 00:00:00 2001 From: Guilhem Fanton <8671905+gfanton@users.noreply.github.com> Date: Wed, 13 Mar 2024 16:15:53 +0100 Subject: [PATCH] feat(gnodev): add more configuration flags for server usage (#1622) --- contribs/gnodev/main.go | 114 +++++++++++++-- contribs/gnodev/pkg/dev/node.go | 200 ++++++++++++-------------- contribs/gnodev/pkg/dev/pkgs.go | 82 +++++++++++ gno.land/pkg/gnoland/node_inmemory.go | 2 +- 4 files changed, 274 insertions(+), 124 deletions(-) create mode 100644 contribs/gnodev/pkg/dev/pkgs.go diff --git a/contribs/gnodev/main.go b/contribs/gnodev/main.go index 0937241695f..df68bf54bec 100644 --- a/contribs/gnodev/main.go +++ b/contribs/gnodev/main.go @@ -5,9 +5,11 @@ import ( "flag" "fmt" "io" + "net" "net/http" "os" "path/filepath" + "strings" "time" "github.com/fsnotify/fsnotify" @@ -34,15 +36,30 @@ const ( ) type devCfg struct { - webListenerAddr string - minimal bool - verbose bool - hotreload bool - noWatch bool + webListenerAddr string + nodeRPCListenerAddr string + nodeP2PListenerAddr string + nodeProxyAppListenerAddr string + + minimal bool + verbose bool + hotreload bool + noWatch bool + noReplay bool + maxGas int64 + chainId string } var defaultDevOptions = &devCfg{ - webListenerAddr: "127.0.0.1:8888", + chainId: "dev", + maxGas: 10_000_000_000, + webListenerAddr: "127.0.0.1:8888", + nodeRPCListenerAddr: "127.0.0.1:36657", + + // As we have no reason to configure this yet, set this to random port + // to avoid potential conflict with other app + nodeP2PListenerAddr: "tcp://127.0.0.1:0", + nodeProxyAppListenerAddr: "tcp://127.0.0.1:0", } func main() { @@ -74,10 +91,17 @@ func (c *devCfg) RegisterFlags(fs *flag.FlagSet) { "web server listening address", ) + fs.StringVar( + &c.nodeRPCListenerAddr, + "node-rpc-listener", + defaultDevOptions.nodeRPCListenerAddr, + "gnoland rpc node listening address", + ) + fs.BoolVar( &c.minimal, "minimal", - defaultDevOptions.verbose, + defaultDevOptions.minimal, "do not load packages from examples directory", ) @@ -88,12 +112,33 @@ func (c *devCfg) RegisterFlags(fs *flag.FlagSet) { "verbose output when deving", ) + fs.StringVar( + &c.chainId, + "chain-id", + defaultDevOptions.chainId, + "set node ChainID", + ) + fs.BoolVar( &c.noWatch, "no-watch", defaultDevOptions.noWatch, "do not watch for files change", ) + + fs.BoolVar( + &c.noReplay, + "no-replay", + defaultDevOptions.noReplay, + "do not replay previous transactions on reload", + ) + + fs.Int64Var( + &c.maxGas, + "max-gas", + defaultDevOptions.maxGas, + "set the maximum gas by block", + ) } func execDev(cfg *devCfg, args []string, io commands.IO) error { @@ -133,7 +178,7 @@ func execDev(cfg *devCfg, args []string, io commands.IO) error { // Setup Dev Node // XXX: find a good way to export or display node logs - devNode, err := setupDevNode(ctx, emitterServer, rt, pkgpaths) + devNode, err := setupDevNode(ctx, cfg, emitterServer, rt, pkgpaths) if err != nil { return err } @@ -141,7 +186,7 @@ func execDev(cfg *devCfg, args []string, io commands.IO) error { rt.Taskf(NodeLogName, "Listener: %s\n", devNode.GetRemoteAddress()) rt.Taskf(NodeLogName, "Default Address: %s\n", gnodev.DefaultCreator.String()) - rt.Taskf(NodeLogName, "Chain ID: %s\n", devNode.Config().ChainID()) + rt.Taskf(NodeLogName, "Chain ID: %s\n", cfg.chainId) // Create server mux := http.NewServeMux() @@ -317,18 +362,40 @@ func setupRawTerm(io commands.IO) (rt *rawterm.RawTerm, restore func() error, er } // setupDevNode initializes and returns a new DevNode. -func setupDevNode(ctx context.Context, emitter emitter.Emitter, rt *rawterm.RawTerm, pkgspath []string) (*gnodev.Node, error) { +func setupDevNode( + ctx context.Context, + cfg *devCfg, + remitter emitter.Emitter, + rt *rawterm.RawTerm, + pkgspath []string, +) (*gnodev.Node, error) { nodeOut := rt.NamespacedWriter("Node") zapLogger := NewZapLogger(nodeOut, zapcore.ErrorLevel) - return gnodev.NewDevNode(ctx, log.ZapLoggerToSlog(zapLogger), emitter, pkgspath) + + gnoroot := gnoenv.RootDir() + + // configure gnoland node + config := gnodev.DefaultNodeConfig(gnoroot) + config.PackagesPathList = pkgspath + config.TMConfig.RPC.ListenAddress = resolveUnixOrTCPAddr(cfg.nodeRPCListenerAddr) + config.NoReplay = cfg.noReplay + config.SkipFailingGenesisTxs = true + config.MaxGasPerBlock = cfg.maxGas + config.ChainID = cfg.chainId + + // other listeners + config.TMConfig.P2P.ListenAddress = defaultDevOptions.nodeP2PListenerAddr + config.TMConfig.ProxyApp = defaultDevOptions.nodeProxyAppListenerAddr + + return gnodev.NewDevNode(ctx, log.ZapLoggerToSlog(zapLogger), remitter, config) } // setupGnowebServer initializes and starts the Gnoweb server. func setupGnoWebServer(cfg *devCfg, dnode *gnodev.Node, rt *rawterm.RawTerm) http.Handler { webConfig := gnoweb.NewDefaultConfig() webConfig.RemoteAddr = dnode.GetRemoteAddress() - webConfig.HelpChainID = dnode.Config().ChainID() webConfig.HelpRemote = dnode.GetRemoteAddress() + webConfig.HelpChainID = cfg.chainId zapLogger := NewZapLogger(rt.NamespacedWriter("GnoWeb"), zapcore.DebugLevel) app := gnoweb.MakeApp(log.ZapLoggerToSlog(zapLogger), webConfig) @@ -379,6 +446,29 @@ func checkForError(w io.Writer, err error) { fmt.Fprintln(w, "[DONE]") } +func resolveUnixOrTCPAddr(in string) (out string) { + var err error + var addr net.Addr + + if strings.HasPrefix(in, "unix://") { + in = strings.TrimPrefix(in, "unix://") + if addr, err := net.ResolveUnixAddr("unix", in); err == nil { + return fmt.Sprintf("%s://%s", addr.Network(), addr.String()) + } + + err = fmt.Errorf("unable to resolve unix address `unix://%s`: %w", in, err) + } else { // don't bother to checking prefix + in = strings.TrimPrefix(in, "tcp://") + if addr, err = net.ResolveTCPAddr("tcp", in); err == nil { + return fmt.Sprintf("%s://%s", addr.Network(), addr.String()) + } + + err = fmt.Errorf("unable to resolve tcp address `tcp://%s`: %w", in, err) + } + + panic(err) +} + // NewZapLogger creates a zap logger with a console encoder for development use. func NewZapLogger(w io.Writer, level zapcore.Level) *zap.Logger { // Build encoder config diff --git a/contribs/gnodev/pkg/dev/node.go b/contribs/gnodev/pkg/dev/node.go index 4decc806638..23ac66a5f9a 100644 --- a/contribs/gnodev/pkg/dev/node.go +++ b/contribs/gnodev/pkg/dev/node.go @@ -9,11 +9,9 @@ import ( "github.com/gnolang/gno/contribs/gnodev/pkg/events" "github.com/gnolang/gno/gno.land/pkg/gnoland" "github.com/gnolang/gno/gno.land/pkg/integration" - vmm "github.com/gnolang/gno/gno.land/pkg/sdk/vm" - "github.com/gnolang/gno/gnovm/pkg/gnoenv" - gno "github.com/gnolang/gno/gnovm/pkg/gnolang" "github.com/gnolang/gno/gnovm/pkg/gnomod" "github.com/gnolang/gno/tm2/pkg/amino" + tmcfg "github.com/gnolang/gno/tm2/pkg/bft/config" "github.com/gnolang/gno/tm2/pkg/bft/node" "github.com/gnolang/gno/tm2/pkg/bft/rpc/client" bft "github.com/gnolang/gno/tm2/pkg/bft/types" @@ -24,16 +22,38 @@ import ( // restore "github.com/gnolang/tx-archive/restore/client" ) -const gnoDevChainID = "tendermint_test" // XXX: this is hardcoded and cannot be change bellow +type NodeConfig struct { + PackagesPathList []string + TMConfig *tmcfg.Config + SkipFailingGenesisTxs bool + NoReplay bool + MaxGasPerBlock int64 + ChainID string +} + +func DefaultNodeConfig(rootdir string) *NodeConfig { + tmc := gnoland.NewDefaultTMConfig(rootdir) + tmc.Consensus.SkipTimeoutCommit = false // avoid time drifting, see issue #1507 + + return &NodeConfig{ + ChainID: tmc.ChainID(), + PackagesPathList: []string{}, + TMConfig: tmc, + SkipFailingGenesisTxs: true, + MaxGasPerBlock: 10_000_000_000, + } +} // Node is not thread safe type Node struct { *node.Node + config *NodeConfig emitter emitter.Emitter client client.Client logger *slog.Logger pkgs PkgsMap // path -> pkg + // keep track of number of loaded package to be able to skip them on restore loadedPackages int } @@ -49,8 +69,8 @@ var ( } ) -func NewDevNode(ctx context.Context, logger *slog.Logger, emitter emitter.Emitter, pkgslist []string) (*Node, error) { - mpkgs, err := newPkgsMap(pkgslist) +func NewDevNode(ctx context.Context, logger *slog.Logger, emitter emitter.Emitter, cfg *NodeConfig) (*Node, error) { + mpkgs, err := newPkgsMap(cfg.PackagesPathList) if err != nil { return nil, fmt.Errorf("unable map pkgs list: %w", err) } @@ -66,20 +86,20 @@ func NewDevNode(ctx context.Context, logger *slog.Logger, emitter emitter.Emitte Txs: pkgsTxs, } - node, err := initializeNode(ctx, logger, emitter, genesis) - if err != nil { - return nil, fmt.Errorf("unable to initialize the node: %w", err) - } - - return &Node{ - Node: node, - + devnode := &Node{ + config: cfg, emitter: emitter, client: client.NewLocal(), pkgs: mpkgs, logger: logger, loadedPackages: len(pkgsTxs), - }, nil + } + + if err := devnode.reset(ctx, genesis); err != nil { + return nil, fmt.Errorf("unable to initialize the node: %w", err) + } + + return devnode, nil } func (d *Node) getLatestBlockNumber() uint64 { @@ -125,10 +145,8 @@ func (d *Node) UpdatePackages(paths ...string) error { // effectively ignoring the current state. func (d *Node) Reset(ctx context.Context) error { // Stop the node if it's currently running. - if d.Node.IsRunning() { - if err := d.Node.Stop(); err != nil { - return fmt.Errorf("unable to stop the node: %w", err) - } + if err := d.stopIfRunning(); err != nil { + return fmt.Errorf("unable to stop the node: %w", err) } // Generate a new genesis state based on the current packages @@ -143,12 +161,11 @@ func (d *Node) Reset(ctx context.Context) error { } // Reset the node with the new genesis state. - node, err := initializeNode(ctx, d.logger, d.emitter, genesis) + err = d.reset(ctx, genesis) if err != nil { return fmt.Errorf("unable to initialize a new node: %w", err) } - d.Node = node d.emitter.Emit(&events.Reset{}) return nil } @@ -173,6 +190,12 @@ func (d *Node) ReloadAll(ctx context.Context) error { // If any transaction, including 'addpkg', fails, it will be ignored. // Use 'Reset' to completely reset the node's state in case of persistent errors. func (d *Node) Reload(ctx context.Context) error { + if d.config.NoReplay { + // If NoReplay is true, reload as the same effect as reset + d.logger.Warn("replay disable") + return d.Reset(ctx) + } + // Get current blockstore state state, err := d.getBlockStoreState(ctx) if err != nil { @@ -180,10 +203,8 @@ func (d *Node) Reload(ctx context.Context) error { } // Stop the node if it's currently running. - if d.Node.IsRunning() { - if err := d.Node.Stop(); err != nil { - return fmt.Errorf("unable to stop the node: %w", err) - } + if err := d.stopIfRunning(); err != nil { + return fmt.Errorf("unable to stop the node: %w", err) } // Load genesis packages @@ -199,15 +220,10 @@ func (d *Node) Reload(ctx context.Context) error { } // Reset the node with the new genesis state. - node, err := initializeNode(ctx, d.logger, d.emitter, genesis) - if err != nil { - return fmt.Errorf("unable to initialize a new node: %w", err) - } - + err = d.reset(ctx, genesis) d.logger.Info("reload done", "pkgs", len(pkgsTxs), "state applied", len(state)) // Update node infos - d.Node = node d.loadedPackages = len(pkgsTxs) d.emitter.Emit(&events.Reload{}) @@ -296,85 +312,21 @@ func (n *Node) getBlockStoreState(ctx context.Context) ([]std.Tx, error) { return state, nil } -type PkgsMap map[string]gnomod.Pkg - -func newPkgsMap(paths []string) (PkgsMap, error) { - pkgs := make(map[string]gnomod.Pkg) - for _, path := range paths { - // list all packages from target path - pkgslist, err := gnomod.ListPkgs(path) - if err != nil { - return nil, fmt.Errorf("listing gno packages: %w", err) - } - - for _, pkg := range pkgslist { - if pkg.Dir == "" { - continue - } - - if _, ok := pkgs[pkg.Dir]; ok { - continue // skip - } - pkgs[pkg.Dir] = pkg - } - } - - // Filter out draft packages. - return pkgs, nil -} - -func (pm PkgsMap) toList() gnomod.PkgList { - list := make([]gnomod.Pkg, 0, len(pm)) - for _, pkg := range pm { - list = append(list, pkg) - } - return list -} - -func (pm PkgsMap) Load(creator bft.Address, fee std.Fee, deposit std.Coins) ([]std.Tx, error) { - pkgs := pm.toList() - - sorted, err := pkgs.Sort() - if err != nil { - return nil, fmt.Errorf("unable to sort pkgs: %w", err) - } - - nonDraft := sorted.GetNonDraftPkgs() - txs := []std.Tx{} - for _, pkg := range nonDraft { - // Open files in directory as MemPackage. - memPkg := gno.ReadMemPackage(pkg.Dir, pkg.Name) - if err := memPkg.Validate(); err != nil { - return nil, fmt.Errorf("invalid package: %w", err) - } - - // Create transaction - tx := std.Tx{ - Fee: fee, - Msgs: []std.Msg{ - vmm.MsgAddPackage{ - Creator: creator, - Package: memPkg, - Deposit: deposit, - }, - }, +func (n *Node) stopIfRunning() error { + if n.Node != nil && n.Node.IsRunning() { + if err := n.Node.Stop(); err != nil { + return fmt.Errorf("unable to stop the node: %w", err) } - - tx.Signatures = make([]std.Signature, len(tx.GetSigners())) - txs = append(txs, tx) } - return txs, nil + return nil } -func initializeNode(ctx context.Context, logger *slog.Logger, emitter emitter.Emitter, genesis gnoland.GnoGenesisState) (*node.Node, error) { - rootdir := gnoenv.RootDir() - +func (n *Node) reset(ctx context.Context, genesis gnoland.GnoGenesisState) error { // Setup node config - nodeConfig := gnoland.NewDefaultInMemoryNodeConfig(rootdir) - nodeConfig.SkipFailingGenesisTxs = true - nodeConfig.TMConfig.Consensus.SkipTimeoutCommit = false // avoid time drifting, see issue #1507 - nodeConfig.Genesis.AppState = genesis + nodeConfig := newNodeConfig(n.config.TMConfig, n.config.ChainID, genesis) + nodeConfig.SkipFailingGenesisTxs = n.config.SkipFailingGenesisTxs + nodeConfig.Genesis.ConsensusParams.Block.MaxGas = n.config.MaxGasPerBlock var recoverErr error @@ -382,8 +334,7 @@ func initializeNode(ctx context.Context, logger *slog.Logger, emitter emitter.Em recoverFromError := func() { if r := recover(); r != nil { var ok bool - recoverErr, ok = r.(error) - if !ok { + if recoverErr, ok = r.(error); !ok { panic(r) // Re-panic if not an error. } } @@ -391,21 +342,23 @@ func initializeNode(ctx context.Context, logger *slog.Logger, emitter emitter.Em // Execute node creation and handle any errors. defer recoverFromError() - node, nodeErr := buildNode(logger, emitter, nodeConfig) + node, nodeErr := buildNode(n.logger, n.emitter, nodeConfig) if recoverErr != nil { // First check for recover error in case of panic - return nil, fmt.Errorf("recovered from a node panic: %w", recoverErr) + return fmt.Errorf("recovered from a node panic: %w", recoverErr) } if nodeErr != nil { // Then for any node error - return nil, fmt.Errorf("unable to build the node: %w", nodeErr) + return fmt.Errorf("unable to build the node: %w", nodeErr) } // Wait for the node to be ready select { case <-gnoland.GetNodeReadiness(node): // Ok - return node, nil + n.Node = node case <-ctx.Done(): - return nil, ctx.Err() + return ctx.Err() } + + return nil } func buildNode(logger *slog.Logger, emitter emitter.Emitter, cfg *gnoland.InMemoryNodeConfig) (*node.Node, error) { @@ -438,3 +391,28 @@ func buildNode(logger *slog.Logger, emitter emitter.Emitter, cfg *gnoland.InMemo return node, nil } + +func newNodeConfig(tmc *tmcfg.Config, chainid string, appstate gnoland.GnoGenesisState) *gnoland.InMemoryNodeConfig { + // Create Mocked Identity + pv := gnoland.NewMockedPrivValidator() + genesis := gnoland.NewDefaultGenesisConfig(pv.GetPubKey(), chainid) + genesis.AppState = appstate + + // Add self as validator + self := pv.GetPubKey() + genesis.Validators = []bft.GenesisValidator{ + { + Address: self.Address(), + PubKey: self, + Power: 10, + Name: "self", + }, + } + + return &gnoland.InMemoryNodeConfig{ + PrivValidator: pv, + TMConfig: tmc, + Genesis: genesis, + GenesisMaxVMCycles: 10_000_000, + } +} diff --git a/contribs/gnodev/pkg/dev/pkgs.go b/contribs/gnodev/pkg/dev/pkgs.go new file mode 100644 index 00000000000..c02508ff33d --- /dev/null +++ b/contribs/gnodev/pkg/dev/pkgs.go @@ -0,0 +1,82 @@ +package dev + +import ( + "fmt" + + vmm "github.com/gnolang/gno/gno.land/pkg/sdk/vm" + gno "github.com/gnolang/gno/gnovm/pkg/gnolang" + "github.com/gnolang/gno/gnovm/pkg/gnomod" + bft "github.com/gnolang/gno/tm2/pkg/bft/types" + "github.com/gnolang/gno/tm2/pkg/std" +) + +type PkgsMap map[string]gnomod.Pkg + +func newPkgsMap(paths []string) (PkgsMap, error) { + pkgs := make(map[string]gnomod.Pkg) + for _, path := range paths { + // list all packages from target path + pkgslist, err := gnomod.ListPkgs(path) + if err != nil { + return nil, fmt.Errorf("listing gno packages: %w", err) + } + + for _, pkg := range pkgslist { + if pkg.Dir == "" { + continue + } + + if _, ok := pkgs[pkg.Dir]; ok { + continue // skip + } + pkgs[pkg.Dir] = pkg + } + } + + // Filter out draft packages. + return pkgs, nil +} + +func (pm PkgsMap) toList() gnomod.PkgList { + list := make([]gnomod.Pkg, 0, len(pm)) + for _, pkg := range pm { + list = append(list, pkg) + } + return list +} + +func (pm PkgsMap) Load(creator bft.Address, fee std.Fee, deposit std.Coins) ([]std.Tx, error) { + pkgs := pm.toList() + + sorted, err := pkgs.Sort() + if err != nil { + return nil, fmt.Errorf("unable to sort pkgs: %w", err) + } + + nonDraft := sorted.GetNonDraftPkgs() + txs := []std.Tx{} + for _, pkg := range nonDraft { + // Open files in directory as MemPackage. + memPkg := gno.ReadMemPackage(pkg.Dir, pkg.Name) + if err := memPkg.Validate(); err != nil { + return nil, fmt.Errorf("invalid package: %w", err) + } + + // Create transaction + tx := std.Tx{ + Fee: fee, + Msgs: []std.Msg{ + vmm.MsgAddPackage{ + Creator: creator, + Package: memPkg, + Deposit: deposit, + }, + }, + } + + tx.Signatures = make([]std.Signature, len(tx.GetSigners())) + txs = append(txs, tx) + } + + return txs, nil +} diff --git a/gno.land/pkg/gnoland/node_inmemory.go b/gno.land/pkg/gnoland/node_inmemory.go index 3ac0e922a19..89f222738d0 100644 --- a/gno.land/pkg/gnoland/node_inmemory.go +++ b/gno.land/pkg/gnoland/node_inmemory.go @@ -42,7 +42,7 @@ func NewDefaultGenesisConfig(pk crypto.PubKey, chainid string) *bft.GenesisDoc { Block: &abci.BlockParams{ MaxTxBytes: 1_000_000, // 1MB, MaxDataBytes: 2_000_000, // 2MB, - MaxGas: 10_0000_000, // 10M gas + MaxGas: 100_000_000, // 10M gas TimeIotaMS: 100, // 100ms }, },