package main
|
|
|
|
import (
|
|
"encoding/hex"
|
|
"fmt"
|
|
"os"
|
|
"os/signal"
|
|
"path"
|
|
"strings"
|
|
|
|
ethKeystore "github.com/ethereum/go-ethereum/accounts/keystore"
|
|
"github.com/ethereum/go-ethereum/crypto"
|
|
"github.com/hermeznetwork/hermez-node/common"
|
|
"github.com/hermeznetwork/hermez-node/config"
|
|
dbUtils "github.com/hermeznetwork/hermez-node/db"
|
|
"github.com/hermeznetwork/hermez-node/db/historydb"
|
|
"github.com/hermeznetwork/hermez-node/db/kvdb"
|
|
"github.com/hermeznetwork/hermez-node/db/l2db"
|
|
"github.com/hermeznetwork/hermez-node/log"
|
|
"github.com/hermeznetwork/hermez-node/node"
|
|
"github.com/hermeznetwork/tracerr"
|
|
"github.com/iden3/go-iden3-crypto/babyjub"
|
|
"github.com/jmoiron/sqlx"
|
|
"github.com/urfave/cli/v2"
|
|
)
|
|
|
|
const (
|
|
flagCfg = "cfg"
|
|
flagMode = "mode"
|
|
flagSK = "privatekey"
|
|
flagYes = "yes"
|
|
flagBlock = "block"
|
|
modeSync = "sync"
|
|
modeCoord = "coord"
|
|
)
|
|
|
|
var (
|
|
// Version represents the program based on the git tag
|
|
Version = "v0.1.0"
|
|
// Build represents the program based on the git commit
|
|
Build = "dev"
|
|
// Date represents the date of application was built
|
|
Date = ""
|
|
)
|
|
|
|
func cmdVersion(c *cli.Context) error {
|
|
fmt.Printf("Version = \"%v\"\n", Version)
|
|
fmt.Printf("Build = \"%v\"\n", Build)
|
|
fmt.Printf("Date = \"%v\"\n", Date)
|
|
return nil
|
|
}
|
|
|
|
func cmdGenBJJ(c *cli.Context) error {
|
|
sk := babyjub.NewRandPrivKey()
|
|
skBuf := [32]byte(sk)
|
|
pk := sk.Public()
|
|
fmt.Printf("BJJ = \"0x%s\"\n", pk.String())
|
|
fmt.Printf("BJJPrivateKey = \"0x%s\"\n", hex.EncodeToString(skBuf[:]))
|
|
return nil
|
|
}
|
|
|
|
func cmdImportKey(c *cli.Context) error {
|
|
_cfg, err := parseCli(c)
|
|
if err != nil {
|
|
return tracerr.Wrap(fmt.Errorf("error parsing flags and config: %w", err))
|
|
}
|
|
if _cfg.mode != node.ModeCoordinator {
|
|
return tracerr.Wrap(fmt.Errorf("importkey must use mode coordinator"))
|
|
}
|
|
cfg := _cfg.node
|
|
|
|
scryptN := ethKeystore.StandardScryptN
|
|
scryptP := ethKeystore.StandardScryptP
|
|
if cfg.Coordinator.Debug.LightScrypt {
|
|
scryptN = ethKeystore.LightScryptN
|
|
scryptP = ethKeystore.LightScryptP
|
|
}
|
|
keyStore := ethKeystore.NewKeyStore(cfg.Coordinator.EthClient.Keystore.Path,
|
|
scryptN, scryptP)
|
|
hexKey := c.String(flagSK)
|
|
hexKey = strings.TrimPrefix(hexKey, "0x")
|
|
sk, err := crypto.HexToECDSA(hexKey)
|
|
if err != nil {
|
|
return tracerr.Wrap(err)
|
|
}
|
|
acc, err := keyStore.ImportECDSA(sk, cfg.Coordinator.EthClient.Keystore.Password)
|
|
if err != nil {
|
|
return tracerr.Wrap(err)
|
|
}
|
|
log.Infow("Imported private key", "addr", acc.Address.Hex())
|
|
return nil
|
|
}
|
|
|
|
func resetStateDBs(cfg *Config, batchNum common.BatchNum) error {
|
|
log.Infof("Reset Synchronizer StateDB to batchNum %v...", batchNum)
|
|
|
|
// Manually make a checkpoint from batchNum to current to force current
|
|
// to be a valid checkpoint. This is useful because in case of a
|
|
// crash, current can be corrupted and the first thing that
|
|
// `kvdb.NewKVDB` does is read the current checkpoint, which wouldn't
|
|
// succeed in case of corruption.
|
|
dbPath := cfg.node.StateDB.Path
|
|
source := path.Join(dbPath, fmt.Sprintf("%s%d", kvdb.PathBatchNum, batchNum))
|
|
current := path.Join(dbPath, kvdb.PathCurrent)
|
|
last := path.Join(dbPath, kvdb.PathLast)
|
|
if err := os.RemoveAll(last); err != nil {
|
|
return tracerr.Wrap(fmt.Errorf("os.RemoveAll: %w", err))
|
|
}
|
|
if batchNum == 0 {
|
|
if err := os.RemoveAll(current); err != nil {
|
|
return tracerr.Wrap(fmt.Errorf("os.RemoveAll: %w", err))
|
|
}
|
|
} else {
|
|
if err := kvdb.PebbleMakeCheckpoint(source, current); err != nil {
|
|
return tracerr.Wrap(fmt.Errorf("kvdb.PebbleMakeCheckpoint: %w", err))
|
|
}
|
|
}
|
|
db, err := kvdb.NewKVDB(kvdb.Config{
|
|
Path: dbPath,
|
|
NoGapsCheck: true,
|
|
NoLast: true,
|
|
})
|
|
if err != nil {
|
|
return tracerr.Wrap(fmt.Errorf("kvdb.NewKVDB: %w", err))
|
|
}
|
|
if err := db.Reset(batchNum); err != nil {
|
|
return tracerr.Wrap(fmt.Errorf("db.Reset: %w", err))
|
|
}
|
|
|
|
if cfg.mode == node.ModeCoordinator {
|
|
log.Infof("Wipe Coordinator StateDBs...")
|
|
|
|
// We wipe the Coordinator StateDBs entirely (by deleting
|
|
// current and resetting to batchNum 0) because the Coordinator
|
|
// StateDBs are always reset from Synchronizer when the
|
|
// coordinator pipeline starts.
|
|
dbPath := cfg.node.Coordinator.TxSelector.Path
|
|
current := path.Join(dbPath, kvdb.PathCurrent)
|
|
if err := os.RemoveAll(current); err != nil {
|
|
return tracerr.Wrap(fmt.Errorf("os.RemoveAll: %w", err))
|
|
}
|
|
db, err := kvdb.NewKVDB(kvdb.Config{
|
|
Path: dbPath,
|
|
NoGapsCheck: true,
|
|
NoLast: true,
|
|
})
|
|
if err != nil {
|
|
return tracerr.Wrap(fmt.Errorf("kvdb.NewKVDB: %w", err))
|
|
}
|
|
if err := db.Reset(0); err != nil {
|
|
return tracerr.Wrap(fmt.Errorf("db.Reset: %w", err))
|
|
}
|
|
|
|
dbPath = cfg.node.Coordinator.BatchBuilder.Path
|
|
current = path.Join(dbPath, kvdb.PathCurrent)
|
|
if err := os.RemoveAll(current); err != nil {
|
|
return tracerr.Wrap(fmt.Errorf("os.RemoveAll: %w", err))
|
|
}
|
|
db, err = kvdb.NewKVDB(kvdb.Config{
|
|
Path: dbPath,
|
|
NoGapsCheck: true,
|
|
NoLast: true,
|
|
})
|
|
if err != nil {
|
|
return tracerr.Wrap(fmt.Errorf("statedb.NewKVDB: %w", err))
|
|
}
|
|
if err := db.Reset(0); err != nil {
|
|
return tracerr.Wrap(fmt.Errorf("db.Reset: %w", err))
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func cmdWipeSQL(c *cli.Context) error {
|
|
_cfg, err := parseCli(c)
|
|
if err != nil {
|
|
return tracerr.Wrap(fmt.Errorf("error parsing flags and config: %w", err))
|
|
}
|
|
cfg := _cfg.node
|
|
yes := c.Bool(flagYes)
|
|
if !yes {
|
|
fmt.Print("*WARNING* Are you sure you want to delete " +
|
|
"the SQL DB and StateDBs? [y/N]: ")
|
|
var input string
|
|
if _, err := fmt.Scanln(&input); err != nil {
|
|
return tracerr.Wrap(err)
|
|
}
|
|
input = strings.ToLower(input)
|
|
if !(input == "y" || input == "yes") {
|
|
return nil
|
|
}
|
|
}
|
|
db, err := dbUtils.ConnectSQLDB(
|
|
cfg.PostgreSQL.PortWrite,
|
|
cfg.PostgreSQL.HostWrite,
|
|
cfg.PostgreSQL.UserWrite,
|
|
cfg.PostgreSQL.PasswordWrite,
|
|
cfg.PostgreSQL.NameWrite,
|
|
)
|
|
if err != nil {
|
|
return tracerr.Wrap(err)
|
|
}
|
|
log.Info("Wiping SQL DB...")
|
|
if err := dbUtils.MigrationsDown(db.DB); err != nil {
|
|
return tracerr.Wrap(fmt.Errorf("dbUtils.MigrationsDown: %w", err))
|
|
}
|
|
|
|
log.Info("Wiping StateDBs...")
|
|
if err := resetStateDBs(_cfg, 0); err != nil {
|
|
return tracerr.Wrap(fmt.Errorf("resetStateDBs: %w", err))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func waitSigInt() {
|
|
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
|
|
}
|
|
|
|
func cmdRun(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()
|
|
waitSigInt()
|
|
node.Stop()
|
|
|
|
return nil
|
|
}
|
|
|
|
func cmdServeAPI(c *cli.Context) error {
|
|
cfg, err := parseCliAPIServer(c)
|
|
if err != nil {
|
|
return tracerr.Wrap(fmt.Errorf("error parsing flags and config: %w", err))
|
|
}
|
|
srv, err := node.NewAPIServer(cfg.mode, cfg.server)
|
|
if err != nil {
|
|
return tracerr.Wrap(fmt.Errorf("error starting api server: %w", err))
|
|
}
|
|
srv.Start()
|
|
waitSigInt()
|
|
srv.Stop()
|
|
|
|
return nil
|
|
}
|
|
|
|
func cmdDiscard(c *cli.Context) error {
|
|
_cfg, err := parseCli(c)
|
|
if err != nil {
|
|
return tracerr.Wrap(fmt.Errorf("error parsing flags and config: %w", err))
|
|
}
|
|
cfg := _cfg.node
|
|
blockNum := c.Int64(flagBlock)
|
|
log.Infof("Discarding all blocks up to block %v...", blockNum)
|
|
|
|
dbWrite, err := dbUtils.InitSQLDB(
|
|
cfg.PostgreSQL.PortWrite,
|
|
cfg.PostgreSQL.HostWrite,
|
|
cfg.PostgreSQL.UserWrite,
|
|
cfg.PostgreSQL.PasswordWrite,
|
|
cfg.PostgreSQL.NameWrite,
|
|
)
|
|
if err != nil {
|
|
return 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 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 tracerr.Wrap(fmt.Errorf("dbUtils.InitSQLDB: %w", err))
|
|
}
|
|
}
|
|
historyDB := historydb.NewHistoryDB(dbRead, dbWrite, nil)
|
|
if err := historyDB.Reorg(blockNum); err != nil {
|
|
return tracerr.Wrap(fmt.Errorf("historyDB.Reorg: %w", err))
|
|
}
|
|
batchNum, err := historyDB.GetLastBatchNum()
|
|
if err != nil {
|
|
return tracerr.Wrap(fmt.Errorf("historyDB.GetLastBatchNum: %w", err))
|
|
}
|
|
l2DB := l2db.NewL2DB(
|
|
dbRead, dbWrite,
|
|
cfg.Coordinator.L2DB.SafetyPeriod,
|
|
cfg.Coordinator.L2DB.MaxTxs,
|
|
cfg.Coordinator.L2DB.MinFeeUSD,
|
|
cfg.Coordinator.L2DB.TTL.Duration,
|
|
nil,
|
|
)
|
|
if err := l2DB.Reorg(batchNum); err != nil {
|
|
return tracerr.Wrap(fmt.Errorf("l2DB.Reorg: %w", err))
|
|
}
|
|
|
|
log.Info("Resetting StateDBs...")
|
|
if err := resetStateDBs(_cfg, batchNum); err != nil {
|
|
return tracerr.Wrap(fmt.Errorf("resetStateDBs: %w", err))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Config is the configuration of the hermez node execution
|
|
type Config struct {
|
|
mode node.Mode
|
|
node *config.Node
|
|
}
|
|
|
|
func parseCli(c *cli.Context) (*Config, error) {
|
|
cfg, err := getConfig(c)
|
|
if err != nil {
|
|
if err := cli.ShowAppHelp(c); err != nil {
|
|
panic(err)
|
|
}
|
|
return nil, tracerr.Wrap(err)
|
|
}
|
|
return cfg, nil
|
|
}
|
|
|
|
func getConfig(c *cli.Context) (*Config, error) {
|
|
var cfg Config
|
|
mode := c.String(flagMode)
|
|
nodeCfgPath := c.String(flagCfg)
|
|
var err error
|
|
switch mode {
|
|
case modeSync:
|
|
cfg.mode = node.ModeSynchronizer
|
|
cfg.node, err = config.LoadNode(nodeCfgPath, false)
|
|
if err != nil {
|
|
return nil, tracerr.Wrap(err)
|
|
}
|
|
case modeCoord:
|
|
cfg.mode = node.ModeCoordinator
|
|
cfg.node, err = config.LoadNode(nodeCfgPath, true)
|
|
if err != nil {
|
|
return nil, tracerr.Wrap(err)
|
|
}
|
|
default:
|
|
return nil, tracerr.Wrap(fmt.Errorf("invalid mode \"%v\"", mode))
|
|
}
|
|
|
|
return &cfg, nil
|
|
}
|
|
|
|
// ConfigAPIServer is the configuration of the api server execution
|
|
type ConfigAPIServer struct {
|
|
mode node.Mode
|
|
server *config.APIServer
|
|
}
|
|
|
|
func parseCliAPIServer(c *cli.Context) (*ConfigAPIServer, error) {
|
|
cfg, err := getConfigAPIServer(c)
|
|
if err != nil {
|
|
if err := cli.ShowAppHelp(c); err != nil {
|
|
panic(err)
|
|
}
|
|
return nil, tracerr.Wrap(err)
|
|
}
|
|
return cfg, nil
|
|
}
|
|
|
|
func getConfigAPIServer(c *cli.Context) (*ConfigAPIServer, error) {
|
|
var cfg ConfigAPIServer
|
|
mode := c.String(flagMode)
|
|
nodeCfgPath := c.String(flagCfg)
|
|
var err error
|
|
switch mode {
|
|
case modeSync:
|
|
cfg.mode = node.ModeSynchronizer
|
|
cfg.server, err = config.LoadAPIServer(nodeCfgPath, false)
|
|
if err != nil {
|
|
return nil, tracerr.Wrap(err)
|
|
}
|
|
case modeCoord:
|
|
cfg.mode = node.ModeCoordinator
|
|
cfg.server, err = config.LoadAPIServer(nodeCfgPath, true)
|
|
if err != nil {
|
|
return nil, tracerr.Wrap(err)
|
|
}
|
|
default:
|
|
return nil, tracerr.Wrap(fmt.Errorf("invalid mode \"%v\"", mode))
|
|
}
|
|
|
|
return &cfg, nil
|
|
}
|
|
|
|
func main() {
|
|
app := cli.NewApp()
|
|
app.Name = "hermez-node"
|
|
app.Version = Version
|
|
flags := []cli.Flag{
|
|
&cli.StringFlag{
|
|
Name: flagMode,
|
|
Usage: fmt.Sprintf("Set node `MODE` (can be \"%v\" or \"%v\")", modeSync, modeCoord),
|
|
Required: true,
|
|
},
|
|
&cli.StringFlag{
|
|
Name: flagCfg,
|
|
Usage: "Node configuration `FILE`",
|
|
Required: true,
|
|
},
|
|
}
|
|
|
|
app.Commands = []*cli.Command{
|
|
{
|
|
Name: "version",
|
|
Aliases: []string{},
|
|
Usage: "Show the application version and build",
|
|
Action: cmdVersion,
|
|
},
|
|
{
|
|
Name: "importkey",
|
|
Aliases: []string{},
|
|
Usage: "Import ethereum private key",
|
|
Action: cmdImportKey,
|
|
Flags: append(flags,
|
|
&cli.StringFlag{
|
|
Name: flagSK,
|
|
Usage: "ethereum `PRIVATE_KEY` in hex",
|
|
Required: true,
|
|
}),
|
|
},
|
|
{
|
|
Name: "genbjj",
|
|
Aliases: []string{},
|
|
Usage: "Generate a new BabyJubJub key",
|
|
Action: cmdGenBJJ,
|
|
},
|
|
{
|
|
Name: "wipesql",
|
|
Aliases: []string{},
|
|
Usage: "Wipe the SQL DB (HistoryDB and L2DB) and the StateDBs, " +
|
|
"leaving the DB in a clean state",
|
|
Action: cmdWipeSQL,
|
|
Flags: append(flags,
|
|
&cli.BoolFlag{
|
|
Name: flagYes,
|
|
Usage: "automatic yes to the prompt",
|
|
Required: false,
|
|
}),
|
|
},
|
|
{
|
|
Name: "run",
|
|
Aliases: []string{},
|
|
Usage: "Run the hermez-node in the indicated mode",
|
|
Action: cmdRun,
|
|
Flags: flags,
|
|
},
|
|
{
|
|
Name: "serveapi",
|
|
Aliases: []string{},
|
|
Usage: "Serve the API only",
|
|
Action: cmdServeAPI,
|
|
Flags: flags,
|
|
},
|
|
{
|
|
Name: "discard",
|
|
Aliases: []string{},
|
|
Usage: "Discard blocks up to a specified block number",
|
|
Action: cmdDiscard,
|
|
Flags: append(flags,
|
|
&cli.Int64Flag{
|
|
Name: flagBlock,
|
|
Usage: "last block number to keep",
|
|
Required: false,
|
|
}),
|
|
},
|
|
}
|
|
|
|
err := app.Run(os.Args)
|
|
if err != nil {
|
|
fmt.Printf("\nError: %v\n", tracerr.Sprint(err))
|
|
os.Exit(1)
|
|
}
|
|
}
|