From 982899efedebbfd93b997c000bf0430eb1baaca0 Mon Sep 17 00:00:00 2001 From: Eduard S Date: Fri, 26 Feb 2021 12:40:00 +0100 Subject: [PATCH 1/3] Serve API only via cli --- cli/node/main.go | 42 ++++++++++++++ config/config.go | 140 +++++++++++++++++++++++++++-------------------- 2 files changed, 124 insertions(+), 58 deletions(-) diff --git a/cli/node/main.go b/cli/node/main.go index 0138640..b95e141 100644 --- a/cli/node/main.go +++ b/cli/node/main.go @@ -143,6 +143,42 @@ func cmdRun(c *cli.Context) error { return nil } +func cmdServeAPI(c *cli.Context) error { + cfg, err := parseCli(c) + if err != nil { + return tracerr.Wrap(fmt.Errorf("error parsing flags and config: %w", err)) + } + node, err := node.NewNode(cfg.mode, cfg.node) + if err != nil { + return tracerr.Wrap(fmt.Errorf("error starting node: %w", err)) + } + node.Start() + + stopCh := make(chan interface{}) + + // catch ^C to send the stop signal + ossig := make(chan os.Signal, 1) + signal.Notify(ossig, os.Interrupt) + const forceStopCount = 3 + go func() { + n := 0 + for sig := range ossig { + if sig == os.Interrupt { + log.Info("Received Interrupt Signal") + stopCh <- nil + n++ + if n == forceStopCount { + log.Fatalf("Received %v Interrupt Signals", forceStopCount) + } + } + } + }() + <-stopCh + node.Stop() + + return nil +} + func cmdDiscard(c *cli.Context) error { _cfg, err := parseCli(c) if err != nil { @@ -304,6 +340,12 @@ func main() { Usage: "Run the hermez-node in the indicated mode", Action: cmdRun, }, + { + Name: "serveapi", + Aliases: []string{}, + Usage: "Serve the API only", + Action: cmdServeAPI, + }, { Name: "discard", Aliases: []string{}, diff --git a/config/config.go b/config/config.go index 63158c7..a7ad88a 100644 --- a/config/config.go +++ b/config/config.go @@ -44,6 +44,13 @@ type ForgeBatchGasCost struct { L2Tx uint64 `validate:"required"` } +// CoordinatorAPI specifies the configuration parameters of the API in mode +// coordinator +type CoordinatorAPI struct { + // Coordinator enables the coordinator API endpoints + Coordinator bool +} + // Coordinator is the coordinator specific configuration. type Coordinator struct { // ForgerAddress is the address under which this coordinator is forging @@ -193,10 +200,7 @@ type Coordinator struct { // ForgeBatch transaction. ForgeBatchGasCost ForgeBatchGasCost `validate:"required"` } `validate:"required"` - API struct { - // Coordinator enables the coordinator API endpoints - Coordinator bool - } `validate:"required"` + API CoordinatorAPI `validate:"required"` Debug struct { // BatchPath if set, specifies the path where batchInfo is stored // in JSON in every step/update of the pipeline @@ -211,6 +215,64 @@ type Coordinator struct { } } +// NodeAPI specifies the configuration parameters of the API +type NodeAPI struct { + // Address where the API will listen if set + Address string + // Explorer enables the Explorer API endpoints + Explorer bool + // UpdateMetricsInterval is the interval between updates of the + // API metrics + UpdateMetricsInterval Duration + // UpdateRecommendedFeeInterval is the interval between updates of the + // recommended fees + UpdateRecommendedFeeInterval Duration + // Maximum concurrent connections allowed between API and SQL + MaxSQLConnections int `validate:"required"` + // SQLConnectionTimeout is the maximum amount of time that an API request + // can wait to stablish a SQL connection + SQLConnectionTimeout Duration +} + +// It's possible to use diferentiated SQL connections for read/write. +// If the read configuration is not provided, the write one it's going to be used +// for both reads and writes +type PostgreSQL struct { + // Port of the PostgreSQL write server + PortWrite int `validate:"required"` + // Host of the PostgreSQL write server + HostWrite string `validate:"required"` + // User of the PostgreSQL write server + UserWrite string `validate:"required"` + // Password of the PostgreSQL write server + PasswordWrite string `validate:"required"` + // Name of the PostgreSQL write server database + NameWrite string `validate:"required"` + // Port of the PostgreSQL read server + PortRead int + // Host of the PostgreSQL read server + HostRead string + // User of the PostgreSQL read server + UserRead string + // Password of the PostgreSQL read server + PasswordRead string + // Name of the PostgreSQL read server database + NameRead string +} + +// NodeDebug specifies debug configuration parameters +type NodeDebug struct { + // APIAddress is the address where the debugAPI will listen if + // set + APIAddress string + // MeddlerLogs enables meddler debug mode, where unused columns and struct + // fields will be logged + MeddlerLogs bool + // GinDebugMode sets Gin-Gonic (the web framework) to run in + // debug mode + GinDebugMode bool +} + // Node is the hermez node configuration. type Node struct { PriceUpdater struct { @@ -227,32 +289,8 @@ type Node struct { // Keep is the number of checkpoints to keep Keep int `validate:"required"` } `validate:"required"` - // It's possible to use diferentiated SQL connections for read/write. - // If the read configuration is not provided, the write one it's going to be used - // for both reads and writes - PostgreSQL struct { - // Port of the PostgreSQL write server - PortWrite int `validate:"required"` - // Host of the PostgreSQL write server - HostWrite string `validate:"required"` - // User of the PostgreSQL write server - UserWrite string `validate:"required"` - // Password of the PostgreSQL write server - PasswordWrite string `validate:"required"` - // Name of the PostgreSQL write server database - NameWrite string `validate:"required"` - // Port of the PostgreSQL read server - PortRead int - // Host of the PostgreSQL read server - HostRead string - // User of the PostgreSQL read server - UserRead string - // Password of the PostgreSQL read server - PasswordRead string - // Name of the PostgreSQL read server database - NameRead string - } `validate:"required"` - Web3 struct { + PostgreSQL PostgreSQL `validate:"required"` + Web3 struct { // URL is the URL of the web3 ethereum-node RPC server URL string `validate:"required"` } `validate:"required"` @@ -282,37 +320,23 @@ type Node struct { // TokenHEZ address TokenHEZName string `validate:"required"` } `validate:"required"` - API struct { - // Address where the API will listen if set - Address string - // Explorer enables the Explorer API endpoints - Explorer bool - // UpdateMetricsInterval is the interval between updates of the - // API metrics - UpdateMetricsInterval Duration - // UpdateRecommendedFeeInterval is the interval between updates of the - // recommended fees - UpdateRecommendedFeeInterval Duration - // Maximum concurrent connections allowed between API and SQL - MaxSQLConnections int `validate:"required"` - // SQLConnectionTimeout is the maximum amount of time that an API request - // can wait to stablish a SQL connection - SQLConnectionTimeout Duration - } `validate:"required"` - Debug struct { - // APIAddress is the address where the debugAPI will listen if - // set - APIAddress string - // MeddlerLogs enables meddler debug mode, where unused columns and struct - // fields will be logged - MeddlerLogs bool - // GinDebugMode sets Gin-Gonic (the web framework) to run in - // debug mode - GinDebugMode bool - } + API NodeAPI `validate:"required"` + Debug NodeDebug `validate:"required"` Coordinator Coordinator `validate:"-"` } +type APIServer struct { + API NodeAPI `validate:"required"` + PostgreSQL PostgreSQL `validate:"required"` + Coordinator struct { + API struct { + // Coordinator enables the coordinator API endpoints + Coordinator bool + } `validate:"required"` + } `validate:"required"` + Debug NodeDebug `validate:"required"` +} + // Load loads a generic config. func Load(path string, cfg interface{}) error { bs, err := ioutil.ReadFile(path) //nolint:gosec From bb4c4642001b44d9e2edc51352bb5488d06456fd Mon Sep 17 00:00:00 2001 From: Eduard S Date: Fri, 26 Feb 2021 13:09:13 +0100 Subject: [PATCH 2/3] WIP --- cli/node/main.go | 10 ++++++---- config/config.go | 24 ++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/cli/node/main.go b/cli/node/main.go index b95e141..d7bf69d 100644 --- a/cli/node/main.go +++ b/cli/node/main.go @@ -144,10 +144,15 @@ func cmdRun(c *cli.Context) error { } func cmdServeAPI(c *cli.Context) error { - cfg, err := parseCli(c) + cfgPath := c.String(flagCfg) + cfg, err := config.LoadAPIServer(cfgPath) if err != nil { + if err := cli.ShowAppHelp(c); err != nil { + panic(err) + } return tracerr.Wrap(fmt.Errorf("error parsing flags and config: %w", err)) } + node, err := node.NewNode(cfg.mode, cfg.node) if err != nil { return tracerr.Wrap(fmt.Errorf("error starting node: %w", err)) @@ -261,9 +266,6 @@ func getConfig(c *cli.Context) (*Config, error) { var cfg Config mode := c.String(flagMode) nodeCfgPath := c.String(flagCfg) - if nodeCfgPath == "" { - return nil, tracerr.Wrap(fmt.Errorf("required flag \"%v\" not set", flagCfg)) - } var err error switch mode { case modeSync: diff --git a/config/config.go b/config/config.go index a7ad88a..197cb99 100644 --- a/config/config.go +++ b/config/config.go @@ -334,6 +334,17 @@ type APIServer struct { Coordinator bool } `validate:"required"` } `validate:"required"` + L2DB struct { + // MaxTxs is the maximum number of pending L2Txs that can be + // stored in the pool. Once this number of pending L2Txs is + // reached, inserts to the pool will be denied until some of + // the pending txs are forged. + MaxTxs uint32 `validate:"required"` + // MinFeeUSD is the minimum fee in USD that a tx must pay in + // order to be accepted into the pool. Txs with lower than + // minimum fee will be rejected at the API level. + MinFeeUSD float64 + } `validate:"required"` Debug NodeDebug `validate:"required"` } @@ -378,3 +389,16 @@ func LoadNode(path string) (*Node, error) { } return &cfg, nil } + +// LoadAPIServer loads the APIServer configuration from path. +func LoadAPIServer(path string) (*APIServer, error) { + var cfg APIServer + if err := Load(path, &cfg); err != nil { + return nil, tracerr.Wrap(fmt.Errorf("error loading apiServer configuration file: %w", err)) + } + validate := validator.New() + if err := validate.Struct(cfg); err != nil { + return nil, tracerr.Wrap(fmt.Errorf("error validating configuration file: %w", err)) + } + return &cfg, nil +} From 26e2bbc262ec2f74e36482a2b081a2f1412a5e7f Mon Sep 17 00:00:00 2001 From: Eduard S Date: Fri, 26 Feb 2021 16:17:06 +0100 Subject: [PATCH 3/3] WIP --- node/node.go | 108 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) diff --git a/node/node.go b/node/node.go index 33ec3b8..52b8dc5 100644 --- a/node/node.go +++ b/node/node.go @@ -426,6 +426,114 @@ func NewNode(mode Mode, cfg *config.Node) (*Node, error) { }, nil } +// APIServer is a server that only runs the API +type APIServer struct { + nodeAPI *NodeAPI +} + +func NewAPIServer(mode Mode, cfg *config.APIServer) (*APIServer, error) { + // NOTE: I just copied some parts of NewNode related to starting the + // API, but it still cotains many parameters that are not available + meddler.Debug = cfg.Debug.MeddlerLogs + // Stablish DB connection + dbWrite, err := dbUtils.InitSQLDB( + cfg.PostgreSQL.PortWrite, + cfg.PostgreSQL.HostWrite, + cfg.PostgreSQL.UserWrite, + cfg.PostgreSQL.PasswordWrite, + cfg.PostgreSQL.NameWrite, + ) + if err != nil { + return nil, tracerr.Wrap(fmt.Errorf("dbUtils.InitSQLDB: %w", err)) + } + var dbRead *sqlx.DB + if cfg.PostgreSQL.HostRead == "" { + dbRead = dbWrite + } else if cfg.PostgreSQL.HostRead == cfg.PostgreSQL.HostWrite { + return nil, tracerr.Wrap(fmt.Errorf( + "PostgreSQL.HostRead and PostgreSQL.HostWrite must be different", + )) + } else { + dbRead, err = dbUtils.InitSQLDB( + cfg.PostgreSQL.PortRead, + cfg.PostgreSQL.HostRead, + cfg.PostgreSQL.UserRead, + cfg.PostgreSQL.PasswordRead, + cfg.PostgreSQL.NameRead, + ) + if err != nil { + return nil, tracerr.Wrap(fmt.Errorf("dbUtils.InitSQLDB: %w", err)) + } + } + var apiConnCon *dbUtils.APIConnectionController + if cfg.API.Explorer || mode == ModeCoordinator { + apiConnCon = dbUtils.NewAPICnnectionController( + cfg.API.MaxSQLConnections, + cfg.API.SQLConnectionTimeout.Duration, + ) + } + + historyDB := historydb.NewHistoryDB(dbRead, dbWrite, apiConnCon) + + var l2DB *l2db.L2DB + if mode == ModeCoordinator { + l2DB = l2db.NewL2DB( + dbRead, dbWrite, + cfg.Coordinator.L2DB.SafetyPeriod, + cfg.Coordinator.L2DB.MaxTxs, + cfg.Coordinator.L2DB.MinFeeUSD, + cfg.Coordinator.L2DB.TTL.Duration, + apiConnCon, + ) + } + + var nodeAPI *NodeAPI + if cfg.API.Address != "" { + if cfg.Debug.GinDebugMode { + gin.SetMode(gin.DebugMode) + } else { + gin.SetMode(gin.ReleaseMode) + } + if cfg.API.UpdateMetricsInterval.Duration == 0 { + return nil, tracerr.Wrap(fmt.Errorf("invalid cfg.API.UpdateMetricsInterval: %v", + cfg.API.UpdateMetricsInterval.Duration)) + } + if cfg.API.UpdateRecommendedFeeInterval.Duration == 0 { + return nil, tracerr.Wrap(fmt.Errorf("invalid cfg.API.UpdateRecommendedFeeInterval: %v", + cfg.API.UpdateRecommendedFeeInterval.Duration)) + } + server := gin.Default() + coord := false + if mode == ModeCoordinator { + coord = cfg.Coordinator.API.Coordinator + } + var err error + nodeAPI, err = NewNodeAPI( + cfg.API.Address, + coord, cfg.API.Explorer, + server, + historyDB, + stateDB, + l2DB, + &api.Config{ + RollupConstants: scConsts.Rollup, + AuctionConstants: scConsts.Auction, + WDelayerConstants: scConsts.WDelayer, + ChainID: chainIDU16, + HermezAddress: cfg.SmartContracts.Rollup, + }, + cfg.Coordinator.ForgeDelay.Duration, + ) + if err != nil { + return nil, tracerr.Wrap(err) + } + nodeAPI.api.SetRollupVariables(*initSCVars.Rollup) + nodeAPI.api.SetAuctionVariables(*initSCVars.Auction) + nodeAPI.api.SetWDelayerVariables(*initSCVars.WDelayer) + } + // ETC... +} + // NodeAPI holds the node http API type NodeAPI struct { //nolint:golint api *api.API