288 lines
7.1 KiB
Go
288 lines
7.1 KiB
Go
// Package pool implements the outbound stratum pool client and failover strategy.
|
|
//
|
|
// client := pool.NewStratumClient(poolCfg, listener)
|
|
// client.Connect()
|
|
package pool
|
|
|
|
import (
|
|
"bufio"
|
|
"crypto/sha256"
|
|
"crypto/tls"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"net"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"dappco.re/go/core/proxy"
|
|
)
|
|
|
|
// StratumClient is one outbound stratum TCP (optionally TLS) connection to a pool.
|
|
// The proxy presents itself to the pool as a standard stratum miner using the
|
|
// wallet address and password from PoolConfig.
|
|
//
|
|
// client := pool.NewStratumClient(poolCfg, listener)
|
|
// client.Connect()
|
|
type StratumClient struct {
|
|
config proxy.PoolConfig
|
|
listener StratumListener
|
|
conn net.Conn
|
|
tlsConn *tls.Conn // nil if plain TCP
|
|
sessionID string // pool-assigned session id from login reply
|
|
requestSequence int64 // atomic JSON-RPC request id counter
|
|
active bool // true once first job received
|
|
disconnectOnce sync.Once
|
|
sendMu sync.Mutex
|
|
}
|
|
|
|
// StratumListener receives events from the pool connection.
|
|
type StratumListener interface {
|
|
// OnJob is called when the pool pushes a new job notification or the login reply contains a job.
|
|
OnJob(job proxy.Job)
|
|
// OnResultAccepted is called when the pool accepts or rejects a submitted share.
|
|
// sequence matches the value returned by Submit(). errorMessage is "" on accept.
|
|
OnResultAccepted(sequence int64, accepted bool, errorMessage string)
|
|
// OnDisconnect is called when the pool TCP connection closes for any reason.
|
|
OnDisconnect()
|
|
}
|
|
|
|
type jsonRPCRequest struct {
|
|
ID int64 `json:"id"`
|
|
Method string `json:"method"`
|
|
Params interface{} `json:"params,omitempty"`
|
|
}
|
|
|
|
type jsonRPCResponse struct {
|
|
ID int64 `json:"id"`
|
|
Method string `json:"method"`
|
|
Params json.RawMessage `json:"params"`
|
|
Result json.RawMessage `json:"result"`
|
|
Error *jsonRPCErrorBody `json:"error"`
|
|
}
|
|
|
|
type jsonRPCErrorBody struct {
|
|
Code int `json:"code"`
|
|
Message string `json:"message"`
|
|
}
|
|
|
|
// NewStratumClient builds one outbound pool client.
|
|
//
|
|
// client := pool.NewStratumClient(proxy.PoolConfig{URL: "pool.lthn.io:3333", User: "WALLET", Pass: "x"}, listener)
|
|
func NewStratumClient(cfg proxy.PoolConfig, listener StratumListener) *StratumClient {
|
|
return &StratumClient{
|
|
config: cfg,
|
|
listener: listener,
|
|
}
|
|
}
|
|
|
|
// Connect dials the pool and starts the read loop.
|
|
//
|
|
// client := pool.NewStratumClient(proxy.PoolConfig{URL: "pool.lthn.io:3333", TLS: true}, listener)
|
|
// errorValue := client.Connect()
|
|
func (c *StratumClient) Connect() error {
|
|
var connection net.Conn
|
|
var errorValue error
|
|
dialer := net.Dialer{}
|
|
if c.config.Keepalive {
|
|
dialer.KeepAlive = 30 * time.Second
|
|
}
|
|
|
|
if c.config.TLS {
|
|
connection, errorValue = dialer.Dial("tcp", c.config.URL)
|
|
if errorValue != nil {
|
|
return errorValue
|
|
}
|
|
|
|
serverName := c.config.URL
|
|
if host, _, splitError := net.SplitHostPort(c.config.URL); splitError == nil && host != "" {
|
|
serverName = host
|
|
}
|
|
|
|
tlsConnection := tls.Client(connection, &tls.Config{MinVersion: tls.VersionTLS12, ServerName: serverName})
|
|
errorValue = tlsConnection.Handshake()
|
|
if errorValue != nil {
|
|
_ = connection.Close()
|
|
return errorValue
|
|
}
|
|
if c.config.TLSFingerprint != "" {
|
|
state := tlsConnection.ConnectionState()
|
|
if len(state.PeerCertificates) == 0 {
|
|
_ = connection.Close()
|
|
return errors.New("missing peer certificate")
|
|
}
|
|
|
|
fingerprint := sha256.Sum256(state.PeerCertificates[0].Raw)
|
|
if hex.EncodeToString(fingerprint[:]) != strings.ToLower(c.config.TLSFingerprint) {
|
|
_ = connection.Close()
|
|
return errors.New("pool fingerprint mismatch")
|
|
}
|
|
}
|
|
connection = tlsConnection
|
|
c.tlsConn = tlsConnection
|
|
} else {
|
|
connection, errorValue = dialer.Dial("tcp", c.config.URL)
|
|
if errorValue != nil {
|
|
return errorValue
|
|
}
|
|
}
|
|
|
|
c.conn = connection
|
|
c.disconnectOnce = sync.Once{}
|
|
go c.readLoop()
|
|
return nil
|
|
}
|
|
|
|
// Login sends the stratum login request using the configured wallet and password.
|
|
//
|
|
// client.Login()
|
|
func (c *StratumClient) Login() {
|
|
password := c.config.Password
|
|
if password == "" {
|
|
password = c.config.Pass
|
|
}
|
|
|
|
params := map[string]interface{}{
|
|
"login": c.config.User,
|
|
"pass": password,
|
|
"rigid": c.config.RigID,
|
|
}
|
|
if c.config.Algo != "" {
|
|
params["algo"] = []string{c.config.Algo}
|
|
}
|
|
|
|
_ = c.writeJSON(jsonRPCRequest{
|
|
ID: 1,
|
|
Method: "login",
|
|
Params: params,
|
|
})
|
|
}
|
|
|
|
// Submit sends a share submission. Returns the sequence number for result correlation.
|
|
//
|
|
// seq := client.Submit("job-1", "deadbeef", "HASH64HEX", "cn/r")
|
|
func (c *StratumClient) Submit(jobID string, nonce string, result string, algo string) int64 {
|
|
requestID := atomic.AddInt64(&c.requestSequence, 1)
|
|
params := map[string]string{
|
|
"id": c.sessionID,
|
|
"job_id": jobID,
|
|
"nonce": nonce,
|
|
"result": result,
|
|
}
|
|
if algo != "" {
|
|
params["algo"] = algo
|
|
}
|
|
|
|
_ = c.writeJSON(jsonRPCRequest{
|
|
ID: requestID,
|
|
Method: "submit",
|
|
Params: params,
|
|
})
|
|
|
|
return requestID
|
|
}
|
|
|
|
// Disconnect closes the connection and emits one disconnect callback.
|
|
//
|
|
// client.Disconnect()
|
|
func (c *StratumClient) Disconnect() {
|
|
if c.conn != nil {
|
|
_ = c.conn.Close()
|
|
}
|
|
c.notifyDisconnect()
|
|
}
|
|
|
|
func (c *StratumClient) writeJSON(value interface{}) error {
|
|
if c.conn == nil {
|
|
return nil
|
|
}
|
|
|
|
data, errorValue := json.Marshal(value)
|
|
if errorValue != nil {
|
|
return errorValue
|
|
}
|
|
|
|
c.sendMu.Lock()
|
|
defer c.sendMu.Unlock()
|
|
_, errorValue = c.conn.Write(append(data, '\n'))
|
|
return errorValue
|
|
}
|
|
|
|
func (c *StratumClient) readLoop() {
|
|
reader := bufio.NewReaderSize(c.conn, 16384)
|
|
for {
|
|
line, isPrefix, errorValue := reader.ReadLine()
|
|
if errorValue != nil {
|
|
c.notifyDisconnect()
|
|
return
|
|
}
|
|
if isPrefix {
|
|
c.notifyDisconnect()
|
|
return
|
|
}
|
|
|
|
response := jsonRPCResponse{}
|
|
if errorValue = json.Unmarshal(line, &response); errorValue != nil {
|
|
continue
|
|
}
|
|
c.handleMessage(response)
|
|
}
|
|
}
|
|
|
|
func (c *StratumClient) notifyDisconnect() {
|
|
c.disconnectOnce.Do(func() {
|
|
if c.listener != nil {
|
|
c.listener.OnDisconnect()
|
|
}
|
|
})
|
|
}
|
|
|
|
func (c *StratumClient) handleMessage(response jsonRPCResponse) {
|
|
if response.Method == "job" {
|
|
var payload proxy.Job
|
|
if json.Unmarshal(response.Params, &payload) == nil && payload.IsValid() {
|
|
payload.ClientID = c.sessionID
|
|
c.active = true
|
|
if c.listener != nil {
|
|
c.listener.OnJob(payload)
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
if response.ID == 1 && c.sessionID == "" {
|
|
if len(response.Result) > 0 {
|
|
var loginResult struct {
|
|
ID string `json:"id"`
|
|
Job proxy.Job `json:"job"`
|
|
}
|
|
if json.Unmarshal(response.Result, &loginResult) == nil && loginResult.ID != "" {
|
|
c.sessionID = loginResult.ID
|
|
if loginResult.Job.IsValid() {
|
|
loginResult.Job.ClientID = c.sessionID
|
|
c.active = true
|
|
if c.listener != nil {
|
|
c.listener.OnJob(loginResult.Job)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if response.Error != nil {
|
|
c.Disconnect()
|
|
}
|
|
return
|
|
}
|
|
|
|
if response.ID == 0 || c.listener == nil {
|
|
return
|
|
}
|
|
|
|
accepted := response.Error == nil
|
|
errorMessage := ""
|
|
if response.Error != nil {
|
|
errorMessage = response.Error.Message
|
|
}
|
|
c.listener.OnResultAccepted(response.ID, accepted, errorMessage)
|
|
}
|