2026-03-09 16:21:42 +00:00
|
|
|
// Copyright (c) 2017-2026 Lethean (https://lt.hn)
|
|
|
|
|
//
|
|
|
|
|
// Licensed under the European Union Public Licence (EUPL) version 1.2.
|
|
|
|
|
// SPDX-License-Identifier: EUPL-1.2
|
|
|
|
|
|
|
|
|
|
package blockchain
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"os/signal"
|
2026-03-09 16:23:36 +00:00
|
|
|
"sync"
|
2026-03-09 16:21:42 +00:00
|
|
|
"syscall"
|
|
|
|
|
|
2026-03-26 14:10:18 +00:00
|
|
|
"dappco.re/go/core"
|
2026-03-22 01:49:26 +00:00
|
|
|
coreerr "dappco.re/go/core/log"
|
2026-03-16 21:16:34 +00:00
|
|
|
|
2026-03-22 01:49:26 +00:00
|
|
|
"dappco.re/go/core/blockchain/chain"
|
|
|
|
|
"dappco.re/go/core/process"
|
|
|
|
|
store "dappco.re/go/core/store"
|
2026-03-09 16:21:42 +00:00
|
|
|
"github.com/spf13/cobra"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
func newSyncCmd(dataDir, seed *string, testnet *bool) *cobra.Command {
|
|
|
|
|
var (
|
|
|
|
|
daemon bool
|
|
|
|
|
stop bool
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
cmd := &cobra.Command{
|
|
|
|
|
Use: "sync",
|
|
|
|
|
Short: "Headless P2P chain sync",
|
|
|
|
|
Long: "Sync the blockchain from P2P peers without the TUI explorer.",
|
|
|
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
|
|
|
if stop {
|
|
|
|
|
return stopSyncDaemon(*dataDir)
|
|
|
|
|
}
|
|
|
|
|
if daemon {
|
|
|
|
|
return runSyncDaemon(*dataDir, *seed, *testnet)
|
|
|
|
|
}
|
|
|
|
|
return runSyncForeground(*dataDir, *seed, *testnet)
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cmd.Flags().BoolVar(&daemon, "daemon", false, "run as background daemon")
|
|
|
|
|
cmd.Flags().BoolVar(&stop, "stop", false, "stop a running sync daemon")
|
|
|
|
|
|
|
|
|
|
return cmd
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func runSyncForeground(dataDir, seed string, testnet bool) error {
|
|
|
|
|
if err := ensureDataDir(dataDir); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-26 14:10:18 +00:00
|
|
|
dbPath := core.JoinPath(dataDir, "chain.db")
|
2026-03-09 16:21:42 +00:00
|
|
|
s, err := store.New(dbPath)
|
|
|
|
|
if err != nil {
|
2026-03-16 21:16:34 +00:00
|
|
|
return coreerr.E("runSyncForeground", "open store", err)
|
2026-03-09 16:21:42 +00:00
|
|
|
}
|
|
|
|
|
defer s.Close()
|
|
|
|
|
|
|
|
|
|
c := chain.New(s)
|
2026-04-04 04:07:45 +00:00
|
|
|
cfg, forks := resolveChainConfig(testnet, &seed)
|
2026-03-09 16:21:42 +00:00
|
|
|
|
2026-03-26 14:10:18 +00:00
|
|
|
ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
2026-03-09 16:21:42 +00:00
|
|
|
defer cancel()
|
|
|
|
|
|
2026-04-02 01:18:37 +01:00
|
|
|
core.Print(nil, "Starting headless P2P sync...")
|
2026-03-09 16:21:42 +00:00
|
|
|
syncLoop(ctx, c, &cfg, forks, seed)
|
2026-04-02 01:18:37 +01:00
|
|
|
core.Print(nil, "Sync stopped.")
|
2026-03-09 16:21:42 +00:00
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func runSyncDaemon(dataDir, seed string, testnet bool) error {
|
|
|
|
|
if err := ensureDataDir(dataDir); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-26 14:10:18 +00:00
|
|
|
pidFile := core.JoinPath(dataDir, "sync.pid")
|
2026-03-09 16:21:42 +00:00
|
|
|
|
|
|
|
|
d := process.NewDaemon(process.DaemonOptions{
|
|
|
|
|
PIDFile: pidFile,
|
|
|
|
|
Registry: process.DefaultRegistry(),
|
|
|
|
|
RegistryEntry: process.DaemonEntry{
|
2026-03-22 01:49:26 +00:00
|
|
|
Code: "dappco.re/go/core/blockchain",
|
2026-03-09 16:21:42 +00:00
|
|
|
Daemon: "sync",
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
if err := d.Start(); err != nil {
|
2026-03-16 21:16:34 +00:00
|
|
|
return coreerr.E("runSyncDaemon", "daemon start", err)
|
2026-03-09 16:21:42 +00:00
|
|
|
}
|
|
|
|
|
|
2026-03-26 14:10:18 +00:00
|
|
|
dbPath := core.JoinPath(dataDir, "chain.db")
|
2026-03-09 16:21:42 +00:00
|
|
|
s, err := store.New(dbPath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
_ = d.Stop()
|
2026-03-16 21:16:34 +00:00
|
|
|
return coreerr.E("runSyncDaemon", "open store", err)
|
2026-03-09 16:21:42 +00:00
|
|
|
}
|
|
|
|
|
defer s.Close()
|
|
|
|
|
|
|
|
|
|
c := chain.New(s)
|
2026-04-04 04:07:45 +00:00
|
|
|
cfg, forks := resolveChainConfig(testnet, &seed)
|
2026-03-09 16:21:42 +00:00
|
|
|
|
2026-03-26 14:10:18 +00:00
|
|
|
ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
2026-03-09 16:21:42 +00:00
|
|
|
defer cancel()
|
|
|
|
|
|
|
|
|
|
d.SetReady(true)
|
2026-04-02 01:18:37 +01:00
|
|
|
core.Print(nil, "Sync daemon started.")
|
2026-03-09 16:21:42 +00:00
|
|
|
|
2026-03-09 16:23:36 +00:00
|
|
|
var wg sync.WaitGroup
|
|
|
|
|
wg.Add(1)
|
|
|
|
|
go func() {
|
|
|
|
|
defer wg.Done()
|
|
|
|
|
syncLoop(ctx, c, &cfg, forks, seed)
|
|
|
|
|
}()
|
2026-03-09 16:21:42 +00:00
|
|
|
|
2026-03-09 16:23:36 +00:00
|
|
|
err = d.Run(ctx)
|
|
|
|
|
wg.Wait() // Wait for syncLoop to finish before closing store.
|
|
|
|
|
return err
|
2026-03-09 16:21:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func stopSyncDaemon(dataDir string) error {
|
2026-03-26 14:10:18 +00:00
|
|
|
pidFile := core.JoinPath(dataDir, "sync.pid")
|
2026-03-09 16:21:42 +00:00
|
|
|
pid, running := process.ReadPID(pidFile)
|
|
|
|
|
if pid == 0 || !running {
|
2026-03-16 21:16:34 +00:00
|
|
|
return coreerr.E("stopSyncDaemon", "no running sync daemon found", nil)
|
2026-03-09 16:21:42 +00:00
|
|
|
}
|
|
|
|
|
|
2026-03-26 14:10:18 +00:00
|
|
|
if err := syscall.Kill(pid, syscall.SIGTERM); err != nil {
|
|
|
|
|
return coreerr.E("stopSyncDaemon", core.Sprintf("signal process %d", pid), err)
|
2026-03-09 16:21:42 +00:00
|
|
|
}
|
|
|
|
|
|
2026-04-02 01:18:37 +01:00
|
|
|
core.Print(nil, "Sent SIGTERM to sync daemon (PID %d)", pid)
|
2026-03-09 16:21:42 +00:00
|
|
|
return nil
|
|
|
|
|
}
|