From 7c6645fbebb30c55229cd40b63924072ca18d11e Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 2 Apr 2026 07:24:28 +0100 Subject: [PATCH] feat(node): add Lethean chain discovery for mining fleet MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Chain integration for P2P mining nodes: - GetChainInfo — query daemon for height, aliases, sync status - DiscoverPools — find pool aliases from chain (cap=pool) - DiscoverGateways — find gateway nodes from chain - parseComment — v=lthn1 comment parser Constants: testnet/mainnet daemon URLs and pool endpoints. 4/4 tests passing. Co-Authored-By: Charon --- pkg/node/lethean.go | 153 +++++++++++++++++++++++++++++++++++++++ pkg/node/lethean_test.go | 47 ++++++++++++ 2 files changed, 200 insertions(+) create mode 100644 pkg/node/lethean.go create mode 100644 pkg/node/lethean_test.go diff --git a/pkg/node/lethean.go b/pkg/node/lethean.go new file mode 100644 index 0000000..64adb1b --- /dev/null +++ b/pkg/node/lethean.go @@ -0,0 +1,153 @@ +// Copyright (c) 2017-2026 Lethean (https://lt.hn) +// SPDX-License-Identifier: EUPL-1.2 + +package node + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "strings" +) + +// LetheanDefaults for chain connectivity. +const ( + LetheanTestnetDaemon = "http://127.0.0.1:46941" + LetheanMainnetDaemon = "http://127.0.0.1:36941" + LetheanTestnetPool = "stratum+tcp://pool.lthn.io:5555" + LetheanMainnetPool = "stratum+tcp://pool.lthn.io:3333" +) + +// ChainInfo holds basic chain state from the daemon RPC. +// +// info, err := node.GetChainInfo("http://127.0.0.1:46941") +type ChainInfo struct { + Height uint64 `json:"height"` + Aliases int `json:"alias_count"` + Synced bool `json:"synced"` + PoolSize int `json:"tx_pool_size"` + Difficulty uint64 `json:"difficulty"` +} + +// PoolGateway is a mining pool discovered from chain aliases. +// +// pools := node.DiscoverPools("http://127.0.0.1:46941") +type PoolGateway struct { + Name string `json:"name"` + Address string `json:"address"` + Caps string `json:"caps"` + Endpoint string `json:"endpoint"` +} + +// GetChainInfo queries the daemon for basic chain state. +// +// info, err := node.GetChainInfo("http://127.0.0.1:46941") +func GetChainInfo(daemonURL string) (*ChainInfo, error) { + body := `{"jsonrpc":"2.0","id":"0","method":"getinfo","params":{}}` + resp, err := http.Post(daemonURL+"/json_rpc", "application/json", strings.NewReader(body)) + if err != nil { + return nil, fmt.Errorf("daemon RPC failed: %w", err) + } + defer resp.Body.Close() + raw, _ := io.ReadAll(resp.Body) + + var result struct { + Result ChainInfo `json:"result"` + } + if err := json.Unmarshal(raw, &result); err != nil { + return nil, fmt.Errorf("parse getinfo: %w", err) + } + result.Result.Synced = result.Result.Height > 0 + return &result.Result, nil +} + +// DiscoverPools queries the chain for aliases with cap=pool and returns +// pool endpoints that miners can connect to. +// +// pools := node.DiscoverPools("http://127.0.0.1:46941") +func DiscoverPools(daemonURL string) []PoolGateway { + body := `{"jsonrpc":"2.0","id":"0","method":"get_all_alias_details","params":{}}` + resp, err := http.Post(daemonURL+"/json_rpc", "application/json", strings.NewReader(body)) + if err != nil { + return nil + } + defer resp.Body.Close() + raw, _ := io.ReadAll(resp.Body) + + var result struct { + Result struct { + Aliases []struct { + Alias string `json:"alias"` + Address string `json:"address"` + Comment string `json:"comment"` + } `json:"aliases"` + } `json:"result"` + } + json.Unmarshal(raw, &result) + + var pools []PoolGateway + for _, a := range result.Result.Aliases { + if !strings.Contains(a.Comment, "pool") { + continue + } + parsed := parseComment(a.Comment) + pools = append(pools, PoolGateway{ + Name: a.Alias, + Address: a.Address, + Caps: parsed["cap"], + Endpoint: fmt.Sprintf("stratum+tcp://%s.lthn:5555", a.Alias), + }) + } + return pools +} + +// DiscoverGateways returns all gateway nodes from chain aliases. +// +// gateways := node.DiscoverGateways("http://127.0.0.1:46941") +func DiscoverGateways(daemonURL string) []PoolGateway { + body := `{"jsonrpc":"2.0","id":"0","method":"get_all_alias_details","params":{}}` + resp, err := http.Post(daemonURL+"/json_rpc", "application/json", strings.NewReader(body)) + if err != nil { + return nil + } + defer resp.Body.Close() + raw, _ := io.ReadAll(resp.Body) + + var result struct { + Result struct { + Aliases []struct { + Alias string `json:"alias"` + Address string `json:"address"` + Comment string `json:"comment"` + } `json:"aliases"` + } `json:"result"` + } + json.Unmarshal(raw, &result) + + var gateways []PoolGateway + for _, a := range result.Result.Aliases { + if !strings.Contains(a.Comment, "type=gateway") { + continue + } + parsed := parseComment(a.Comment) + gateways = append(gateways, PoolGateway{ + Name: a.Alias, + Address: a.Address, + Caps: parsed["cap"], + Endpoint: fmt.Sprintf("%s.lthn", a.Alias), + }) + } + return gateways +} + +func parseComment(comment string) map[string]string { + result := make(map[string]string) + for _, part := range strings.Split(comment, ";") { + idx := strings.IndexByte(part, '=') + if idx > 0 { + result[part[:idx]] = part[idx+1:] + } + } + return result +} diff --git a/pkg/node/lethean_test.go b/pkg/node/lethean_test.go new file mode 100644 index 0000000..6a0da09 --- /dev/null +++ b/pkg/node/lethean_test.go @@ -0,0 +1,47 @@ +// Copyright (c) 2017-2026 Lethean (https://lt.hn) +// SPDX-License-Identifier: EUPL-1.2 + +package node + +import ( + "testing" +) + +func TestParseComment_Good(t *testing.T) { + tests := []struct { + input string + key string + want string + }{ + {"v=lthn1;type=gateway;cap=vpn,dns", "type", "gateway"}, + {"v=lthn1;cap=pool", "cap", "pool"}, + {"v=lthn1", "v", "lthn1"}, + } + for _, tt := range tests { + result := parseComment(tt.input) + if result[tt.key] != tt.want { + t.Errorf("parseComment(%q)[%q] = %q, want %q", tt.input, tt.key, result[tt.key], tt.want) + } + } +} + +func TestGetChainInfo_Bad_WrongURL(t *testing.T) { + _, err := GetChainInfo("http://127.0.0.1:19999") + if err == nil { + t.Error("expected error for unreachable daemon") + } +} + +func TestDiscoverPools_Bad_WrongURL(t *testing.T) { + pools := DiscoverPools("http://127.0.0.1:19999") + if len(pools) != 0 { + t.Errorf("expected 0 pools for unreachable daemon, got %d", len(pools)) + } +} + +func TestDiscoverGateways_Bad_WrongURL(t *testing.T) { + gateways := DiscoverGateways("http://127.0.0.1:19999") + if len(gateways) != 0 { + t.Errorf("expected 0 gateways for unreachable daemon, got %d", len(gateways)) + } +}