feat: modernise CLI — AddChainCommands, explorer/sync subcommands, core-chain binary
Migrate from stdlib flag to cli.Main() + WithCommands() pattern: - AddChainCommands() registration with persistent --data-dir/--seed/--testnet flags - Explorer subcommand (TUI block explorer, replaces old default mode) - Sync subcommand with headless foreground and --daemon/--stop modes - sync_service.go extracts syncLoop/syncOnce from old main.go - cmd/core-chain/main.go standalone binary entry point - Add go-process dependency for daemon lifecycle Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
d8c2e84115
commit
aa3d8270a2
8 changed files with 470 additions and 13 deletions
18
cmd/core-chain/main.go
Normal file
18
cmd/core-chain/main.go
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
// 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 main
|
||||
|
||||
import (
|
||||
cli "forge.lthn.ai/core/cli/pkg/cli"
|
||||
blockchain "forge.lthn.ai/core/go-blockchain"
|
||||
)
|
||||
|
||||
func main() {
|
||||
cli.WithAppName("core-chain")
|
||||
cli.Main(
|
||||
cli.WithCommands("chain", blockchain.AddChainCommands),
|
||||
)
|
||||
}
|
||||
66
cmd_explorer.go
Normal file
66
cmd_explorer.go
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
// 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"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
|
||||
cli "forge.lthn.ai/core/cli/pkg/cli"
|
||||
store "forge.lthn.ai/core/go-store"
|
||||
|
||||
"forge.lthn.ai/core/go-blockchain/chain"
|
||||
"forge.lthn.ai/core/go-blockchain/tui"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func newExplorerCmd(dataDir, seed *string, testnet *bool) *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "explorer",
|
||||
Short: "TUI block explorer",
|
||||
Long: "Interactive terminal block explorer with live sync status.",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runExplorer(*dataDir, *seed, *testnet)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func runExplorer(dataDir, seed string, testnet bool) error {
|
||||
if err := ensureDataDir(dataDir); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dbPath := filepath.Join(dataDir, "chain.db")
|
||||
s, err := store.New(dbPath)
|
||||
if err != nil {
|
||||
log.Fatalf("open store: %v", err)
|
||||
}
|
||||
defer s.Close()
|
||||
|
||||
c := chain.New(s)
|
||||
cfg, forks := resolveConfig(testnet, &seed)
|
||||
|
||||
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
|
||||
defer cancel()
|
||||
|
||||
go syncLoop(ctx, c, &cfg, forks, seed)
|
||||
|
||||
node := tui.NewNode(c)
|
||||
status := tui.NewStatusModel(node)
|
||||
explorer := tui.NewExplorerModel(c)
|
||||
hints := tui.NewKeyHintsModel()
|
||||
|
||||
frame := cli.NewFrame("HCF")
|
||||
frame.Header(status)
|
||||
frame.Content(explorer)
|
||||
frame.Footer(hints)
|
||||
frame.Run()
|
||||
|
||||
return nil
|
||||
}
|
||||
134
cmd_sync.go
Normal file
134
cmd_sync.go
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
// 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"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
|
||||
"forge.lthn.ai/core/go-blockchain/chain"
|
||||
"forge.lthn.ai/core/go-process"
|
||||
store "forge.lthn.ai/core/go-store"
|
||||
"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
|
||||
}
|
||||
|
||||
dbPath := filepath.Join(dataDir, "chain.db")
|
||||
s, err := store.New(dbPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("open store: %w", err)
|
||||
}
|
||||
defer s.Close()
|
||||
|
||||
c := chain.New(s)
|
||||
cfg, forks := resolveConfig(testnet, &seed)
|
||||
|
||||
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||
defer cancel()
|
||||
|
||||
log.Println("Starting headless P2P sync...")
|
||||
syncLoop(ctx, c, &cfg, forks, seed)
|
||||
log.Println("Sync stopped.")
|
||||
return nil
|
||||
}
|
||||
|
||||
func runSyncDaemon(dataDir, seed string, testnet bool) error {
|
||||
if err := ensureDataDir(dataDir); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pidFile := filepath.Join(dataDir, "sync.pid")
|
||||
|
||||
d := process.NewDaemon(process.DaemonOptions{
|
||||
PIDFile: pidFile,
|
||||
Registry: process.DefaultRegistry(),
|
||||
RegistryEntry: process.DaemonEntry{
|
||||
Code: "forge.lthn.ai/core/go-blockchain",
|
||||
Daemon: "sync",
|
||||
},
|
||||
})
|
||||
|
||||
if err := d.Start(); err != nil {
|
||||
return fmt.Errorf("daemon start: %w", err)
|
||||
}
|
||||
|
||||
dbPath := filepath.Join(dataDir, "chain.db")
|
||||
s, err := store.New(dbPath)
|
||||
if err != nil {
|
||||
_ = d.Stop()
|
||||
return fmt.Errorf("open store: %w", err)
|
||||
}
|
||||
defer s.Close()
|
||||
|
||||
c := chain.New(s)
|
||||
cfg, forks := resolveConfig(testnet, &seed)
|
||||
|
||||
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||
defer cancel()
|
||||
|
||||
d.SetReady(true)
|
||||
log.Println("Sync daemon started.")
|
||||
|
||||
go syncLoop(ctx, c, &cfg, forks, seed)
|
||||
|
||||
return d.Run(ctx)
|
||||
}
|
||||
|
||||
func stopSyncDaemon(dataDir string) error {
|
||||
pidFile := filepath.Join(dataDir, "sync.pid")
|
||||
pid, running := process.ReadPID(pidFile)
|
||||
if pid == 0 || !running {
|
||||
return fmt.Errorf("no running sync daemon found")
|
||||
}
|
||||
|
||||
proc, err := os.FindProcess(pid)
|
||||
if err != nil {
|
||||
return fmt.Errorf("find process %d: %w", pid, err)
|
||||
}
|
||||
|
||||
if err := proc.Signal(syscall.SIGTERM); err != nil {
|
||||
return fmt.Errorf("signal process %d: %w", pid, err)
|
||||
}
|
||||
|
||||
log.Printf("Sent SIGTERM to sync daemon (PID %d)", pid)
|
||||
return nil
|
||||
}
|
||||
67
commands.go
Normal file
67
commands.go
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
// 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 (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"forge.lthn.ai/core/go-blockchain/config"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// AddChainCommands registers the "chain" command group with explorer,
|
||||
// sync, and mine subcommands.
|
||||
func AddChainCommands(root *cobra.Command) {
|
||||
var (
|
||||
dataDir string
|
||||
seed string
|
||||
testnet bool
|
||||
)
|
||||
|
||||
chainCmd := &cobra.Command{
|
||||
Use: "chain",
|
||||
Short: "Lethean blockchain node",
|
||||
Long: "Manage the Lethean blockchain — sync, explore, and mine.",
|
||||
}
|
||||
|
||||
chainCmd.PersistentFlags().StringVar(&dataDir, "data-dir", defaultDataDir(), "blockchain data directory")
|
||||
chainCmd.PersistentFlags().StringVar(&seed, "seed", "seeds.lthn.io:36942", "seed peer address (host:port)")
|
||||
chainCmd.PersistentFlags().BoolVar(&testnet, "testnet", false, "use testnet")
|
||||
|
||||
chainCmd.AddCommand(
|
||||
newExplorerCmd(&dataDir, &seed, &testnet),
|
||||
newSyncCmd(&dataDir, &seed, &testnet),
|
||||
)
|
||||
|
||||
root.AddCommand(chainCmd)
|
||||
}
|
||||
|
||||
func resolveConfig(testnet bool, seed *string) (config.ChainConfig, []config.HardFork) {
|
||||
if testnet {
|
||||
if *seed == "seeds.lthn.io:36942" {
|
||||
*seed = "localhost:46942"
|
||||
}
|
||||
return config.Testnet, config.TestnetForks
|
||||
}
|
||||
return config.Mainnet, config.MainnetForks
|
||||
}
|
||||
|
||||
func defaultDataDir() string {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return ".lethean"
|
||||
}
|
||||
return filepath.Join(home, ".lethean", "chain")
|
||||
}
|
||||
|
||||
func ensureDataDir(dataDir string) error {
|
||||
if err := os.MkdirAll(dataDir, 0o755); err != nil {
|
||||
return fmt.Errorf("create data dir: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
48
commands_test.go
Normal file
48
commands_test.go
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
// 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 (
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAddChainCommands_Good_RegistersParent(t *testing.T) {
|
||||
root := &cobra.Command{Use: "test"}
|
||||
AddChainCommands(root)
|
||||
|
||||
chainCmd, _, err := root.Find([]string{"chain"})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "chain", chainCmd.Name())
|
||||
}
|
||||
|
||||
func TestAddChainCommands_Good_HasSubcommands(t *testing.T) {
|
||||
root := &cobra.Command{Use: "test"}
|
||||
AddChainCommands(root)
|
||||
|
||||
chainCmd, _, _ := root.Find([]string{"chain"})
|
||||
|
||||
var names []string
|
||||
for _, sub := range chainCmd.Commands() {
|
||||
names = append(names, sub.Name())
|
||||
}
|
||||
assert.Contains(t, names, "explorer")
|
||||
assert.Contains(t, names, "sync")
|
||||
}
|
||||
|
||||
func TestAddChainCommands_Good_PersistentFlags(t *testing.T) {
|
||||
root := &cobra.Command{Use: "test"}
|
||||
AddChainCommands(root)
|
||||
|
||||
chainCmd, _, _ := root.Find([]string{"chain"})
|
||||
|
||||
assert.NotNil(t, chainCmd.PersistentFlags().Lookup("data-dir"))
|
||||
assert.NotNil(t, chainCmd.PersistentFlags().Lookup("seed"))
|
||||
assert.NotNil(t, chainCmd.PersistentFlags().Lookup("testnet"))
|
||||
}
|
||||
29
go.mod
29
go.mod
|
|
@ -3,27 +3,29 @@ module forge.lthn.ai/core/go-blockchain
|
|||
go 1.26.0
|
||||
|
||||
require (
|
||||
forge.lthn.ai/core/cli v0.0.1
|
||||
forge.lthn.ai/core/cli v0.1.0
|
||||
forge.lthn.ai/core/go-p2p v0.0.0-00010101000000-000000000000
|
||||
forge.lthn.ai/core/go-store v0.1.0
|
||||
forge.lthn.ai/core/go-process v0.1.2
|
||||
forge.lthn.ai/core/go-store v0.1.3
|
||||
github.com/charmbracelet/bubbletea v1.3.10
|
||||
github.com/spf13/cobra v1.10.2
|
||||
github.com/stretchr/testify v1.11.1
|
||||
golang.org/x/crypto v0.48.0
|
||||
)
|
||||
|
||||
require (
|
||||
forge.lthn.ai/core/go v0.0.1 // indirect
|
||||
forge.lthn.ai/core/go-crypt v0.0.1 // indirect
|
||||
forge.lthn.ai/core/go v0.1.0 // indirect
|
||||
forge.lthn.ai/core/go-crypt v0.1.0 // indirect
|
||||
github.com/ProtonMail/go-crypto v1.3.0 // indirect
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||
github.com/charmbracelet/colorprofile v0.4.1 // indirect
|
||||
github.com/charmbracelet/colorprofile v0.4.2 // indirect
|
||||
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 // indirect
|
||||
github.com/charmbracelet/x/ansi v0.11.4 // indirect
|
||||
github.com/charmbracelet/x/cellbuf v0.0.14 // indirect
|
||||
github.com/charmbracelet/x/ansi v0.11.6 // indirect
|
||||
github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
|
||||
github.com/charmbracelet/x/term v0.2.2 // indirect
|
||||
github.com/clipperhouse/displaywidth v0.7.0 // indirect
|
||||
github.com/clipperhouse/displaywidth v0.11.0 // indirect
|
||||
github.com/clipperhouse/stringish v0.1.1 // indirect
|
||||
github.com/clipperhouse/uax29/v2 v2.4.0 // indirect
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
|
||||
github.com/cloudflare/circl v1.6.3 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
|
|
@ -34,7 +36,7 @@ require (
|
|||
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-localereader v0.0.1 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.19 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.20 // indirect
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||
github.com/muesli/termenv v0.16.0 // indirect
|
||||
|
|
@ -42,15 +44,14 @@ require (
|
|||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/spf13/cobra v1.10.2 // indirect
|
||||
github.com/spf13/pflag v1.0.10 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a // indirect
|
||||
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa // indirect
|
||||
golang.org/x/sys v0.41.0 // indirect
|
||||
golang.org/x/term v0.40.0 // indirect
|
||||
golang.org/x/text v0.34.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
modernc.org/libc v1.67.7 // indirect
|
||||
modernc.org/libc v1.68.0 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
modernc.org/sqlite v1.46.1 // indirect
|
||||
|
|
@ -64,4 +65,6 @@ replace forge.lthn.ai/core/go-crypt => /Users/snider/Code/core/go-crypt
|
|||
|
||||
replace forge.lthn.ai/core/go-p2p => /Users/snider/Code/core/go-p2p
|
||||
|
||||
replace forge.lthn.ai/core/go-process => /Users/snider/Code/core/go-process
|
||||
|
||||
replace forge.lthn.ai/core/go-store => /Users/snider/Code/core/go-store
|
||||
|
|
|
|||
10
go.sum
10
go.sum
|
|
@ -1,3 +1,5 @@
|
|||
forge.lthn.ai/core/go-process v0.1.2 h1:0fdLJq/DPssilN9E5yude/xHNfZRKHghIjo++b5aXgc=
|
||||
forge.lthn.ai/core/go-process v0.1.2/go.mod h1:9oxVALrZaZCqFe8YDdheIS5bRUV1SBz4tVW/MflAtxM=
|
||||
github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw=
|
||||
github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||
|
|
@ -6,20 +8,25 @@ github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlv
|
|||
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
|
||||
github.com/charmbracelet/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk=
|
||||
github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk=
|
||||
github.com/charmbracelet/colorprofile v0.4.2/go.mod h1:0rTi81QpwDElInthtrQ6Ni7cG0sDtwAd4C4le060fT8=
|
||||
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 h1:ZR7e0ro+SZZiIZD7msJyA+NjkCNNavuiPBLgerbOziE=
|
||||
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834/go.mod h1:aKC/t2arECF6rNOnaKaVU6y4t4ZeHQzqfxedE/VkVhA=
|
||||
github.com/charmbracelet/x/ansi v0.11.4 h1:6G65PLu6HjmE858CnTUQY1LXT3ZUWwfvqEROLF8vqHI=
|
||||
github.com/charmbracelet/x/ansi v0.11.4/go.mod h1:/5AZ+UfWExW3int5H5ugnsG/PWjNcSQcwYsHBlPFQN4=
|
||||
github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=
|
||||
github.com/charmbracelet/x/cellbuf v0.0.14 h1:iUEMryGyFTelKW3THW4+FfPgi4fkmKnnaLOXuc+/Kj4=
|
||||
github.com/charmbracelet/x/cellbuf v0.0.14/go.mod h1:P447lJl49ywBbil/KjCk2HexGh4tEY9LH0/1QrZZ9rA=
|
||||
github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q=
|
||||
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
|
||||
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
|
||||
github.com/clipperhouse/displaywidth v0.7.0 h1:QNv1GYsnLX9QBrcWUtMlogpTXuM5FVnBwKWp1O5NwmE=
|
||||
github.com/clipperhouse/displaywidth v0.7.0/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o=
|
||||
github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
|
||||
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
|
||||
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
|
||||
github.com/clipperhouse/uax29/v2 v2.4.0 h1:RXqE/l5EiAbA4u97giimKNlmpvkmz+GrBVTelsoXy9g=
|
||||
github.com/clipperhouse/uax29/v2 v2.4.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
|
||||
github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
|
||||
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
|
|
@ -50,6 +57,7 @@ github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2J
|
|||
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
||||
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
|
||||
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||
github.com/mattn/go-runewidth v0.0.20/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
|
||||
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
||||
|
|
@ -81,6 +89,7 @@ golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
|||
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||
golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a h1:ovFr6Z0MNmU7nH8VaX5xqw+05ST2uO1exVfZPVqRC5o=
|
||||
golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA=
|
||||
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA=
|
||||
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
|
||||
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
|
|
@ -114,6 +123,7 @@ modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
|||
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
||||
modernc.org/libc v1.67.7 h1:H+gYQw2PyidyxwxQsGTwQw6+6H+xUk+plvOKW7+d3TI=
|
||||
modernc.org/libc v1.67.7/go.mod h1:UjCSJFl2sYbJbReVQeVpq/MgzlbmDM4cRHIYFelnaDk=
|
||||
modernc.org/libc v1.68.0/go.mod h1:NnKCYeoYgsEqnY3PgvNgAeaJnso968ygU8Z0DxjoEc0=
|
||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||
|
|
|
|||
111
sync_service.go
Normal file
111
sync_service.go
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
// 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"
|
||||
"crypto/rand"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"forge.lthn.ai/core/go-blockchain/chain"
|
||||
"forge.lthn.ai/core/go-blockchain/config"
|
||||
"forge.lthn.ai/core/go-blockchain/p2p"
|
||||
levin "forge.lthn.ai/core/go-p2p/node/levin"
|
||||
)
|
||||
|
||||
func syncLoop(ctx context.Context, c *chain.Chain, cfg *config.ChainConfig, forks []config.HardFork, seed string) {
|
||||
opts := chain.SyncOptions{
|
||||
VerifySignatures: false,
|
||||
Forks: forks,
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
if err := syncOnce(ctx, c, cfg, opts, seed); err != nil {
|
||||
log.Printf("sync: %v (retrying in 10s)", err)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-time.After(10 * time.Second):
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-time.After(30 * time.Second):
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func syncOnce(ctx context.Context, c *chain.Chain, cfg *config.ChainConfig, opts chain.SyncOptions, seed string) error {
|
||||
conn, err := net.DialTimeout("tcp", seed, 10*time.Second)
|
||||
if err != nil {
|
||||
return fmt.Errorf("dial %s: %w", seed, err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
lc := levin.NewConnection(conn)
|
||||
|
||||
var peerIDBuf [8]byte
|
||||
rand.Read(peerIDBuf[:])
|
||||
peerID := binary.LittleEndian.Uint64(peerIDBuf[:])
|
||||
|
||||
localHeight, _ := c.Height()
|
||||
|
||||
req := p2p.HandshakeRequest{
|
||||
NodeData: p2p.NodeData{
|
||||
NetworkID: cfg.NetworkID,
|
||||
PeerID: peerID,
|
||||
LocalTime: time.Now().Unix(),
|
||||
MyPort: 0,
|
||||
},
|
||||
PayloadData: p2p.CoreSyncData{
|
||||
CurrentHeight: localHeight,
|
||||
ClientVersion: config.ClientVersion,
|
||||
NonPruningMode: true,
|
||||
},
|
||||
}
|
||||
payload, err := p2p.EncodeHandshakeRequest(&req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("encode handshake: %w", err)
|
||||
}
|
||||
if err := lc.WritePacket(p2p.CommandHandshake, payload, true); err != nil {
|
||||
return fmt.Errorf("write handshake: %w", err)
|
||||
}
|
||||
|
||||
hdr, data, err := lc.ReadPacket()
|
||||
if err != nil {
|
||||
return fmt.Errorf("read handshake: %w", err)
|
||||
}
|
||||
if hdr.Command != uint32(p2p.CommandHandshake) {
|
||||
return fmt.Errorf("unexpected command %d", hdr.Command)
|
||||
}
|
||||
|
||||
var resp p2p.HandshakeResponse
|
||||
if err := resp.Decode(data); err != nil {
|
||||
return fmt.Errorf("decode handshake: %w", err)
|
||||
}
|
||||
|
||||
localSync := p2p.CoreSyncData{
|
||||
CurrentHeight: localHeight,
|
||||
ClientVersion: config.ClientVersion,
|
||||
NonPruningMode: true,
|
||||
}
|
||||
p2pConn := chain.NewLevinP2PConn(lc, resp.PayloadData.CurrentHeight, localSync)
|
||||
|
||||
return c.P2PSync(ctx, p2pConn, opts)
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue