fix(proxy): align runtime with RFC

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-04-04 18:38:28 +00:00
parent 64443c41f6
commit 6f4d7019e2
5 changed files with 167 additions and 24 deletions

View file

@ -216,7 +216,12 @@ func (m *NonceMapper) Submit(event *proxy.SubmitEvent) {
return
}
seq := m.strategy.Submit(jobID, event.Nonce, event.Result, event.Algo)
m.pending[seq] = SubmitContext{RequestID: event.RequestID, MinerID: event.Miner.ID(), JobID: jobID}
m.pending[seq] = SubmitContext{
RequestID: event.RequestID,
MinerID: event.Miner.ID(),
JobID: jobID,
StartedAt: time.Now(),
}
m.lastUsed = time.Now()
}
@ -263,16 +268,25 @@ func (m *NonceMapper) OnResultAccepted(sequence int64, accepted bool, errorMessa
if !ok || miner == nil {
return
}
latency := uint16(0)
if !ctx.StartedAt.IsZero() {
elapsed := time.Since(ctx.StartedAt).Milliseconds()
if elapsed > int64(^uint16(0)) {
latency = ^uint16(0)
} else {
latency = uint16(elapsed)
}
}
if accepted {
miner.Success(ctx.RequestID, "OK")
if m.events != nil {
m.events.Dispatch(proxy.Event{Type: proxy.EventAccept, Miner: miner, Job: &job, Diff: job.DifficultyFromTarget(), Latency: 0, Expired: expired})
m.events.Dispatch(proxy.Event{Type: proxy.EventAccept, Miner: miner, Job: &job, Diff: job.DifficultyFromTarget(), Latency: latency, Expired: expired})
}
return
}
miner.ReplyWithError(ctx.RequestID, errorMessage)
if m.events != nil {
m.events.Dispatch(proxy.Event{Type: proxy.EventReject, Miner: miner, Job: &job, Diff: job.DifficultyFromTarget(), Error: errorMessage})
m.events.Dispatch(proxy.Event{Type: proxy.EventReject, Miner: miner, Job: &job, Diff: job.DifficultyFromTarget(), Error: errorMessage, Latency: latency})
}
}

View file

@ -33,4 +33,5 @@ type SubmitContext struct {
RequestID int64 // JSON-RPC id from the miner's submit request
MinerID int64 // miner that submitted
JobID string
StartedAt time.Time
}

View file

@ -136,8 +136,10 @@ func (s *SimpleSplitter) GC() {
}
}
// Tick is a no-op for simple mode.
func (s *SimpleSplitter) Tick(ticks uint64) {}
// Tick advances timeout checks in simple mode.
func (s *SimpleSplitter) Tick(ticks uint64) {
s.GC()
}
// Upstreams returns active/idle/error counts.
func (s *SimpleSplitter) Upstreams() proxy.UpstreamStats {
@ -159,7 +161,7 @@ func (s *SimpleSplitter) newMapperLocked() *SimpleMapper {
mapper := &SimpleMapper{
id: id,
events: s.events,
pending: make(map[int64]*proxy.SubmitEvent),
pending: make(map[int64]submitContext),
}
mapper.strategy = s.factory(mapper)
if mapper.strategy == nil {
@ -176,7 +178,7 @@ func (m *SimpleMapper) Submit(event *proxy.SubmitEvent) {
m.mu.Lock()
defer m.mu.Unlock()
seq := m.strategy.Submit(event.JobID, event.Nonce, event.Result, event.Algo)
m.pending[seq] = event
m.pending[seq] = submitContext{RequestID: event.RequestID, StartedAt: time.Now()}
}
// OnJob forwards the latest pool job to the active miner.
@ -200,25 +202,36 @@ func (m *SimpleMapper) OnResultAccepted(sequence int64, accepted bool, errorMess
return
}
m.mu.Lock()
ctx := m.pending[sequence]
delete(m.pending, sequence)
ctx, ok := m.pending[sequence]
if ok {
delete(m.pending, sequence)
}
miner := m.miner
m.mu.Unlock()
if ctx == nil || miner == nil {
if !ok || miner == nil {
return
}
latency := uint16(0)
if !ctx.StartedAt.IsZero() {
elapsed := time.Since(ctx.StartedAt).Milliseconds()
if elapsed > int64(^uint16(0)) {
latency = ^uint16(0)
} else {
latency = uint16(elapsed)
}
}
if accepted {
miner.Success(ctx.RequestID, "OK")
if m.events != nil {
job := miner.CurrentJob()
m.events.Dispatch(proxy.Event{Type: proxy.EventAccept, Miner: miner, Diff: job.DifficultyFromTarget(), Job: &job})
m.events.Dispatch(proxy.Event{Type: proxy.EventAccept, Miner: miner, Diff: job.DifficultyFromTarget(), Job: &job, Latency: latency})
}
return
}
miner.ReplyWithError(ctx.RequestID, errorMessage)
if m.events != nil {
job := miner.CurrentJob()
m.events.Dispatch(proxy.Event{Type: proxy.EventReject, Miner: miner, Diff: job.DifficultyFromTarget(), Job: &job, Error: errorMessage})
m.events.Dispatch(proxy.Event{Type: proxy.EventReject, Miner: miner, Diff: job.DifficultyFromTarget(), Job: &job, Error: errorMessage, Latency: latency})
}
}

View file

@ -21,6 +21,11 @@ type SimpleMapper struct {
idleAt time.Time // zero when active
stopped bool
events *proxy.EventBus
pending map[int64]*proxy.SubmitEvent
pending map[int64]submitContext
mu sync.Mutex
}
type submitContext struct {
RequestID int64
StartedAt time.Time
}

View file

@ -6,7 +6,6 @@ import (
"crypto/tls"
"encoding/json"
"errors"
"io"
"net"
"net/http"
"sort"
@ -120,7 +119,7 @@ func (p *Proxy) MinerSnapshots() []MinerSnapshot {
TX: miner.tx,
RX: miner.rx,
State: miner.state,
Diff: miner.customDiff,
Diff: miner.diff,
User: miner.user,
Password: "********",
RigID: miner.rigID,
@ -147,6 +146,14 @@ func (p *Proxy) Upstreams() UpstreamStats {
return p.splitter.Upstreams()
}
// Events returns the proxy event bus for external composition.
func (p *Proxy) Events() *EventBus {
if p == nil {
return nil
}
return p.events
}
// Start starts the TCP listeners, ticker loop, and optional HTTP API.
func (p *Proxy) Start() {
if p == nil {
@ -300,7 +307,100 @@ func buildTLSConfig(cfg TLSConfig) *tls.Config {
if err != nil {
return nil
}
return &tls.Config{Certificates: []tls.Certificate{cert}}
tlsConfig := &tls.Config{Certificates: []tls.Certificate{cert}}
applyTLSProtocols(tlsConfig, cfg.Protocols)
applyTLSCiphers(tlsConfig, cfg.Ciphers)
return tlsConfig
}
func applyTLSProtocols(tlsConfig *tls.Config, protocols string) {
if tlsConfig == nil || strings.TrimSpace(protocols) == "" {
return
}
parts := splitTLSConfigList(protocols)
minVersion := uint16(0)
maxVersion := uint16(0)
for _, part := range parts {
if part == "" {
continue
}
if strings.Contains(part, "-") {
bounds := strings.SplitN(part, "-", 2)
low := parseTLSVersion(bounds[0])
high := parseTLSVersion(bounds[1])
if low == 0 || high == 0 {
continue
}
if minVersion == 0 || low < minVersion {
minVersion = low
}
if high > maxVersion {
maxVersion = high
}
continue
}
version := parseTLSVersion(part)
if version == 0 {
continue
}
if minVersion == 0 || version < minVersion {
minVersion = version
}
if version > maxVersion {
maxVersion = version
}
}
if minVersion != 0 {
tlsConfig.MinVersion = minVersion
}
if maxVersion != 0 {
tlsConfig.MaxVersion = maxVersion
}
}
func applyTLSCiphers(tlsConfig *tls.Config, ciphers string) {
if tlsConfig == nil || strings.TrimSpace(ciphers) == "" {
return
}
allowed := map[string]uint16{}
for _, suite := range tls.CipherSuites() {
allowed[strings.ToLower(suite.Name)] = suite.ID
}
for _, suite := range tls.InsecureCipherSuites() {
allowed[strings.ToLower(suite.Name)] = suite.ID
}
parts := splitTLSConfigList(ciphers)
for _, part := range parts {
if id, ok := allowed[strings.ToLower(part)]; ok {
tlsConfig.CipherSuites = append(tlsConfig.CipherSuites, id)
}
}
}
func splitTLSConfigList(value string) []string {
return strings.FieldsFunc(value, func(r rune) bool {
switch r {
case ',', ';', ':', '|', ' ':
return true
default:
return false
}
})
}
func parseTLSVersion(value string) uint16 {
switch strings.ToLower(strings.TrimSpace(value)) {
case "tls1.0", "tlsv1.0", "tls1", "tlsv1", "1.0", "1", "tls10", "tlsv10":
return tls.VersionTLS10
case "tls1.1", "tlsv1.1", "1.1", "tls11", "tlsv11":
return tls.VersionTLS11
case "tls1.2", "tlsv1.2", "1.2", "tls12", "tlsv12":
return tls.VersionTLS12
case "tls1.3", "tlsv1.3", "1.3", "tls13", "tlsv13":
return tls.VersionTLS13
default:
return 0
}
}
func (p *Proxy) startHTTP() {
@ -551,12 +651,7 @@ func (m *Miner) readLoop() {
}
line, isPrefix, err := reader.ReadLine()
if err != nil {
if ne, ok := err.(net.Error); ok && ne.Timeout() {
return
}
if err != io.EOF {
return
}
m.Close()
return
}
if isPrefix {
@ -569,6 +664,7 @@ func (m *Miner) readLoop() {
m.rx += uint64(len(line) + 1)
m.lastActivityAt = time.Now().UTC()
if !m.handleLine(line) {
m.Close()
return
}
}
@ -695,11 +791,11 @@ func (m *Miner) handleSubmit(req stratumRequest) {
RequestID: requestID(req.ID),
})
}
m.lastActivityAt = time.Now().UTC()
m.touchActivity()
}
func (m *Miner) handleKeepalived(req stratumRequest) {
m.lastActivityAt = time.Now().UTC()
m.touchActivity()
m.Success(requestID(req.ID), "KEEPALIVED")
}
@ -736,6 +832,7 @@ func (m *Miner) ForwardJob(job Job, algo string) {
return
}
m.currentJob = job
m.diff = job.DifficultyFromTarget()
if algo == "" {
algo = job.Algo
}
@ -759,6 +856,7 @@ func (m *Miner) ForwardJob(job Job, algo string) {
payload["params"].(map[string]any)["algo"] = algo
}
_ = m.writeJSON(payload)
m.touchActivity()
if m.state == MinerStateWaitReady {
m.state = MinerStateReady
}
@ -780,6 +878,7 @@ func (m *Miner) replyLoginSuccess(id int64) {
if m.extNH {
blob = job.BlobWithFixedByte(m.fixedByte)
}
m.diff = job.DifficultyFromTarget()
jobPayload := map[string]any{
"blob": blob,
"job_id": job.JobID,
@ -792,6 +891,7 @@ func (m *Miner) replyLoginSuccess(id int64) {
jobPayload["algo"] = job.Algo
}
result["job"] = jobPayload
m.touchActivity()
m.state = MinerStateReady
}
payload := map[string]any{
@ -833,6 +933,16 @@ func (m *Miner) Success(id int64, status string) {
_ = m.writeJSON(payload)
}
func (m *Miner) touchActivity() {
if m == nil {
return
}
m.lastActivityAt = time.Now().UTC()
if m.conn != nil {
_ = m.conn.SetReadDeadline(time.Now().Add(600 * time.Second))
}
}
func (m *Miner) writeJSON(payload any) error {
m.sendMu.Lock()
defer m.sendMu.Unlock()