248 lines
6 KiB
Go
248 lines
6 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"
|
|
|
|
"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 {
|
|
cfg proxy.PoolConfig
|
|
listener StratumListener
|
|
conn net.Conn
|
|
tlsConn *tls.Conn // nil if plain TCP
|
|
sessionID string // pool-assigned session id from login reply
|
|
seq int64 // atomic JSON-RPC request id counter
|
|
active bool // true once first job received
|
|
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 stores the pool config and listener.
|
|
//
|
|
// client := pool.NewStratumClient(poolCfg, listener)
|
|
func NewStratumClient(cfg proxy.PoolConfig, listener StratumListener) *StratumClient {
|
|
return &StratumClient{
|
|
cfg: cfg,
|
|
listener: listener,
|
|
}
|
|
}
|
|
|
|
// Connect dials the pool. Applies TLS if cfg.TLS is true.
|
|
//
|
|
// errorValue := client.Connect()
|
|
func (c *StratumClient) Connect() error {
|
|
var connection net.Conn
|
|
var errorValue error
|
|
|
|
if c.cfg.TLS {
|
|
tlsConfig := &tls.Config{MinVersion: tls.VersionTLS12}
|
|
connection, errorValue = tls.Dial("tcp", c.cfg.URL, tlsConfig)
|
|
if errorValue != nil {
|
|
return errorValue
|
|
}
|
|
|
|
tlsConnection := connection.(*tls.Conn)
|
|
if c.cfg.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.cfg.TLSFingerprint) {
|
|
_ = connection.Close()
|
|
return errors.New("pool fingerprint mismatch")
|
|
}
|
|
}
|
|
c.tlsConn = tlsConnection
|
|
} else {
|
|
connection, errorValue = net.Dial("tcp", c.cfg.URL)
|
|
if errorValue != nil {
|
|
return errorValue
|
|
}
|
|
}
|
|
|
|
c.conn = connection
|
|
go c.readLoop()
|
|
return nil
|
|
}
|
|
|
|
// Login sends the stratum login request using cfg.User and cfg.Pass.
|
|
//
|
|
// client.Login()
|
|
func (c *StratumClient) Login() {
|
|
params := map[string]interface{}{
|
|
"login": c.cfg.User,
|
|
"pass": c.cfg.Pass,
|
|
"rigid": c.cfg.RigID,
|
|
}
|
|
if c.cfg.Algo != "" {
|
|
params["algo"] = []string{c.cfg.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(jobID, "deadbeef", "HASH64HEX", "cn/r")
|
|
func (c *StratumClient) Submit(jobID string, nonce string, result string, algo string) int64 {
|
|
sequence := atomic.AddInt64(&c.seq, 1)
|
|
params := map[string]string{
|
|
"id": c.sessionID,
|
|
"job_id": jobID,
|
|
"nonce": nonce,
|
|
"result": result,
|
|
}
|
|
if algo != "" {
|
|
params["algo"] = algo
|
|
}
|
|
|
|
_ = c.writeJSON(jsonRPCRequest{
|
|
ID: sequence,
|
|
Method: "submit",
|
|
Params: params,
|
|
})
|
|
|
|
return sequence
|
|
}
|
|
|
|
// Disconnect closes the connection cleanly. Triggers OnDisconnect on the listener.
|
|
//
|
|
// client.Disconnect()
|
|
func (c *StratumClient) Disconnect() {
|
|
if c.conn != nil {
|
|
_ = c.conn.Close()
|
|
}
|
|
if c.listener != nil {
|
|
c.listener.OnDisconnect()
|
|
}
|
|
}
|
|
|
|
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.NewReader(c.conn)
|
|
for {
|
|
line, errorValue := reader.ReadBytes('\n')
|
|
if errorValue != nil {
|
|
if c.listener != nil {
|
|
c.listener.OnDisconnect()
|
|
}
|
|
return
|
|
}
|
|
|
|
response := jsonRPCResponse{}
|
|
if errorValue = json.Unmarshal(line, &response); errorValue != nil {
|
|
continue
|
|
}
|
|
c.handleMessage(response)
|
|
}
|
|
}
|
|
|
|
func (c *StratumClient) handleMessage(response jsonRPCResponse) {
|
|
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() {
|
|
c.active = true
|
|
if c.listener != nil {
|
|
c.listener.OnJob(loginResult.Job)
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
if c.listener != nil {
|
|
accepted := response.Error == nil
|
|
errorMessage := ""
|
|
if response.Error != nil {
|
|
errorMessage = response.Error.Message
|
|
}
|
|
c.listener.OnResultAccepted(response.ID, accepted, errorMessage)
|
|
}
|
|
return
|
|
}
|
|
|
|
if response.Method == "job" {
|
|
var payload proxy.Job
|
|
if json.Unmarshal(response.Params, &payload) == nil && payload.IsValid() {
|
|
c.active = true
|
|
if c.listener != nil {
|
|
c.listener.OnJob(payload)
|
|
}
|
|
}
|
|
}
|
|
}
|