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:
Snider 2026-03-09 16:21:42 +00:00
parent d8c2e84115
commit aa3d8270a2
8 changed files with 470 additions and 13 deletions

18
cmd/core-chain/main.go Normal file
View 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
View 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
View 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
View 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
View 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
View file

@ -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
View file

@ -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
View 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)
}