hermez-node/cli/node/main.go
cli/node/main.goRaw
1package main
2
3import (
4 "encoding/hex"
5 "fmt"
6 "os"
7 "os/signal"
8 "path"
9 "strings"
10
11 ethKeystore "github.com/ethereum/go-ethereum/accounts/keystore"
12 "github.com/ethereum/go-ethereum/crypto"
13 "github.com/hermeznetwork/hermez-node/common"
14 "github.com/hermeznetwork/hermez-node/config"
15 dbUtils "github.com/hermeznetwork/hermez-node/db"
16 "github.com/hermeznetwork/hermez-node/db/historydb"
17 "github.com/hermeznetwork/hermez-node/db/kvdb"
18 "github.com/hermeznetwork/hermez-node/db/l2db"
19 "github.com/hermeznetwork/hermez-node/log"
20 "github.com/hermeznetwork/hermez-node/node"
21 "github.com/hermeznetwork/tracerr"
22 "github.com/iden3/go-iden3-crypto/babyjub"
23 "github.com/jmoiron/sqlx"
24 "github.com/urfave/cli/v2"
25)
26
27const (
28 flagCfg = "cfg"
29 flagMode = "mode"
30 flagSK = "privatekey"
31 flagYes = "yes"
32 flagBlock = "block"
33 modeSync = "sync"
34 modeCoord = "coord"
35)
36
37func cmdGenBJJ(c *cli.Context) error {
38 sk := babyjub.NewRandPrivKey()
39 skBuf := [32]byte(sk)
40 pk := sk.Public()
41 fmt.Printf("BJJ = \"0x%s\"\n", pk.String())
42 fmt.Printf("BJJPrivateKey = \"0x%s\"\n", hex.EncodeToString(skBuf[:]))
43 return nil
44}
45
46func cmdImportKey(c *cli.Context) error {
47 _cfg, err := parseCli(c)
48 if err != nil {
49 return tracerr.Wrap(fmt.Errorf("error parsing flags and config: %w", err))
50 }
51 if _cfg.mode != node.ModeCoordinator {
52 return tracerr.Wrap(fmt.Errorf("importkey must use mode coordinator"))
53 }
54 cfg := _cfg.node
55
56 scryptN := ethKeystore.StandardScryptN
57 scryptP := ethKeystore.StandardScryptP
58 if cfg.Coordinator.Debug.LightScrypt {
59 scryptN = ethKeystore.LightScryptN
60 scryptP = ethKeystore.LightScryptP
61 }
62 keyStore := ethKeystore.NewKeyStore(cfg.Coordinator.EthClient.Keystore.Path,
63 scryptN, scryptP)
64 hexKey := c.String(flagSK)
65 hexKey = strings.TrimPrefix(hexKey, "0x")
66 sk, err := crypto.HexToECDSA(hexKey)
67 if err != nil {
68 return tracerr.Wrap(err)
69 }
70 acc, err := keyStore.ImportECDSA(sk, cfg.Coordinator.EthClient.Keystore.Password)
71 if err != nil {
72 return tracerr.Wrap(err)
73 }
74 log.Infow("Imported private key", "addr", acc.Address.Hex())
75 return nil
76}
77
78func resetStateDBs(cfg *Config, batchNum common.BatchNum) error {
79 log.Infof("Reset Synchronizer StateDB to batchNum %v...", batchNum)
80
81 // Manually make a checkpoint from batchNum to current to force current
82 // to be a valid checkpoint. This is useful because in case of a
83 // crash, current can be corrupted and the first thing that
84 // `kvdb.NewKVDB` does is read the current checkpoint, which wouldn't
85 // succeed in case of corruption.
86 dbPath := cfg.node.StateDB.Path
87 source := path.Join(dbPath, fmt.Sprintf("%s%d", kvdb.PathBatchNum, batchNum))
88 current := path.Join(dbPath, kvdb.PathCurrent)
89 last := path.Join(dbPath, kvdb.PathLast)
90 if err := os.RemoveAll(last); err != nil {
91 return tracerr.Wrap(fmt.Errorf("os.RemoveAll: %w", err))
92 }
93 if batchNum == 0 {
94 if err := os.RemoveAll(current); err != nil {
95 return tracerr.Wrap(fmt.Errorf("os.RemoveAll: %w", err))
96 }
97 } else {
98 if err := kvdb.PebbleMakeCheckpoint(source, current); err != nil {
99 return tracerr.Wrap(fmt.Errorf("kvdb.PebbleMakeCheckpoint: %w", err))
100 }
101 }
102 db, err := kvdb.NewKVDB(kvdb.Config{
103 Path: dbPath,
104 NoGapsCheck: true,
105 NoLast: true,
106 })
107 if err != nil {
108 return tracerr.Wrap(fmt.Errorf("kvdb.NewKVDB: %w", err))
109 }
110 if err := db.Reset(batchNum); err != nil {
111 return tracerr.Wrap(fmt.Errorf("db.Reset: %w", err))
112 }
113
114 if cfg.mode == node.ModeCoordinator {
115 log.Infof("Wipe Coordinator StateDBs...")
116
117 // We wipe the Coordinator StateDBs entirely (by deleting
118 // current and resetting to batchNum 0) because the Coordinator
119 // StateDBs are always reset from Synchronizer when the
120 // coordinator pipeline starts.
121 dbPath := cfg.node.Coordinator.TxSelector.Path
122 current := path.Join(dbPath, kvdb.PathCurrent)
123 if err := os.RemoveAll(current); err != nil {
124 return tracerr.Wrap(fmt.Errorf("os.RemoveAll: %w", err))
125 }
126 db, err := kvdb.NewKVDB(kvdb.Config{
127 Path: dbPath,
128 NoGapsCheck: true,
129 NoLast: true,
130 })
131 if err != nil {
132 return tracerr.Wrap(fmt.Errorf("kvdb.NewKVDB: %w", err))
133 }
134 if err := db.Reset(0); err != nil {
135 return tracerr.Wrap(fmt.Errorf("db.Reset: %w", err))
136 }
137
138 dbPath = cfg.node.Coordinator.BatchBuilder.Path
139 current = path.Join(dbPath, kvdb.PathCurrent)
140 if err := os.RemoveAll(current); err != nil {
141 return tracerr.Wrap(fmt.Errorf("os.RemoveAll: %w", err))
142 }
143 db, err = kvdb.NewKVDB(kvdb.Config{
144 Path: dbPath,
145 NoGapsCheck: true,
146 NoLast: true,
147 })
148 if err != nil {
149 return tracerr.Wrap(fmt.Errorf("statedb.NewKVDB: %w", err))
150 }
151 if err := db.Reset(0); err != nil {
152 return tracerr.Wrap(fmt.Errorf("db.Reset: %w", err))
153 }
154 }
155 return nil
156}
157
158func cmdWipeSQL(c *cli.Context) error {
159 _cfg, err := parseCli(c)
160 if err != nil {
161 return tracerr.Wrap(fmt.Errorf("error parsing flags and config: %w", err))
162 }
163 cfg := _cfg.node
164 yes := c.Bool(flagYes)
165 if !yes {
166 fmt.Print("*WARNING* Are you sure you want to delete " +
167 "the SQL DB and StateDBs? [y/N]: ")
168 var input string
169 if _, err := fmt.Scanln(&input); err != nil {
170 return tracerr.Wrap(err)
171 }
172 input = strings.ToLower(input)
173 if !(input == "y" || input == "yes") {
174 return nil
175 }
176 }
177 db, err := dbUtils.ConnectSQLDB(
178 cfg.PostgreSQL.PortWrite,
179 cfg.PostgreSQL.HostWrite,
180 cfg.PostgreSQL.UserWrite,
181 cfg.PostgreSQL.PasswordWrite,
182 cfg.PostgreSQL.NameWrite,
183 )
184 if err != nil {
185 return tracerr.Wrap(err)
186 }
187 log.Info("Wiping SQL DB...")
188 if err := dbUtils.MigrationsDown(db.DB); err != nil {
189 return tracerr.Wrap(fmt.Errorf("dbUtils.MigrationsDown: %w", err))
190 }
191
192 log.Info("Wiping StateDBs...")
193 if err := resetStateDBs(_cfg, 0); err != nil {
194 return tracerr.Wrap(fmt.Errorf("resetStateDBs: %w", err))
195 }
196 return nil
197}
198
199func waitSigInt() {
200 stopCh := make(chan interface{})
201
202 // catch ^C to send the stop signal
203 ossig := make(chan os.Signal, 1)
204 signal.Notify(ossig, os.Interrupt)
205 const forceStopCount = 3
206 go func() {
207 n := 0
208 for sig := range ossig {
209 if sig == os.Interrupt {
210 log.Info("Received Interrupt Signal")
211 stopCh <- nil
212 n++
213 if n == forceStopCount {
214 log.Fatalf("Received %v Interrupt Signals", forceStopCount)
215 }
216 }
217 }
218 }()
219 <-stopCh
220}
221
222func cmdRun(c *cli.Context) error {
223 cfg, err := parseCli(c)
224 if err != nil {
225 return tracerr.Wrap(fmt.Errorf("error parsing flags and config: %w", err))
226 }
227 node, err := node.NewNode(cfg.mode, cfg.node)
228 if err != nil {
229 return tracerr.Wrap(fmt.Errorf("error starting node: %w", err))
230 }
231 node.Start()
232 waitSigInt()
233 node.Stop()
234
235 return nil
236}
237
238func cmdServeAPI(c *cli.Context) error {
239 cfg, err := parseCliAPIServer(c)
240 if err != nil {
241 return tracerr.Wrap(fmt.Errorf("error parsing flags and config: %w", err))
242 }
243 srv, err := node.NewAPIServer(cfg.mode, cfg.server)
244 if err != nil {
245 return tracerr.Wrap(fmt.Errorf("error starting api server: %w", err))
246 }
247 srv.Start()
248 waitSigInt()
249 srv.Stop()
250
251 return nil
252}
253
254func cmdDiscard(c *cli.Context) error {
255 _cfg, err := parseCli(c)
256 if err != nil {
257 return tracerr.Wrap(fmt.Errorf("error parsing flags and config: %w", err))
258 }
259 cfg := _cfg.node
260 blockNum := c.Int64(flagBlock)
261 log.Infof("Discarding all blocks up to block %v...", blockNum)
262
263 dbWrite, err := dbUtils.InitSQLDB(
264 cfg.PostgreSQL.PortWrite,
265 cfg.PostgreSQL.HostWrite,
266 cfg.PostgreSQL.UserWrite,
267 cfg.PostgreSQL.PasswordWrite,
268 cfg.PostgreSQL.NameWrite,
269 )
270 if err != nil {
271 return tracerr.Wrap(fmt.Errorf("dbUtils.InitSQLDB: %w", err))
272 }
273 var dbRead *sqlx.DB
274 if cfg.PostgreSQL.HostRead == "" {
275 dbRead = dbWrite
276 } else if cfg.PostgreSQL.HostRead == cfg.PostgreSQL.HostWrite {
277 return tracerr.Wrap(fmt.Errorf(
278 "PostgreSQL.HostRead and PostgreSQL.HostWrite must be different",
279 ))
280 } else {
281 dbRead, err = dbUtils.InitSQLDB(
282 cfg.PostgreSQL.PortRead,
283 cfg.PostgreSQL.HostRead,
284 cfg.PostgreSQL.UserRead,
285 cfg.PostgreSQL.PasswordRead,
286 cfg.PostgreSQL.NameRead,
287 )
288 if err != nil {
289 return tracerr.Wrap(fmt.Errorf("dbUtils.InitSQLDB: %w", err))
290 }
291 }
292 historyDB := historydb.NewHistoryDB(dbRead, dbWrite, nil)
293 if err := historyDB.Reorg(blockNum); err != nil {
294 return tracerr.Wrap(fmt.Errorf("historyDB.Reorg: %w", err))
295 }
296 batchNum, err := historyDB.GetLastBatchNum()
297 if err != nil {
298 return tracerr.Wrap(fmt.Errorf("historyDB.GetLastBatchNum: %w", err))
299 }
300 l2DB := l2db.NewL2DB(
301 dbRead, dbWrite,
302 cfg.Coordinator.L2DB.SafetyPeriod,
303 cfg.Coordinator.L2DB.MaxTxs,
304 cfg.Coordinator.L2DB.MinFeeUSD,
305 cfg.Coordinator.L2DB.TTL.Duration,
306 nil,
307 )
308 if err := l2DB.Reorg(batchNum); err != nil {
309 return tracerr.Wrap(fmt.Errorf("l2DB.Reorg: %w", err))
310 }
311
312 log.Info("Resetting StateDBs...")
313 if err := resetStateDBs(_cfg, batchNum); err != nil {
314 return tracerr.Wrap(fmt.Errorf("resetStateDBs: %w", err))
315 }
316
317 return nil
318}
319
320// Config is the configuration of the hermez node execution
321type Config struct {
322 mode node.Mode
323 node *config.Node
324}
325
326func parseCli(c *cli.Context) (*Config, error) {
327 cfg, err := getConfig(c)
328 if err != nil {
329 if err := cli.ShowAppHelp(c); err != nil {
330 panic(err)
331 }
332 return nil, tracerr.Wrap(err)
333 }
334 return cfg, nil
335}
336
337func getConfig(c *cli.Context) (*Config, error) {
338 var cfg Config
339 mode := c.String(flagMode)
340 nodeCfgPath := c.String(flagCfg)
341 var err error
342 switch mode {
343 case modeSync:
344 cfg.mode = node.ModeSynchronizer
345 cfg.node, err = config.LoadNode(nodeCfgPath, false)
346 if err != nil {
347 return nil, tracerr.Wrap(err)
348 }
349 case modeCoord:
350 cfg.mode = node.ModeCoordinator
351 cfg.node, err = config.LoadNode(nodeCfgPath, true)
352 if err != nil {
353 return nil, tracerr.Wrap(err)
354 }
355 default:
356 return nil, tracerr.Wrap(fmt.Errorf("invalid mode \"%v\"", mode))
357 }
358
359 return &cfg, nil
360}
361
362// ConfigAPIServer is the configuration of the api server execution
363type ConfigAPIServer struct {
364 mode node.Mode
365 server *config.APIServer
366}
367
368func parseCliAPIServer(c *cli.Context) (*ConfigAPIServer, error) {
369 cfg, err := getConfigAPIServer(c)
370 if err != nil {
371 if err := cli.ShowAppHelp(c); err != nil {
372 panic(err)
373 }
374 return nil, tracerr.Wrap(err)
375 }
376 return cfg, nil
377}
378
379func getConfigAPIServer(c *cli.Context) (*ConfigAPIServer, error) {
380 var cfg ConfigAPIServer
381 mode := c.String(flagMode)
382 nodeCfgPath := c.String(flagCfg)
383 var err error
384 switch mode {
385 case modeSync:
386 cfg.mode = node.ModeSynchronizer
387 cfg.server, err = config.LoadAPIServer(nodeCfgPath, false)
388 if err != nil {
389 return nil, tracerr.Wrap(err)
390 }
391 case modeCoord:
392 cfg.mode = node.ModeCoordinator
393 cfg.server, err = config.LoadAPIServer(nodeCfgPath, true)
394 if err != nil {
395 return nil, tracerr.Wrap(err)
396 }
397 default:
398 return nil, tracerr.Wrap(fmt.Errorf("invalid mode \"%v\"", mode))
399 }
400
401 return &cfg, nil
402}
403
404func main() {
405 app := cli.NewApp()
406 app.Name = "hermez-node"
407 app.Version = "0.1.0-alpha"
408 app.Flags = []cli.Flag{
409 &cli.StringFlag{
410 Name: flagMode,
411 Usage: fmt.Sprintf("Set node `MODE` (can be \"%v\" or \"%v\")", modeSync, modeCoord),
412 Required: true,
413 },
414 &cli.StringFlag{
415 Name: flagCfg,
416 Usage: "Node configuration `FILE`",
417 Required: true,
418 },
419 }
420
421 app.Commands = []*cli.Command{
422 {
423 Name: "importkey",
424 Aliases: []string{},
425 Usage: "Import ethereum private key",
426 Action: cmdImportKey,
427 Flags: []cli.Flag{
428 &cli.StringFlag{
429 Name: flagSK,
430 Usage: "ethereum `PRIVATE_KEY` in hex",
431 Required: true,
432 }},
433 },
434 {
435 Name: "genbjj",
436 Aliases: []string{},
437 Usage: "Generate a new BabyJubJub key",
438 Action: cmdGenBJJ,
439 },
440 {
441 Name: "wipesql",
442 Aliases: []string{},
443 Usage: "Wipe the SQL DB (HistoryDB and L2DB) and the StateDBs, " +
444 "leaving the DB in a clean state",
445 Action: cmdWipeSQL,
446 Flags: []cli.Flag{
447 &cli.BoolFlag{
448 Name: flagYes,
449 Usage: "automatic yes to the prompt",
450 Required: false,
451 }},
452 },
453 {
454 Name: "run",
455 Aliases: []string{},
456 Usage: "Run the hermez-node in the indicated mode",
457 Action: cmdRun,
458 },
459 {
460 Name: "serveapi",
461 Aliases: []string{},
462 Usage: "Serve the API only",
463 Action: cmdServeAPI,
464 },
465 {
466 Name: "discard",
467 Aliases: []string{},
468 Usage: "Discard blocks up to a specified block number",
469 Action: cmdDiscard,
470 Flags: []cli.Flag{
471 &cli.Int64Flag{
472 Name: flagBlock,
473 Usage: "last block number to keep",
474 Required: false,
475 }},
476 },
477 }
478
479 err := app.Run(os.Args)
480 if err != nil {
481 fmt.Printf("\nError: %v\n", tracerr.Sprint(err))
482 os.Exit(1)
483 }
484}