mirror of
https://github.com/luxfi/node.git
synced 2026-10-30 15:24:05 +00:00
go list ./... returned nothing, so no package here could be built or tested and the failure was invisible: one directory held a Go test beside fifteen .cpp files, which makes it a Go package the toolchain then refuses. The generator moves to the golden/ subdirectory the other two C++ chains already use. That silence was hiding a package that does not compile. host/ calls EnsureIdentity and identity.go defines LoadIdentity — a rename that happened on one side — and it has no importers, because the Go host is cmd/luxd now. Deleted rather than repaired: the thing it was a stand-in for arrived. The operator tools came across too (backup, blscheck, ceremony, config, genesishash, keytoaddr, pqkeygen, repair-proposervm, wire) plus benchmarks and examples, so the package set now differs from the retired repo by exactly one name: main/ is cmd/luxd. Two tests were right and are now satisfied rather than silenced. The purity test looked for ./main; it looks where the daemon lives. The chain-address test found conformance/dex spelling /v1/chain by hand — but that is its own module on purpose, since it links an EVM ABI and a signer the node must not carry, so it cannot call Chain. The rule is about this module, and the walk now says so.
275 lines
7.8 KiB
Go
275 lines
7.8 KiB
Go
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
// Command backup provides CLI tools for database backup and restore operations.
|
|
//
|
|
// Usage:
|
|
//
|
|
// # Full backup with zstd compression
|
|
// luxd backup --output /path/to/backup.zst --since 0
|
|
//
|
|
// # Incremental backup (using version from last backup)
|
|
// luxd backup --output /path/to/backup-incr.zst --since 12345678
|
|
//
|
|
// # Restore from backup
|
|
// luxd restore --input /path/to/backup.zst
|
|
//
|
|
// # Get last backup metadata
|
|
// luxd backup-info --data-dir ~/.lux/mainnet
|
|
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/spf13/cobra"
|
|
|
|
"github.com/luxfi/database"
|
|
databasefactory "github.com/luxfi/database/factory"
|
|
"github.com/luxfi/log"
|
|
"github.com/luxfi/metric"
|
|
"github.com/luxfi/node/service/backup"
|
|
)
|
|
|
|
var (
|
|
// Flags
|
|
outputPath string
|
|
inputPath string
|
|
sinceVer uint64
|
|
dataDir string
|
|
dbType string
|
|
noCompress bool
|
|
forceRestore bool
|
|
)
|
|
|
|
func main() {
|
|
rootCmd := &cobra.Command{
|
|
Use: "backup",
|
|
Short: "Database backup and restore utilities",
|
|
Long: `Backup and restore utilities for Lux node database.
|
|
|
|
Supports:
|
|
- Full and incremental backups using ZapDB native backup
|
|
- Zstd compression (enabled by default for .zst extension)
|
|
- Works with both ZapDB and PebbleDB`,
|
|
}
|
|
|
|
// backup command
|
|
backupCmd := &cobra.Command{
|
|
Use: "backup",
|
|
Short: "Create a database backup",
|
|
Long: `Create a backup of the node database.
|
|
|
|
Examples:
|
|
# Full backup with zstd compression
|
|
luxd backup --output /backups/lux-backup.zst --data-dir ~/.lux/mainnet
|
|
|
|
# Incremental backup (pass version from previous backup)
|
|
luxd backup --output /backups/lux-backup-incr.zst --since 12345678 --data-dir ~/.lux/mainnet
|
|
|
|
# Backup without compression
|
|
luxd backup --output /backups/lux-backup.db --no-compress --data-dir ~/.lux/mainnet`,
|
|
RunE: runBackup,
|
|
}
|
|
backupCmd.Flags().StringVarP(&outputPath, "output", "o", "", "Output file path (required)")
|
|
backupCmd.Flags().Uint64VarP(&sinceVer, "since", "s", 0, "Version for incremental backup (0 for full)")
|
|
backupCmd.Flags().StringVarP(&dataDir, "data-dir", "d", "", "Node data directory (required)")
|
|
backupCmd.Flags().StringVar(&dbType, "db-type", "zapdb", "Database type (zapdb or pebbledb)")
|
|
backupCmd.Flags().BoolVar(&noCompress, "no-compress", false, "Disable zstd compression")
|
|
backupCmd.MarkFlagRequired("output")
|
|
backupCmd.MarkFlagRequired("data-dir")
|
|
|
|
// restore command
|
|
restoreCmd := &cobra.Command{
|
|
Use: "restore",
|
|
Short: "Restore database from backup",
|
|
Long: `Restore the node database from a backup file.
|
|
|
|
WARNING: This will overwrite the existing database!
|
|
|
|
Examples:
|
|
# Restore from compressed backup
|
|
luxd restore --input /backups/lux-backup.zst --data-dir ~/.lux/mainnet
|
|
|
|
# Force restore (skip confirmation)
|
|
luxd restore --input /backups/lux-backup.zst --data-dir ~/.lux/mainnet --force`,
|
|
RunE: runRestore,
|
|
}
|
|
restoreCmd.Flags().StringVarP(&inputPath, "input", "i", "", "Input backup file path (required)")
|
|
restoreCmd.Flags().StringVarP(&dataDir, "data-dir", "d", "", "Node data directory (required)")
|
|
restoreCmd.Flags().StringVar(&dbType, "db-type", "zapdb", "Database type (zapdb or pebbledb)")
|
|
restoreCmd.Flags().BoolVarP(&forceRestore, "force", "f", false, "Skip confirmation prompt")
|
|
restoreCmd.MarkFlagRequired("input")
|
|
restoreCmd.MarkFlagRequired("data-dir")
|
|
|
|
// info command
|
|
infoCmd := &cobra.Command{
|
|
Use: "backup-info",
|
|
Short: "Show backup metadata",
|
|
Long: `Display information about the last backup.
|
|
|
|
Examples:
|
|
luxd backup-info --data-dir ~/.lux/mainnet`,
|
|
RunE: runInfo,
|
|
}
|
|
infoCmd.Flags().StringVarP(&dataDir, "data-dir", "d", "", "Node data directory (required)")
|
|
infoCmd.MarkFlagRequired("data-dir")
|
|
|
|
rootCmd.AddCommand(backupCmd, restoreCmd, infoCmd)
|
|
|
|
if err := rootCmd.Execute(); err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
func runBackup(cmd *cobra.Command, args []string) error {
|
|
logger := log.New("cmd", "backup")
|
|
|
|
// Open database
|
|
dbPath := filepath.Join(dataDir, "db")
|
|
db, err := openDatabase(dbPath, dbType, logger)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to open database: %w", err)
|
|
}
|
|
defer db.Close()
|
|
|
|
// Create backup service
|
|
backupSvc, err := backup.New(backup.Config{
|
|
DB: db,
|
|
MetadataDir: filepath.Join(dataDir, "backup"),
|
|
Log: logger,
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create backup service: %w", err)
|
|
}
|
|
|
|
// Determine compression
|
|
compress := !noCompress && (strings.HasSuffix(outputPath, ".zst") || strings.HasSuffix(outputPath, ".zstd"))
|
|
|
|
fmt.Printf("Starting backup...\n")
|
|
fmt.Printf(" Output: %s\n", outputPath)
|
|
fmt.Printf(" Since version: %d\n", sinceVer)
|
|
fmt.Printf(" Compression: %v\n", compress)
|
|
|
|
version, err := backupSvc.BackupToFile(outputPath, sinceVer, compress)
|
|
if err != nil {
|
|
return fmt.Errorf("backup failed: %w", err)
|
|
}
|
|
|
|
// Get file size
|
|
info, err := os.Stat(outputPath)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to stat backup file: %w", err)
|
|
}
|
|
|
|
fmt.Printf("\nBackup completed successfully!\n")
|
|
fmt.Printf(" Version: %d\n", version)
|
|
fmt.Printf(" Size: %s\n", formatBytes(info.Size()))
|
|
fmt.Printf(" Incremental: %v\n", sinceVer > 0)
|
|
fmt.Printf("\nFor next incremental backup, use: --since %d\n", version)
|
|
|
|
return nil
|
|
}
|
|
|
|
func runRestore(cmd *cobra.Command, args []string) error {
|
|
logger := log.New("cmd", "restore")
|
|
|
|
// Check if backup file exists
|
|
if _, err := os.Stat(inputPath); os.IsNotExist(err) {
|
|
return fmt.Errorf("backup file not found: %s", inputPath)
|
|
}
|
|
|
|
// Confirm restore
|
|
if !forceRestore {
|
|
fmt.Printf("WARNING: This will overwrite the database at %s\n", dataDir)
|
|
fmt.Printf("Are you sure you want to continue? (yes/no): ")
|
|
var response string
|
|
fmt.Scanln(&response)
|
|
if response != "yes" {
|
|
fmt.Println("Restore cancelled.")
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// Open database
|
|
dbPath := filepath.Join(dataDir, "db")
|
|
db, err := openDatabase(dbPath, dbType, logger)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to open database: %w", err)
|
|
}
|
|
defer db.Close()
|
|
|
|
// Create backup service
|
|
backupSvc, err := backup.New(backup.Config{
|
|
DB: db,
|
|
MetadataDir: filepath.Join(dataDir, "backup"),
|
|
Log: logger,
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create backup service: %w", err)
|
|
}
|
|
|
|
// Determine if compressed
|
|
compressed := strings.HasSuffix(inputPath, ".zst") || strings.HasSuffix(inputPath, ".zstd")
|
|
|
|
fmt.Printf("Starting restore...\n")
|
|
fmt.Printf(" Input: %s\n", inputPath)
|
|
fmt.Printf(" Compressed: %v\n", compressed)
|
|
|
|
if err := backupSvc.RestoreFromFile(inputPath, compressed); err != nil {
|
|
return fmt.Errorf("restore failed: %w", err)
|
|
}
|
|
|
|
fmt.Printf("\nRestore completed successfully!\n")
|
|
return nil
|
|
}
|
|
|
|
func runInfo(cmd *cobra.Command, args []string) error {
|
|
metadataPath := filepath.Join(dataDir, "backup", backup.MetadataFileName)
|
|
|
|
data, err := os.ReadFile(metadataPath)
|
|
if os.IsNotExist(err) {
|
|
fmt.Println("No backup metadata found.")
|
|
fmt.Printf("Metadata path: %s\n", metadataPath)
|
|
return nil
|
|
}
|
|
if err != nil {
|
|
return fmt.Errorf("failed to read metadata: %w", err)
|
|
}
|
|
|
|
fmt.Println("Backup Metadata:")
|
|
fmt.Println(string(data))
|
|
return nil
|
|
}
|
|
|
|
func openDatabase(path string, dbType string, logger log.Logger) (database.Database, error) {
|
|
// Ensure directory exists
|
|
if err := os.MkdirAll(path, 0755); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
gatherer := metric.NewRegistry()
|
|
db, err := databasefactory.New(dbType, path, false, nil, gatherer, logger, "backup", "db")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return db, nil
|
|
}
|
|
|
|
func formatBytes(bytes int64) string {
|
|
const unit = 1024
|
|
if bytes < unit {
|
|
return fmt.Sprintf("%d B", bytes)
|
|
}
|
|
div, exp := int64(unit), 0
|
|
for n := bytes / unit; n >= unit; n /= unit {
|
|
div *= unit
|
|
exp++
|
|
}
|
|
return fmt.Sprintf("%.1f %cB", float64(bytes)/float64(div), "KMGTPE"[exp])
|
|
}
|