feat(node): add Lethean chain discovery for mining fleet
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 <charon@lethean.io>
This commit is contained in:
parent
67698256fd
commit
7c6645fbeb
2 changed files with 200 additions and 0 deletions
153
pkg/node/lethean.go
Normal file
153
pkg/node/lethean.go
Normal file
|
|
@ -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
|
||||
}
|
||||
47
pkg/node/lethean_test.go
Normal file
47
pkg/node/lethean_test.go
Normal file
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue