Skip to content

Commit ce719f1

Browse files
committed
Call miner_start with no parameters for the cores and resume controls
The node's miner_start takes no core count and its miner API offers no call to set one, so passing the count made it answer "Invalid number of input parameters" and both controls failed. Apply now saves the count to <node-dir>/miner-threads and starts the miner with no parameters; resume does the same. The page, the messages and the README say a new core count applies at the next node restart. The test node now refuses miner_start with any parameter, as the real node does.
1 parent cee6e4c commit ce719f1

5 files changed

Lines changed: 101 additions & 22 deletions

File tree

‎README.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,8 @@ The dashboard finds your blocks by reading the chain over IPC and keeps what it
4141

4242
The Mining panel has a cores slider, from 1 to the computer's logical core count, and a pause or resume button.
4343

44-
- **Apply** asks the node to mine on that many cores at once (`miner_start`) and saves the number in `<node-dir>/miner-threads`, the file the installer and updater read, so restarts and updates keep it. The file is replaced in one step (a temporary file renamed over it) and keeps its permissions. While mining is paused the number is only saved, and resuming uses it.
45-
- **Pause** stops mining (`miner_stop`) and writes `<node-dir>/mining-paused`; the node keeps syncing. **Resume** removes that file and starts mining on the saved core count. Until the node installer reads `mining-paused`, a node restart resumes mining.
44+
- **Apply** saves the number in `<node-dir>/miner-threads`, the file the installer and updater read, and makes sure the node is mining (`miner_start`, with no parameters). The node's `miner_start` takes no core count and its miner API has no call to set one, so the new count applies at the next node restart, and restarts and updates keep it. The file is replaced in one step (a temporary file renamed over it) and keeps its permissions. While mining is paused the number is only saved.
45+
- **Pause** stops mining (`miner_stop`) and writes `<node-dir>/mining-paused`; the node keeps syncing. **Resume** removes that file and starts mining (`miner_start`, with no parameters) on the core count the node started with; a saved count applies at the next node restart. Until the node installer reads `mining-paused`, a node restart resumes mining.
4646

4747
After each change the dashboard reads the node back (`eth_mining`) and says in plain words what happened, or what went wrong. The controls are `POST /api/mining/cores` (body `{"cores": n}`, a whole number in range, else 400), `POST /api/mining/pause` and `POST /api/mining/resume`, behind the dashboard key and the Origin check.
4848

‎internal/ipc/ipctest/fake.go‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,20 @@ func (s *Server) serve(conn net.Conn) {
129129
}
130130
}
131131

132+
// NoParams wraps h so that a call with any parameter gets the error the real
133+
// node sends for a method that takes none (its miner_start does).
134+
func NoParams(h Handler) Handler {
135+
return func(params json.RawMessage) (any, *Error) {
136+
var list []json.RawMessage
137+
if len(params) > 0 && string(params) != "null" {
138+
if err := json.Unmarshal(params, &list); err != nil || len(list) > 0 {
139+
return nil, &Error{Code: -32602, Message: "Invalid number of input parameters to RPC method"}
140+
}
141+
}
142+
return h(params)
143+
}
144+
}
145+
132146
// Value is a handler that always returns v.
133147
func Value(v any) Handler {
134148
return func(json.RawMessage) (any, *Error) { return v, nil }

‎internal/server/controls_test.go‎

Lines changed: 70 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package server
22

33
import (
4+
"context"
45
"encoding/json"
6+
"errors"
57
"net/http"
68
"net/http/httptest"
79
"os"
@@ -12,14 +14,16 @@ import (
1214
"sync"
1315
"testing"
1416

17+
"github.com/ghostcoin-core/ghost-node-dashboard/internal/ipc"
1518
"github.com/ghostcoin-core/ghost-node-dashboard/internal/ipc/ipctest"
1619
"github.com/ghostcoin-core/ghost-node-dashboard/internal/status"
1720
)
1821

1922
const goodOrigin = "http://127.0.0.1:8580"
2023

2124
// fakeMiner is a node whose miner_start and miner_stop change what
22-
// eth_mining reports, recording the thread counts miner_start was given.
25+
// eth_mining reports, recording the params of each miner_start it took. Like
26+
// the real node, its miner_start refuses any parameter.
2327
type fakeMiner struct {
2428
mu sync.Mutex
2529
mining bool
@@ -28,13 +32,13 @@ type fakeMiner struct {
2832

2933
func (m *fakeMiner) handlers() map[string]ipctest.Handler {
3034
return map[string]ipctest.Handler{
31-
"miner_start": func(params json.RawMessage) (any, *ipctest.Error) {
35+
"miner_start": ipctest.NoParams(func(params json.RawMessage) (any, *ipctest.Error) {
3236
m.mu.Lock()
3337
defer m.mu.Unlock()
3438
m.mining = true
3539
m.started = append(m.started, params)
3640
return nil, nil
37-
},
41+
}),
3842
"miner_stop": func(json.RawMessage) (any, *ipctest.Error) {
3943
m.mu.Lock()
4044
defer m.mu.Unlock()
@@ -119,15 +123,15 @@ func TestSetCores(t *testing.T) {
119123
if !res.OK || res.Error != "" {
120124
t.Errorf("result %+v, want ok", res)
121125
}
122-
if !strings.Contains(res.Message, "3 cores") {
123-
t.Errorf("message %q does not name 3 cores", res.Message)
126+
if !strings.Contains(res.Message, "3 cores") || !strings.Contains(res.Message, "next node restart") {
127+
t.Errorf("message %q does not say 3 cores apply at the next node restart", res.Message)
124128
}
125129
if res.Mining == nil || !*res.Mining || res.CoresUsed != 3 {
126130
t.Errorf("read back mining %v, cores %d", res.Mining, res.CoresUsed)
127131
}
128132

129-
if got := miner.starts(); !slices.Equal(got, []string{"[3]"}) {
130-
t.Errorf("miner_start params %v, want [[3]]", got)
133+
if got := miner.starts(); !slices.Equal(got, []string{"[]"}) {
134+
t.Errorf("miner_start params %v, want one call with []", got)
131135
}
132136
calls := fake.Calls()
133137
if i := slices.Index(calls, "miner_start"); i < 0 || !slices.Contains(calls[i:], "eth_mining") {
@@ -298,8 +302,14 @@ func TestPauseResume(t *testing.T) {
298302
if !res.OK || res.Paused || res.Mining == nil || !*res.Mining {
299303
t.Errorf("resume result %+v, want ok, not paused and mining", res)
300304
}
301-
if got := miner.starts(); !slices.Equal(got, []string{"[6]"}) {
302-
t.Errorf("resume miner_start params %v, want the saved [6]", got)
305+
if got := miner.starts(); !slices.Equal(got, []string{"[]"}) {
306+
t.Errorf("resume miner_start params %v, want one call with []", got)
307+
}
308+
if !strings.Contains(res.Message, "6 cores") || !strings.Contains(res.Message, "next node restart") {
309+
t.Errorf("resume message %q does not say the saved 6 cores apply at the next node restart", res.Message)
310+
}
311+
if got := readFile(t, filepath.Join(dir, status.MinerThreadsFile)); got != "6\n" {
312+
t.Errorf("miner-threads = %q, want 6", got)
303313
}
304314
if _, err := os.Stat(paused); !os.IsNotExist(err) {
305315
t.Errorf("mining-paused still there after resume (%v)", err)
@@ -309,6 +319,57 @@ func TestPauseResume(t *testing.T) {
309319
}
310320
}
311321

322+
// TestMinerStartNoParams checks that the fake miner refuses miner_start with a
323+
// thread count, as the real node does, and that the cores control and resume
324+
// still start mining on it.
325+
func TestMinerStartNoParams(t *testing.T) {
326+
miner := &fakeMiner{}
327+
_, node := startFake(t, miner.handlers())
328+
329+
var rpcErr *ipc.RPCError
330+
err := node.Call(context.Background(), nil, "miner_start", 2)
331+
if !errors.As(err, &rpcErr) || !strings.Contains(rpcErr.Message, "Invalid number of input parameters") {
332+
t.Fatalf("miner_start(2) on the fake: %v, want the node's invalid parameters error", err)
333+
}
334+
if err := node.Call(context.Background(), nil, "miner_start"); err != nil {
335+
t.Fatalf("miner_start() on the fake: %v", err)
336+
}
337+
if err := node.Call(context.Background(), nil, "miner_stop"); err != nil {
338+
t.Fatal(err)
339+
}
340+
341+
dir := t.TempDir()
342+
h := panelHandler(t, node, panelOpts{nodeDir: dir})
343+
344+
res := decodeResult(t, post(t, h, "/api/mining/cores", `{"cores": 2}`, signedPost))
345+
if !res.OK || res.Error != "" || res.Mining == nil || !*res.Mining {
346+
t.Errorf("cores result %+v, want ok and mining", res)
347+
}
348+
if !strings.Contains(res.Message, "next node restart") {
349+
t.Errorf("cores message %q does not say the count applies at the next node restart", res.Message)
350+
}
351+
if got := readFile(t, filepath.Join(dir, status.MinerThreadsFile)); got != "2\n" {
352+
t.Errorf("miner-threads = %q, want 2", got)
353+
}
354+
355+
if res := decodeResult(t, post(t, h, "/api/mining/pause", "", signedPost)); !res.OK {
356+
t.Fatalf("pause result %+v", res)
357+
}
358+
res = decodeResult(t, post(t, h, "/api/mining/resume", "", signedPost))
359+
if !res.OK || res.Error != "" || res.Mining == nil || !*res.Mining {
360+
t.Errorf("resume result %+v, want ok and mining", res)
361+
}
362+
363+
for _, p := range miner.starts() {
364+
if p != "[]" && p != "" && p != "null" {
365+
t.Errorf("miner_start took params %s, want none", p)
366+
}
367+
}
368+
if got := len(miner.starts()); got != 3 {
369+
t.Errorf("miner_start ran %d times, want 3", got)
370+
}
371+
}
372+
312373
func TestPauseNodeStillMining(t *testing.T) {
313374
handlers := (&fakeMiner{}).handlers()
314375
handlers["eth_mining"] = ipctest.Value(true)

‎internal/status/control.go‎

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,12 @@ func ValidCores(n, logical int) bool {
5555
return n >= 1 && n <= logical
5656
}
5757

58-
// SetCores saves n to <node-dir>/miner-threads and, unless mining is paused,
59-
// starts the miner on n cores at once.
58+
// The node's miner_start takes no parameters and its miner API has no way to
59+
// set the thread count, so a new core count only reaches the node through
60+
// <node-dir>/miner-threads when the node next starts.
61+
62+
// SetCores saves n to <node-dir>/miner-threads, which applies at the next node
63+
// restart, and, unless mining is paused, makes sure the miner is running.
6064
func SetCores(ctx context.Context, node Caller, env MiningEnv, n int) ControlResult {
6165
if !ValidCores(n, env.LogicalCores) {
6266
return ControlResult{
@@ -73,17 +77,17 @@ func SetCores(ctx context.Context, node Caller, env MiningEnv, n int) ControlRes
7377
res.CoresUsed = n
7478
if res.Paused {
7579
res.OK = true
76-
res.Message = fmt.Sprintf("Saved %d cores. Mining is paused; resuming will use them.", n)
80+
res.Message = fmt.Sprintf("Saved %d cores. They apply at the next node restart. Mining is paused.", n)
7781
res.Mining = readMining(ctx, node)
7882
return res
7983
}
80-
if err := node.Call(ctx, nil, "miner_start", n); err != nil {
81-
res.Error = fmt.Sprintf("Saved %d cores for the next start, but the node did not take them: %s.", n, nodeProblem(err))
84+
if err := node.Call(ctx, nil, "miner_start"); err != nil {
85+
res.Error = fmt.Sprintf("Saved %d cores for the next node restart, but the node did not start mining: %s.", n, nodeProblem(err))
8286
res.Mining = readMining(ctx, node)
8387
return res
8488
}
8589
res.Mining = readMining(ctx, node)
86-
res.OK, res.Message = miningOutcome(res.Mining, true, fmt.Sprintf("Mining on %d cores. Saved for restarts and updates.", n))
90+
res.OK, res.Message = miningOutcome(res.Mining, true, fmt.Sprintf("Saved %d cores. They apply at the next node restart; until then the node keeps its current core count.", n))
8791
if !res.OK {
8892
res.Error, res.Message = res.Message, ""
8993
}
@@ -112,8 +116,8 @@ func Pause(ctx context.Context, node Caller, env MiningEnv) ControlResult {
112116
return res
113117
}
114118

115-
// Resume removes <node-dir>/mining-paused and starts the miner on the saved
116-
// core count.
119+
// Resume removes <node-dir>/mining-paused and starts the miner. The node mines
120+
// on the core count it started with; a saved count applies at its next restart.
117121
func Resume(ctx context.Context, node Caller, env MiningEnv) ControlResult {
118122
cores := SavedCores(env)
119123
res := ControlResult{CoresUsed: cores, Paused: MiningPaused(env.NodeDir)}
@@ -125,13 +129,13 @@ func Resume(ctx context.Context, node Caller, env MiningEnv) ControlResult {
125129
}
126130
}
127131
res.Paused = false
128-
if err := node.Call(ctx, nil, "miner_start", cores); err != nil {
132+
if err := node.Call(ctx, nil, "miner_start"); err != nil {
129133
res.Error = "Cleared the pause, but the node did not start mining: " + nodeProblem(err) + "."
130134
res.Mining = readMining(ctx, node)
131135
return res
132136
}
133137
res.Mining = readMining(ctx, node)
134-
res.OK, res.Message = miningOutcome(res.Mining, true, fmt.Sprintf("Mining resumed on %d cores.", cores))
138+
res.OK, res.Message = miningOutcome(res.Mining, true, fmt.Sprintf("Mining resumed. The saved %d cores apply at the next node restart.", cores))
135139
if !res.OK {
136140
res.Error, res.Message = res.Message, ""
137141
}

‎web/index.html‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ <h3>Controls</h3>
8484
</form>
8585
<p class="message" id="control-result" role="status" hidden></p>
8686
<p class="note">A new payout address applies at once and is saved in the node's <code>config.toml</code>, so restarts keep it. Only the address is needed: never enter a private key or recovery words here.</p>
87-
<p class="note">The core count is saved in the node folder, so restarts and updates keep it. While mining is paused the node keeps syncing. For now a node restart resumes mining, until the node installer learns to read the <code>mining-paused</code> file.</p>
87+
<p class="note">The core count is saved in the node folder and applies at the next node restart: the node cannot change it while running. Restarts and updates keep it. While mining is paused the node keeps syncing. For now a node restart resumes mining, until the node installer learns to read the <code>mining-paused</code> file.</p>
8888
<p class="updated">Updated <span data-updated>never</span></p>
8989
</section>
9090

0 commit comments

Comments
 (0)