Implement miner target capping and RFC line limits
This commit is contained in:
parent
186524b3a8
commit
9028334d49
3 changed files with 204 additions and 29 deletions
18
core_impl.go
18
core_impl.go
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue