package proxy import ( "bufio" "encoding/json" "net" "strings" "testing" "time" ) func TestMiner_HandleLogin_Good(t *testing.T) { minerConn, clientConn := net.Pipe() defer minerConn.Close() defer clientConn.Close() miner := NewMiner(minerConn, 3333, nil) miner.algoEnabled = true miner.extNH = true miner.fixedByte = 0x2a miner.onLogin = func(m *Miner) { m.SetMapperID(1) } miner.currentJob = Job{ Blob: strings.Repeat("0", 160), JobID: "job-1", Target: "b88d0600", Algo: "cn/r", Height: 7, SeedHash: "seed", } params, err := json.Marshal(loginParams{ Login: "wallet", Pass: "x", Agent: "xmrig", Algo: []string{"cn/r"}, RigID: "rig-1", }) if err != nil { t.Fatalf("marshal login params: %v", err) } done := make(chan struct{}) go func() { miner.handleLogin(stratumRequest{ID: 1, Method: "login", Params: params}) close(done) }() line, err := bufio.NewReader(clientConn).ReadBytes('\n') if err != nil { t.Fatalf("read login response: %v", err) } <-done var payload struct { Error json.RawMessage `json:"error"` Result struct { ID string `json:"id"` Status string `json:"status"` Extensions []string `json:"extensions"` Job map[string]any `json:"job"` } `json:"result"` } if err := json.Unmarshal(line, &payload); err != nil { t.Fatalf("unmarshal login response: %v", err) } if string(payload.Error) != "null" { t.Fatalf("expected login response error to be null, got %s", string(payload.Error)) } if payload.Result.Status != "OK" { t.Fatalf("expected login success, got %q", payload.Result.Status) } if payload.Result.ID == "" { t.Fatalf("expected rpc id in login response") } if len(payload.Result.Extensions) != 1 || payload.Result.Extensions[0] != "algo" { t.Fatalf("expected algo extension, got %#v", payload.Result.Extensions) } if got := miner.LoginAlgos(); len(got) != 1 || got[0] != "cn/r" { t.Fatalf("expected login algo list to be stored, got %#v", got) } if got := payload.Result.Job["job_id"]; got != "job-1" { t.Fatalf("expected embedded job, got %#v", got) } if got := payload.Result.Job["algo"]; got != "cn/r" { t.Fatalf("expected embedded algo, got %#v", got) } blob, _ := payload.Result.Job["blob"].(string) if blob[78:80] != "2a" { t.Fatalf("expected fixed-byte patched blob, got %q", blob[78:80]) } if miner.State() != MinerStateReady { t.Fatalf("expected miner ready after login reply with job, got %d", miner.State()) } } func TestProxy_New_Watch_Good(t *testing.T) { cfg := &Config{ Mode: "nicehash", Workers: WorkersByRigID, Bind: []BindAddr{{Host: "127.0.0.1", Port: 3333}}, Pools: []PoolConfig{{URL: "pool.example:3333", Enabled: true}}, Watch: true, configPath: "/tmp/proxy.json", } proxyInstance, result := New(cfg) if !result.OK { t.Fatalf("expected valid proxy, got error: %v", result.Error) } if proxyInstance.watcher == nil { t.Fatalf("expected config watcher when watch is enabled and source path is known") } } func TestMiner_HandleLogin_Ugly(t *testing.T) { for i := 0; i < 256; i++ { miner := &Miner{} miner.SetID(int64(i + 1)) miner.SetMapperID(int64(i + 1)) } serverConn, clientConn := net.Pipe() defer serverConn.Close() defer clientConn.Close() miner := NewMiner(serverConn, 3333, nil) miner.extNH = true miner.onLogin = func(*Miner) {} params, err := json.Marshal(loginParams{ Login: "wallet", Pass: "x", }) if err != nil { t.Fatalf("marshal login params: %v", err) } done := make(chan []byte, 1) go func() { line, readErr := bufio.NewReader(clientConn).ReadBytes('\n') if readErr != nil { done <- nil return } done <- line }() miner.handleLogin(stratumRequest{ID: 2, Method: "login", Params: params}) line := <-done if line == nil { t.Fatal("expected login rejection response") } var payload struct { Error struct { Message string `json:"message"` } `json:"error"` Result map[string]any `json:"result"` } if err := json.Unmarshal(line, &payload); err != nil { t.Fatalf("unmarshal login response: %v", err) } if payload.Error.Message != "Proxy is full, try again later" { t.Fatalf("expected full-table error, got %q", payload.Error.Message) } if payload.Result != nil { t.Fatalf("expected no login success payload, got %#v", payload.Result) } if miner.MapperID() != -1 { t.Fatalf("expected rejected miner to remain unassigned, got mapper %d", miner.MapperID()) } } func TestMiner_HandleLogin_FailedAssignmentDoesNotDispatchLoginEvent(t *testing.T) { minerConn, clientConn := net.Pipe() defer minerConn.Close() defer clientConn.Close() proxyInstance := &Proxy{ config: &Config{ Mode: "nicehash", Workers: WorkersByUser, Bind: []BindAddr{{Host: "127.0.0.1", Port: 3333}}, Pools: []PoolConfig{{URL: "pool.example:3333", Enabled: true}}, }, events: NewEventBus(), stats: NewStats(), workers: NewWorkers(WorkersByUser, nil), miners: make(map[int64]*Miner), } proxyInstance.events.Subscribe(EventLogin, proxyInstance.stats.OnLogin) proxyInstance.workers.bindEvents(proxyInstance.events) miner := NewMiner(minerConn, 3333, nil) miner.extNH = true miner.onLogin = func(*Miner) {} miner.onLoginReady = func(m *Miner) { proxyInstance.events.Dispatch(Event{Type: EventLogin, Miner: m}) } proxyInstance.miners[miner.ID()] = miner params, err := json.Marshal(loginParams{ Login: "wallet", Pass: "x", }) if err != nil { t.Fatalf("marshal login params: %v", err) } go miner.handleLogin(stratumRequest{ID: 12, Method: "login", Params: params}) line, err := bufio.NewReader(clientConn).ReadBytes('\n') if err != nil { t.Fatalf("read login rejection: %v", err) } var payload struct { Error struct { Message string `json:"message"` } `json:"error"` } if err := json.Unmarshal(line, &payload); err != nil { t.Fatalf("unmarshal login rejection: %v", err) } if payload.Error.Message != "Proxy is full, try again later" { t.Fatalf("expected full-table rejection, got %q", payload.Error.Message) } if now, max := proxyInstance.MinerCount(); now != 0 || max != 0 { t.Fatalf("expected failed login not to affect miner counts, got now=%d max=%d", now, max) } if records := proxyInstance.WorkerRecords(); len(records) != 0 { t.Fatalf("expected failed login not to create worker records, got %d", len(records)) } } 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 = 5000 } miner.currentJob = Job{ Blob: strings.Repeat("0", 160), JobID: "job-1", Target: "01000000", } 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 > 5000 { t.Fatalf("expected capped difficulty at or below 5000, 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_HandleLogin_CustomDiffSuffix_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) } params, err := json.Marshal(loginParams{ Login: "wallet+50000", Pass: "x", }) if err != nil { t.Fatalf("marshal login params: %v", err) } go miner.handleLogin(stratumRequest{ID: 4, 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 { Status string `json:"status"` } `json:"result"` } if err := json.Unmarshal(line, &payload); err != nil { t.Fatalf("unmarshal login response: %v", err) } if payload.Result.Status != "OK" { t.Fatalf("expected login success, got %q", payload.Result.Status) } if got := miner.User(); got != "wallet" { t.Fatalf("expected stripped wallet name, got %q", got) } if got := miner.customDiff; got != 50000 { t.Fatalf("expected custom diff 50000, got %d", got) } } func TestMiner_HandleKeepalived_Good(t *testing.T) { minerConn, clientConn := net.Pipe() defer minerConn.Close() defer clientConn.Close() miner := NewMiner(minerConn, 3333, nil) done := make(chan struct{}) go func() { miner.handleKeepalived(stratumRequest{ID: 9, Method: "keepalived"}) close(done) }() line, err := bufio.NewReader(clientConn).ReadBytes('\n') if err != nil { t.Fatalf("read keepalived response: %v", err) } <-done var payload map[string]json.RawMessage if err := json.Unmarshal(line, &payload); err != nil { t.Fatalf("unmarshal keepalived response: %v", err) } if _, ok := payload["error"]; !ok { t.Fatalf("expected keepalived response to include error field, got %s", string(line)) } if string(payload["error"]) != "null" { t.Fatalf("expected keepalived response error to be null, got %s", string(payload["error"])) } var result struct { Status string `json:"status"` } if err := json.Unmarshal(payload["result"], &result); err != nil { t.Fatalf("unmarshal keepalived result: %v", err) } if result.Status != "KEEPALIVED" { t.Fatalf("expected KEEPALIVED status, got %q", result.Status) } } 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) } }