Implement miner target capping and RFC line limits

This commit is contained in:
Virgil 2026-04-04 21:08:28 +00:00
parent 186524b3a8
commit 9028334d49
3 changed files with 204 additions and 29 deletions

View file

@ -1,6 +1,7 @@
package proxy
import (
"encoding/binary"
"crypto/rand"
"crypto/sha256"
"encoding/hex"
@ -171,6 +172,23 @@ func (j Job) DifficultyFromTarget() uint64 {
return uint64(math.MaxUint32 / uint64(target))
}
func targetFromDifficulty(diff uint64) string {
if diff <= 1 {
return "ffffffff"
}
maxTarget := uint64(math.MaxUint32)
target := (maxTarget + diff - 1) / diff
if target == 0 {
target = 1
}
if target > maxTarget {
target = maxTarget
}
var raw [4]byte
binary.LittleEndian.PutUint32(raw[:], uint32(target))
return hex.EncodeToString(raw[:])
}
// NewCustomDiff creates a login-time custom difficulty resolver.
func NewCustomDiff(globalDiff uint64) *CustomDiff {
return &CustomDiff{globalDiff: globalDiff}

View file

@ -6,6 +6,7 @@ import (
"net"
"strings"
"testing"
"time"
)
func TestMiner_HandleLogin_Good(t *testing.T) {
@ -169,3 +170,148 @@ func TestMiner_HandleLogin_Ugly(t *testing.T) {
t.Fatalf("expected rejected miner to remain unassigned, got mapper %d", miner.MapperID())
}
}
func TestMiner_HandleLogin_CustomDiffCap_Good(t *testing.T) {
minerConn, clientConn := net.Pipe()
defer minerConn.Close()
defer clientConn.Close()
miner := NewMiner(minerConn, 3333, nil)
miner.onLogin = func(m *Miner) {
m.SetRouteID(1)
m.customDiff = 50000
}
miner.currentJob = Job{
Blob: strings.Repeat("0", 160),
JobID: "job-1",
Target: targetFromDifficulty(100000),
}
params, err := json.Marshal(loginParams{
Login: "wallet",
Pass: "x",
})
if err != nil {
t.Fatalf("marshal login params: %v", err)
}
go miner.handleLogin(stratumRequest{ID: 3, Method: "login", Params: params})
line, err := bufio.NewReader(clientConn).ReadBytes('\n')
if err != nil {
t.Fatalf("read login response: %v", err)
}
var payload struct {
Result struct {
Job struct {
Target string `json:"target"`
} `json:"job"`
} `json:"result"`
}
if err := json.Unmarshal(line, &payload); err != nil {
t.Fatalf("unmarshal login response: %v", err)
}
originalDiff := miner.currentJob.DifficultyFromTarget()
cappedDiff := Job{Target: payload.Result.Job.Target}.DifficultyFromTarget()
if cappedDiff == 0 || cappedDiff > 50000 {
t.Fatalf("expected capped difficulty at or below 50000, got %d", cappedDiff)
}
if cappedDiff >= originalDiff {
t.Fatalf("expected lowered target difficulty below %d, got %d", originalDiff, cappedDiff)
}
if miner.diff != cappedDiff {
t.Fatalf("expected miner diff %d, got %d", cappedDiff, miner.diff)
}
}
func TestMiner_ReadLoop_RFCLineLimit_Good(t *testing.T) {
minerConn, clientConn := net.Pipe()
defer minerConn.Close()
defer clientConn.Close()
miner := NewMiner(minerConn, 3333, nil)
miner.onLogin = func(m *Miner) {
m.SetRouteID(1)
}
miner.Start()
params, err := json.Marshal(loginParams{
Login: "wallet",
Pass: "x",
Agent: strings.Repeat("a", 5000),
})
if err != nil {
t.Fatalf("marshal login params: %v", err)
}
request, err := json.Marshal(stratumRequest{ID: 4, Method: "login", Params: params})
if err != nil {
t.Fatalf("marshal request: %v", err)
}
if len(request) >= maxStratumLineLength {
t.Fatalf("expected test request below RFC limit, got %d bytes", len(request))
}
if _, err := clientConn.Write(append(request, '\n')); err != nil {
t.Fatalf("write login request: %v", err)
}
_ = clientConn.SetReadDeadline(time.Now().Add(time.Second))
line, err := bufio.NewReader(clientConn).ReadBytes('\n')
if err != nil {
t.Fatalf("read login response: %v", err)
}
if len(line) == 0 {
t.Fatal("expected login response for request under RFC limit")
}
}
func TestMiner_ReadLoop_RFCLineLimit_Ugly(t *testing.T) {
minerConn, clientConn := net.Pipe()
defer minerConn.Close()
defer clientConn.Close()
miner := NewMiner(minerConn, 3333, nil)
miner.Start()
params, err := json.Marshal(loginParams{
Login: "wallet",
Pass: "x",
Agent: strings.Repeat("b", maxStratumLineLength),
})
if err != nil {
t.Fatalf("marshal login params: %v", err)
}
request, err := json.Marshal(stratumRequest{ID: 5, Method: "login", Params: params})
if err != nil {
t.Fatalf("marshal request: %v", err)
}
if len(request) <= maxStratumLineLength {
t.Fatalf("expected test request above RFC limit, got %d bytes", len(request))
}
writeDone := make(chan error, 1)
go func() {
_, writeErr := clientConn.Write(append(request, '\n'))
writeDone <- writeErr
}()
var writeErr error
select {
case writeErr = <-writeDone:
case <-time.After(time.Second):
t.Fatal("timed out writing oversized request")
}
if writeErr == nil {
_ = clientConn.SetReadDeadline(time.Now().Add(time.Second))
line, err := bufio.NewReader(clientConn).ReadBytes('\n')
if err == nil || len(line) > 0 {
t.Fatalf("expected oversized request to close the connection, got line=%q err=%v", string(line), err)
}
return
}
if !strings.Contains(writeErr.Error(), "closed pipe") {
t.Fatalf("expected oversized request to close the connection, got write error %v", writeErr)
}
}

View file

@ -15,6 +15,8 @@ import (
"time"
)
const maxStratumLineLength = 16384
// MinerSnapshot is a serialisable view of one miner connection.
type MinerSnapshot struct {
ID int64
@ -826,7 +828,7 @@ func (m *Miner) readLoop() {
}
}()
reader := bufio.NewReader(m.conn)
reader := bufio.NewReaderSize(m.conn, maxStratumLineLength+1)
for {
if m.state == MinerStateClosing {
return
@ -839,7 +841,7 @@ func (m *Miner) readLoop() {
m.Close()
return
}
if isPrefix {
if isPrefix || len(line) > maxStratumLineLength {
m.Close()
return
}
@ -1034,28 +1036,21 @@ func (m *Miner) ForwardJob(job Job, algo string) {
return
}
m.currentJob = job
m.diff = job.DifficultyFromTarget()
if algo == "" {
algo = job.Algo
}
blob := job.Blob
if m.extNH {
blob = job.BlobWithFixedByte(m.fixedByte)
}
renderedJob, effectiveAlgo := m.renderJob(job, algo)
payload := map[string]any{
"jsonrpc": "2.0",
"method": "job",
"params": map[string]any{
"blob": blob,
"job_id": job.JobID,
"target": job.Target,
"blob": renderedJob.Blob,
"job_id": renderedJob.JobID,
"target": renderedJob.Target,
"id": m.rpcID,
"height": job.Height,
"seed_hash": job.SeedHash,
"height": renderedJob.Height,
"seed_hash": renderedJob.SeedHash,
},
}
if m.supportsAlgoExtension() && algo != "" {
payload["params"].(map[string]any)["algo"] = algo
if m.supportsAlgoExtension() && effectiveAlgo != "" {
payload["params"].(map[string]any)["algo"] = effectiveAlgo
}
_ = m.writeJSON(payload)
m.touchActivity()
@ -1076,21 +1071,17 @@ func (m *Miner) replyLoginSuccess(id int64) {
result["extensions"] = []string{"algo"}
}
if job := m.CurrentJob(); job.IsValid() {
blob := job.Blob
if m.extNH {
blob = job.BlobWithFixedByte(m.fixedByte)
}
m.diff = job.DifficultyFromTarget()
renderedJob, effectiveAlgo := m.renderJob(job, job.Algo)
jobPayload := map[string]any{
"blob": blob,
"job_id": job.JobID,
"target": job.Target,
"blob": renderedJob.Blob,
"job_id": renderedJob.JobID,
"target": renderedJob.Target,
"id": m.rpcID,
"height": job.Height,
"seed_hash": job.SeedHash,
"height": renderedJob.Height,
"seed_hash": renderedJob.SeedHash,
}
if m.supportsAlgoExtension() && job.Algo != "" {
jobPayload["algo"] = job.Algo
if m.supportsAlgoExtension() && effectiveAlgo != "" {
jobPayload["algo"] = effectiveAlgo
}
result["job"] = jobPayload
m.touchActivity()
@ -1105,6 +1096,26 @@ func (m *Miner) replyLoginSuccess(id int64) {
_ = m.writeJSON(payload)
}
func (m *Miner) renderJob(job Job, algo string) (Job, string) {
if m == nil {
return job, algo
}
rendered := job
if algo == "" {
algo = job.Algo
}
if m.extNH {
rendered.Blob = job.BlobWithFixedByte(m.fixedByte)
}
effectiveDiff := job.DifficultyFromTarget()
if m.customDiff > 0 && effectiveDiff > 0 && effectiveDiff > m.customDiff {
rendered.Target = targetFromDifficulty(m.customDiff)
effectiveDiff = rendered.DifficultyFromTarget()
}
m.diff = effectiveDiff
return rendered, algo
}
func (m *Miner) ReplyWithError(id int64, message string) {
if m == nil {
return