-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathstratum.c
More file actions
1476 lines (1349 loc) · 56.9 KB
/
Copy pathstratum.c
File metadata and controls
1476 lines (1349 loc) · 56.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/* Stratum V1 server: TCP listener + thread-per-connection.
*
* Wire format: newline-delimited JSON-RPC 2.0. Methods handled:
* mining.subscribe, mining.authorize, mining.submit
*
* Concurrency: an rwlock guards `current_job`. set_job swaps the pointer
* under a write lock and pushes the previous job into a small ring of
* "recent jobs" kept alive ~60s for late submits. Connection threads take
* read locks for notify/submit lookups.
*
* Vardiff adjusts each connection's difficulty toward cfg.vardiff_target_spm
* shares/minute, clamped so the share target never exceeds the network
* target (see vardiff_maybe_retarget).
*/
#define _POSIX_C_SOURCE 200809L
#include "stratum.h"
#include "coinbase.h"
#include "share.h"
#include "log.h"
#include "thunder.h"
#include "cjson/cJSON.h"
#include <arpa/inet.h>
#include <errno.h>
#include <fcntl.h>
#include <inttypes.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <pthread.h>
#include <stdarg.h>
#include <stdatomic.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <sys/types.h>
#include <time.h>
#include <unistd.h>
#define MAX_LINE_BYTES 16384
#define DEDUPE_RING 1024
#define RECENT_JOBS 8
#define RECENT_JOB_TTL_MS 60000
/* BIP320 reserved version-rolling bits (ASICBoost). Advertised in
* mining.configure; only these block-header version bits may be rolled by a
* miner, and a per-connection mask (this ANDed with the client's request) is
* applied to every submitted version. */
#define VERSION_ROLLING_MASK 0x1fffe000u
/* ============================================================== job ===== */
struct stratum_job {
char job_id[32];
int32_t version;
uint8_t prev_hash_le[32];
/* Template-level inputs for per-connection coinbase rendering. */
int64_t value_sats;
char *wc_hex; /* witness commitment hex, owned, may be NULL */
/* Server-provided coinbase (BIP22 "coinbasetxn"), owned, may be NULL. When
* set, the per-connection coinbase is built from this rather than from
* scratch; coinbase_has_witness says whether to re-attach the witness
* reserved value when assembling a found block. */
char *coinbasetxn_hex;
int coinbase_has_witness;
size_t en1_size;
size_t en2_size;
uint8_t (*merkle_branches)[32];
size_t branch_count;
uint32_t nbits;
uint32_t ntime;
uint8_t network_target_be[32];
uint32_t height;
char **tx_hex_list; /* owned */
size_t tx_count;
uint64_t created_ms; /* for retention ring */
};
stratum_job_t *stratum_job_new(
const char *job_id,
int32_t version,
const uint8_t prev_hash_le[32],
int64_t value_sats,
const char *witness_commitment_hex,
size_t en1_size, size_t en2_size,
const uint8_t (*merkle_branches)[32], size_t branch_count,
uint32_t nbits, uint32_t ntime,
const uint8_t network_target_be[32],
uint32_t height,
const char *const *tx_hex_list, size_t tx_count,
const char *coinbasetxn_hex, int coinbase_has_witness)
{
stratum_job_t *j = calloc(1, sizeof(*j));
if (!j) return NULL;
snprintf(j->job_id, sizeof(j->job_id), "%s", job_id ? job_id : "");
j->version = version;
if (prev_hash_le) memcpy(j->prev_hash_le, prev_hash_le, 32);
j->value_sats = value_sats;
j->en1_size = en1_size;
j->en2_size = en2_size;
j->coinbase_has_witness = coinbase_has_witness;
if (witness_commitment_hex && *witness_commitment_hex) {
j->wc_hex = strdup(witness_commitment_hex);
if (!j->wc_hex) goto fail;
}
if (coinbasetxn_hex && *coinbasetxn_hex) {
j->coinbasetxn_hex = strdup(coinbasetxn_hex);
if (!j->coinbasetxn_hex) goto fail;
}
if (branch_count) {
j->merkle_branches = calloc(branch_count, sizeof(*j->merkle_branches));
if (!j->merkle_branches) goto fail;
memcpy(j->merkle_branches, merkle_branches, branch_count * 32);
j->branch_count = branch_count;
}
j->nbits = nbits;
j->ntime = ntime;
if (network_target_be) memcpy(j->network_target_be, network_target_be, 32);
j->height = height;
if (tx_count && tx_hex_list) {
j->tx_hex_list = calloc(tx_count, sizeof(char *));
if (!j->tx_hex_list) goto fail;
for (size_t i = 0; i < tx_count; ++i) {
j->tx_hex_list[i] = tx_hex_list[i] ? strdup(tx_hex_list[i]) : strdup("");
if (!j->tx_hex_list[i]) goto fail;
}
j->tx_count = tx_count;
}
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
j->created_ms = (uint64_t)ts.tv_sec * 1000u + (uint64_t)(ts.tv_nsec / 1000000);
return j;
fail:
stratum_job_free(j);
return NULL;
}
void stratum_job_free(stratum_job_t *j) {
if (!j) return;
free(j->wc_hex);
free(j->coinbasetxn_hex);
free(j->merkle_branches);
if (j->tx_hex_list) {
for (size_t i = 0; i < j->tx_count; ++i) free(j->tx_hex_list[i]);
free(j->tx_hex_list);
}
free(j);
}
/* ============================================================ server ==== */
struct stratum_server {
stratum_cfg_t cfg;
int listen_fd;
atomic_int stop;
atomic_int conn_count;
atomic_uint extranonce1_seq;
pthread_t listener_thr;
int listener_started;
pthread_rwlock_t job_lock;
stratum_job_t *current_job; /* protected by job_lock */
stratum_job_t *recent[RECENT_JOBS]; /* small retention ring */
size_t recent_head;
pthread_mutex_t recent_lock;
/* List of live connections — for broadcasting notify on job swap. */
pthread_mutex_t conns_lock;
struct stratum_conn *conns_head;
};
struct stratum_conn {
stratum_server_t *server;
int fd; /* -1 in tests */
pthread_t thr;
int thr_started;
uint8_t extranonce1[4];
double difficulty;
int subscribed;
int authorized;
uint32_t version_mask; /* negotiated version-rolling bits; 0 = off */
char worker_name[129]; /* full stratum username (sanitized) */
char payout_address[128]; /* validated bech32/base58 */
/* Per-connection coinbase, rendered against the current job using
* payout_address (miner) + cfg.operator_address (fee). Refreshed any
* time we hand out a new notify for a job id we haven't rendered
* coinbase for yet. */
uint8_t *cb1;
size_t cb1_len;
uint8_t *cb2;
size_t cb2_len;
char cb_for_job_id[32];
/* Dedupe ring. Each entry is a small hash of
* (job_id|en2|ntime|nonce|version). */
uint64_t dedupe[DEDUPE_RING];
size_t dedupe_head;
/* Vardiff window state — counts accepted shares since vd_window_start_ms.
* Every cfg.vardiff_window_sec the rate is compared to vardiff_target_spm
* and `difficulty` is multiplied/divided to converge on the target. */
uint64_t vd_window_start_ms;
uint32_t vd_window_shares;
/* The pre-retarget difficulty, honored for a grace period after a
* set_difficulty: the miner applies the new value only on a later job,
* so in-flight and old-job shares still arrive at the old difficulty.
* A back-to-back retarget overwrites this — only the latest old value
* is honored. */
double prev_difficulty;
uint64_t diff_changed_ms;
/* Monotonic timestamp of the most recent recv() that got any bytes.
* The conn thread checks this against cfg.idle_timeout_sec after each
* SO_RCVTIMEO wake-up so silent connections are reaped. */
uint64_t last_activity_ms;
pthread_mutex_t write_lock;
struct stratum_conn *next; /* server->conns_head linked list */
};
static void conn_clear_coinbase(stratum_conn_t *c) {
free(c->cb1); c->cb1 = NULL; c->cb1_len = 0;
free(c->cb2); c->cb2 = NULL; c->cb2_len = 0;
c->cb_for_job_id[0] = '\0';
}
/* ----------------------------------------------------- helpers ---------- */
static uint64_t now_ms(void) {
struct timespec ts;
clock_gettime(CLOCK_REALTIME, &ts);
return (uint64_t)ts.tv_sec * 1000u + (uint64_t)(ts.tv_nsec / 1000000);
}
static uint64_t mono_ms(void) {
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return (uint64_t)ts.tv_sec * 1000u + (uint64_t)(ts.tv_nsec / 1000000);
}
static int hex_nib(char c) {
if (c >= '0' && c <= '9') return c - '0';
if (c >= 'a' && c <= 'f') return 10 + c - 'a';
if (c >= 'A' && c <= 'F') return 10 + c - 'A';
return -1;
}
/* Decode hex into out (exactly outlen bytes). Returns 0 on success. */
static int hex_to_bytes(const char *hex, uint8_t *out, size_t outlen) {
if (!hex) return -1;
size_t hl = strlen(hex);
if (hl != outlen * 2) return -1;
for (size_t i = 0; i < outlen; ++i) {
int hi = hex_nib(hex[2 * i]);
int lo = hex_nib(hex[2 * i + 1]);
if (hi < 0 || lo < 0) return -1;
out[i] = (uint8_t)((hi << 4) | lo);
}
return 0;
}
/* Decode an arbitrary-length hex string. Returns malloc'd buffer, *outlen
* set, or NULL on error. */
static uint8_t *hex_to_bytes_alloc(const char *hex, size_t *outlen) {
if (!hex) return NULL;
size_t hl = strlen(hex);
if (hl % 2) return NULL;
size_t n = hl / 2;
uint8_t *out = malloc(n ? n : 1);
if (!out) return NULL;
for (size_t i = 0; i < n; ++i) {
int hi = hex_nib(hex[2 * i]);
int lo = hex_nib(hex[2 * i + 1]);
if (hi < 0 || lo < 0) { free(out); return NULL; }
out[i] = (uint8_t)((hi << 4) | lo);
}
*outlen = n;
return out;
}
static void bytes_to_hex(const uint8_t *bytes, size_t n, char *out) {
static const char *H = "0123456789abcdef";
for (size_t i = 0; i < n; ++i) {
out[2 * i] = H[(bytes[i] >> 4) & 0xf];
out[2 * i + 1] = H[bytes[i] & 0xf];
}
out[2 * n] = '\0';
}
/* Parse a hex u32, big-endian semantics: e.g. "5f5e1000" -> 0x5f5e1000. */
static int parse_u32_hex(const char *hex, uint32_t *out) {
uint8_t b[4];
if (hex_to_bytes(hex, b, 4) != 0) return -1;
*out = ((uint32_t)b[0] << 24) | ((uint32_t)b[1] << 16) |
((uint32_t)b[2] << 8) | (uint32_t)b[3];
return 0;
}
static void sanitize_worker(const char *in, char *out, size_t outlen) {
size_t j = 0;
for (size_t i = 0; in && in[i] && j + 1 < outlen; ++i) {
char c = in[i];
if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') ||
(c >= '0' && c <= '9') || c == '.' || c == '_' || c == '-') {
out[j++] = c;
} else {
out[j++] = '_';
}
}
out[j] = '\0';
}
static uint64_t fnv1a(const char *s) {
uint64_t h = 1469598103934665603ULL;
for (; *s; ++s) {
h ^= (uint8_t)*s;
h *= 1099511628211ULL;
}
return h;
}
/* ---- output buffer helpers ---- */
static int buf_append(char **buf, size_t *len, const char *s, size_t n) {
char *nb = realloc(*buf, *len + n + 1);
if (!nb) return -1;
*buf = nb;
memcpy(*buf + *len, s, n);
*len += n;
(*buf)[*len] = '\0';
return 0;
}
static int buf_append_json_line(char **buf, size_t *len, cJSON *obj) {
char *s = cJSON_PrintUnformatted(obj);
if (!s) return -1;
int rc = buf_append(buf, len, s, strlen(s));
if (rc == 0) rc = buf_append(buf, len, "\n", 1);
free(s);
return rc;
}
/* ---- job retention ring ---- */
static void retire_job(stratum_server_t *s, stratum_job_t *j) {
if (!j) return;
pthread_mutex_lock(&s->recent_lock);
/* Sweep expired. */
uint64_t cutoff = mono_ms();
for (size_t i = 0; i < RECENT_JOBS; ++i) {
if (s->recent[i] && cutoff - s->recent[i]->created_ms > RECENT_JOB_TTL_MS) {
stratum_job_free(s->recent[i]);
s->recent[i] = NULL;
}
}
/* Free whatever is in the slot we are about to overwrite. */
if (s->recent[s->recent_head]) {
stratum_job_free(s->recent[s->recent_head]);
}
s->recent[s->recent_head] = j;
s->recent_head = (s->recent_head + 1) % RECENT_JOBS;
pthread_mutex_unlock(&s->recent_lock);
}
/* Find a job by id under read lock (current) or recent ring. Returned
* pointer is borrowed — only valid while caller holds appropriate locks
* (current_job: rdlock; recent: recent_lock). For simplicity we return
* a reference that is safe so long as set_job hasn't replaced it; in this
* design submit handlers complete quickly and shares for retired jobs are
* rare. */
static stratum_job_t *find_job(stratum_server_t *s, const char *job_id) {
if (!job_id) return NULL;
/* current */
pthread_rwlock_rdlock(&s->job_lock);
stratum_job_t *cur = s->current_job;
if (cur && strcmp(cur->job_id, job_id) == 0) {
pthread_rwlock_unlock(&s->job_lock);
return cur;
}
pthread_rwlock_unlock(&s->job_lock);
/* recent */
pthread_mutex_lock(&s->recent_lock);
for (size_t i = 0; i < RECENT_JOBS; ++i) {
if (s->recent[i] && strcmp(s->recent[i]->job_id, job_id) == 0) {
stratum_job_t *r = s->recent[i];
pthread_mutex_unlock(&s->recent_lock);
return r;
}
}
pthread_mutex_unlock(&s->recent_lock);
return NULL;
}
/* ---- notify payload ---- */
/* Build a mining.notify params array. cb1/cb2 are supplied separately
* because they are rendered per-connection (each miner's coinbase pays
* that miner's payout_address). */
static cJSON *make_notify_params(const stratum_job_t *j,
const uint8_t *cb1, size_t cb1_len,
const uint8_t *cb2, size_t cb2_len,
int clean_jobs) {
cJSON *p = cJSON_CreateArray();
cJSON_AddItemToArray(p, cJSON_CreateString(j->job_id));
/* prev_hash: mining.notify uses the stratum convention where the 32-byte
* hash is sent with each 4-byte word byte-reversed (word order preserved).
* prev_hash_le holds the header-internal little-endian bytes, so we
* word-swap before emitting; the miner word-swaps again to recover the
* exact bytes that go into the block header. Sending the raw little-endian
* bytes makes standard ASICs hash the wrong header (every share rejected
* as "low difficulty"). */
char hex[65];
uint8_t prev_ws[32];
for (int wi = 0; wi < 8; ++wi)
for (int bi = 0; bi < 4; ++bi)
prev_ws[wi * 4 + bi] = j->prev_hash_le[wi * 4 + 3 - bi];
bytes_to_hex(prev_ws, 32, hex);
cJSON_AddItemToArray(p, cJSON_CreateString(hex));
char *cb1_hex = malloc(cb1_len * 2 + 1);
char *cb2_hex = malloc(cb2_len * 2 + 1);
if (!cb1_hex || !cb2_hex) {
free(cb1_hex); free(cb2_hex); cJSON_Delete(p); return NULL;
}
bytes_to_hex(cb1, cb1_len, cb1_hex);
bytes_to_hex(cb2, cb2_len, cb2_hex);
cJSON_AddItemToArray(p, cJSON_CreateString(cb1_hex));
cJSON_AddItemToArray(p, cJSON_CreateString(cb2_hex));
free(cb1_hex); free(cb2_hex);
cJSON *branches = cJSON_CreateArray();
for (size_t i = 0; i < j->branch_count; ++i) {
bytes_to_hex(j->merkle_branches[i], 32, hex);
cJSON_AddItemToArray(branches, cJSON_CreateString(hex));
}
cJSON_AddItemToArray(p, branches);
char vhex[9], thex[9], nhex[9];
snprintf(vhex, sizeof(vhex), "%08x", (uint32_t)j->version);
snprintf(thex, sizeof(thex), "%08x", j->nbits);
snprintf(nhex, sizeof(nhex), "%08x", j->ntime);
cJSON_AddItemToArray(p, cJSON_CreateString(vhex));
cJSON_AddItemToArray(p, cJSON_CreateString(thex));
cJSON_AddItemToArray(p, cJSON_CreateString(nhex));
cJSON_AddItemToArray(p, cJSON_CreateBool(clean_jobs ? 1 : 0));
return p;
}
/* Render a fresh coinbase for `c` against `job` using c->payout_address
* and the server's operator_address / fee_bps / coinbase_tag. Caches into
* c->cb1/cb2 keyed by job->job_id. Returns 0 ok, negative on error. */
static int conn_render_coinbase(stratum_server_t *s, stratum_conn_t *c,
const stratum_job_t *job) {
if (!c->authorized || c->payout_address[0] == '\0') return -1;
if (c->cb_for_job_id[0] && strcmp(c->cb_for_job_id, job->job_id) == 0) {
return 0; /* cached */
}
coinbase_parts_t parts = {0};
char err[256] = {0};
int rc;
if (s->cfg.pps_enabled) {
/* PPS-classic: every miner's coinbase is identical, paying the
* pool's BTC wallet for the net-of-fee reward and the operator
* address for the fee. The operator later moves accumulated BTC
* into Thunder via the admin dashboard's deposit action; per-miner
* accounting happens off-chain via pps_credits. */
if (job->coinbasetxn_hex) {
rc = coinbase_build_from_template(job->coinbasetxn_hex,
s->cfg.pool_btc_address,
s->cfg.operator_address, s->cfg.fee_bps,
s->cfg.coinbase_tag,
job->en1_size, job->en2_size,
&parts, NULL, NULL, NULL, err, sizeof err);
} else {
rc = coinbase_build_split(job->height, job->value_sats,
s->cfg.pool_btc_address,
s->cfg.operator_address, s->cfg.fee_bps,
job->wc_hex, s->cfg.coinbase_tag,
job->en1_size, job->en2_size,
&parts, NULL, NULL, err, sizeof err);
}
} else if (job->coinbasetxn_hex) {
/* Backend dictated the coinbase (e.g. CUSF enforcer): build from it,
* redirecting the reward output to this miner and preserving the
* mandatory commitment outputs. The witness commitment is already in
* the server's coinbase, so job->wc_hex is not used here. */
rc = coinbase_build_from_template(job->coinbasetxn_hex,
c->payout_address,
s->cfg.operator_address, s->cfg.fee_bps,
s->cfg.coinbase_tag,
job->en1_size, job->en2_size,
&parts, NULL, NULL, NULL, err, sizeof err);
} else {
rc = coinbase_build_split(job->height, job->value_sats,
c->payout_address,
s->cfg.operator_address, s->cfg.fee_bps,
job->wc_hex, s->cfg.coinbase_tag,
job->en1_size, job->en2_size,
&parts, NULL, NULL, err, sizeof err);
}
if (rc < 0) {
LOG_WARN("stratum: coinbase render failed for %s: %s",
c->worker_name, err);
return -1;
}
free(c->cb1); free(c->cb2);
c->cb1 = parts.cb1; c->cb1_len = parts.cb1_len;
c->cb2 = parts.cb2; c->cb2_len = parts.cb2_len;
snprintf(c->cb_for_job_id, sizeof c->cb_for_job_id, "%s", job->job_id);
return 0;
}
static int emit_notification(char **buf, size_t *len, const char *method, cJSON *params) {
cJSON *obj = cJSON_CreateObject();
cJSON_AddItemToObject(obj, "id", cJSON_CreateNull());
cJSON_AddStringToObject(obj, "method", method);
cJSON_AddItemToObject(obj, "params", params);
int rc = buf_append_json_line(buf, len, obj);
cJSON_Delete(obj);
return rc;
}
static int emit_response(char **buf, size_t *len, cJSON *id, cJSON *result, cJSON *err) {
cJSON *obj = cJSON_CreateObject();
cJSON_AddItemToObject(obj, "id", id ? cJSON_Duplicate(id, 1) : cJSON_CreateNull());
cJSON_AddItemToObject(obj, "result", result ? result : cJSON_CreateNull());
cJSON_AddItemToObject(obj, "error", err ? err : cJSON_CreateNull());
int rc = buf_append_json_line(buf, len, obj);
cJSON_Delete(obj);
return rc;
}
static cJSON *make_error(int code, const char *msg) {
cJSON *e = cJSON_CreateArray();
cJSON_AddItemToArray(e, cJSON_CreateNumber(code));
cJSON_AddItemToArray(e, cJSON_CreateString(msg));
cJSON_AddItemToArray(e, cJSON_CreateNull());
return e;
}
/* ---- varint for block assembly ---- */
static void varint_append(uint8_t **buf, size_t *cap, size_t *len, uint64_t n) {
/* ensure 9 bytes */
if (*len + 9 > *cap) {
size_t nc = (*cap ? *cap * 2 : 64);
while (nc < *len + 9) nc *= 2;
uint8_t *nb = realloc(*buf, nc);
if (!nb) return;
*buf = nb; *cap = nc;
}
uint8_t *p = *buf + *len;
if (n < 0xfd) { p[0] = (uint8_t)n; *len += 1; return; }
if (n <= 0xffff) {
p[0] = 0xfd; p[1] = (uint8_t)(n & 0xff); p[2] = (uint8_t)((n >> 8) & 0xff);
*len += 3; return;
}
if (n <= 0xffffffffULL) {
p[0] = 0xfe;
for (int i = 0; i < 4; ++i) p[1 + i] = (uint8_t)((n >> (8 * i)) & 0xff);
*len += 5; return;
}
p[0] = 0xff;
for (int i = 0; i < 8; ++i) p[1 + i] = (uint8_t)((n >> (8 * i)) & 0xff);
*len += 9;
}
static void bytes_append(uint8_t **buf, size_t *cap, size_t *len, const uint8_t *src, size_t n) {
if (*len + n > *cap) {
size_t nc = (*cap ? *cap * 2 : 64);
while (nc < *len + n) nc *= 2;
uint8_t *nb = realloc(*buf, nc);
if (!nb) return;
*buf = nb; *cap = nc;
}
memcpy(*buf + *len, src, n);
*len += n;
}
/* ---- core message handler --------------------------------------------- */
static void send_set_difficulty(char **buf, size_t *len, double diff) {
cJSON *p = cJSON_CreateArray();
cJSON_AddItemToArray(p, cJSON_CreateNumber(diff));
emit_notification(buf, len, "mining.set_difficulty", p);
}
/* Network difficulty of the current job, or 0 when no job is set. */
static double current_net_diff(stratum_server_t *s) {
double d = 0.0;
pthread_rwlock_rdlock(&s->job_lock);
if (s->current_job) d = target_to_diff(s->current_job->network_target_be);
pthread_rwlock_unlock(&s->job_lock);
return d;
}
/* How long shares at the pre-retarget difficulty stay acceptable. The miner
* applies a set_difficulty on a later job notify, which on a slow chain can
* lag well past the vardiff window. */
static uint64_t diff_grace_ms(const stratum_server_t *s) {
uint64_t g = (uint64_t)s->cfg.vardiff_window_sec * 2000ULL;
return g > 60000 ? g : 60000;
}
/* Vardiff: every cfg.vardiff_window_sec, look at how many shares the
* connection submitted in that window and rescale its difficulty so the
* rate converges on cfg.vardiff_target_spm shares/minute. Called from
* handle_submit() after each accepted share.
*
* Conservative algorithm:
* ratio = observed_spm / target_spm
* if ratio in [0.5, 2.0] → leave it (avoid jitter)
* else → new_diff = old_diff * ratio, clamped
* Always emits a single mining.set_difficulty when diff changes. The
* client picks it up for the next job notify; we don't force a re-notify
* because handle_submit keeps accepting shares at the old difficulty for
* a grace period (diff_grace_ms). */
static void vardiff_maybe_retarget(stratum_server_t *s, stratum_conn_t *c,
uint64_t now,
char **buf, size_t *len)
{
if (!s->cfg.vardiff_enabled) return;
if (c->vd_window_start_ms == 0) {
c->vd_window_start_ms = now;
c->vd_window_shares = 0;
return;
}
uint64_t elapsed_ms = now - c->vd_window_start_ms;
uint64_t window_ms = (uint64_t)s->cfg.vardiff_window_sec * 1000ULL;
if (elapsed_ms < window_ms) return;
/* Observed shares per minute over this window. */
double observed_spm = ((double)c->vd_window_shares * 60000.0) /
(double)elapsed_ms;
double target_spm = s->cfg.vardiff_target_spm;
double ratio = observed_spm / target_spm;
double old_diff = c->difficulty;
double new_diff = old_diff;
if (ratio > 2.0 || ratio < 0.5) {
new_diff = old_diff * ratio;
/* Cap each adjustment to a 4x step to avoid wild swings on small
* windows. */
if (new_diff > old_diff * 4.0) new_diff = old_diff * 4.0;
if (new_diff < old_diff / 4.0) new_diff = old_diff / 4.0;
if (new_diff < s->cfg.vardiff_min) new_diff = s->cfg.vardiff_min;
if (new_diff > s->cfg.vardiff_max) new_diff = s->cfg.vardiff_max;
/* Never raise the share difficulty above the network difficulty:
* the miner discards hashes above the stratum target locally, so a
* share target harder than the network target throws away valid
* blocks before the pool ever sees them. This clamp wins over
* vardiff_min/max — it bites on low-difficulty networks where an
* ASIC's vardiff otherwise climbs orders of magnitude past the
* chain difficulty. */
double net_diff = current_net_diff(s);
if (net_diff > 0.0 && new_diff > net_diff) new_diff = net_diff;
}
/* Reset the window regardless of whether we changed diff. */
c->vd_window_start_ms = now;
c->vd_window_shares = 0;
if (new_diff != old_diff) {
c->difficulty = new_diff;
c->prev_difficulty = old_diff;
c->diff_changed_ms = now;
LOG_INFO("stratum: vardiff %s: %.0f -> %.0f (%.1f spm observed, %.1f target)",
c->worker_name, old_diff, new_diff, observed_spm, target_spm);
send_set_difficulty(buf, len, new_diff);
}
}
/* Send mining.notify for the current job to a specific connection, using
* that connection's rendered coinbase. Skips silently if the conn is not
* yet authorized (we have no payout address to render against). */
static void send_current_notify(stratum_server_t *s, stratum_conn_t *c,
char **buf, size_t *len, int clean) {
pthread_rwlock_rdlock(&s->job_lock);
stratum_job_t *cur = s->current_job;
if (cur && conn_render_coinbase(s, c, cur) == 0) {
cJSON *p = make_notify_params(cur, c->cb1, c->cb1_len,
c->cb2, c->cb2_len, clean);
if (p) emit_notification(buf, len, "mining.notify", p);
}
pthread_rwlock_unlock(&s->job_lock);
}
static int handle_subscribe(stratum_server_t *s, stratum_conn_t *c, cJSON *id,
char **buf, size_t *len) {
/* Allocate extranonce1 from server counter ^ time. */
unsigned seq = atomic_fetch_add(&s->extranonce1_seq, 1);
uint32_t mix = seq ^ (uint32_t)now_ms();
c->extranonce1[0] = (uint8_t)(mix >> 24);
c->extranonce1[1] = (uint8_t)(mix >> 16);
c->extranonce1[2] = (uint8_t)(mix >> 8);
c->extranonce1[3] = (uint8_t)mix;
c->subscribed = 1;
char ex1_hex[9];
bytes_to_hex(c->extranonce1, 4, ex1_hex);
cJSON *result = cJSON_CreateArray();
cJSON *subs = cJSON_CreateArray();
cJSON *sd = cJSON_CreateArray();
cJSON_AddItemToArray(sd, cJSON_CreateString("mining.set_difficulty"));
cJSON_AddItemToArray(sd, cJSON_CreateString("sd"));
cJSON_AddItemToArray(subs, sd);
cJSON *sn = cJSON_CreateArray();
cJSON_AddItemToArray(sn, cJSON_CreateString("mining.notify"));
cJSON_AddItemToArray(sn, cJSON_CreateString("sn"));
cJSON_AddItemToArray(subs, sn);
cJSON_AddItemToArray(result, subs);
cJSON_AddItemToArray(result, cJSON_CreateString(ex1_hex));
cJSON_AddItemToArray(result, cJSON_CreateNumber(4));
return emit_response(buf, len, id, result, NULL);
}
/* mining.configure (BIP310). Only the version-rolling extension is supported.
* params = [ [extension names...], { extension parameters... } ]. We negotiate
* the version-rolling mask as (client mask AND our BIP320 mask) and report it
* back; other requested extensions are silently left unacknowledged. */
static int handle_configure(stratum_server_t *s, stratum_conn_t *c, cJSON *id,
cJSON *params, char **buf, size_t *len) {
(void)s;
cJSON *exts = NULL, *args = NULL;
if (cJSON_IsArray(params)) {
exts = cJSON_GetArrayItem(params, 0);
args = cJSON_GetArrayItem(params, 1);
}
int wants_vr = 0;
if (cJSON_IsArray(exts)) {
int n = cJSON_GetArraySize(exts);
for (int i = 0; i < n; ++i) {
cJSON *e = cJSON_GetArrayItem(exts, i);
if (cJSON_IsString(e) &&
strcmp(e->valuestring, "version-rolling") == 0) {
wants_vr = 1;
}
}
}
cJSON *result = cJSON_CreateObject();
if (wants_vr) {
/* Client mask defaults to "roll everything" when omitted; we clamp it
* to the bits we actually allow. */
uint32_t client_mask = 0xffffffffu;
if (cJSON_IsObject(args)) {
cJSON *m = cJSON_GetObjectItemCaseSensitive(args,
"version-rolling.mask");
uint32_t parsed;
if (cJSON_IsString(m) && parse_u32_hex(m->valuestring, &parsed) == 0) {
client_mask = parsed;
}
}
c->version_mask = client_mask & VERSION_ROLLING_MASK;
char mask_hex[9];
snprintf(mask_hex, sizeof mask_hex, "%08x", c->version_mask);
cJSON_AddItemToObject(result, "version-rolling", cJSON_CreateTrue());
cJSON_AddStringToObject(result, "version-rolling.mask", mask_hex);
LOG_INFO("stratum: version-rolling negotiated, mask=%s", mask_hex);
}
return emit_response(buf, len, id, result, NULL);
}
static int handle_authorize(stratum_server_t *s, stratum_conn_t *c, cJSON *id,
cJSON *params, char **buf, size_t *len) {
const char *worker = NULL;
if (cJSON_IsArray(params) && cJSON_GetArraySize(params) >= 1) {
cJSON *w = cJSON_GetArrayItem(params, 0);
if (cJSON_IsString(w)) worker = w->valuestring;
}
if (!worker) {
cJSON *err = make_error(24, "missing worker name");
return emit_response(buf, len, id, NULL, err);
}
/* Username format: <address>[.<rig_label>]. The address part must be
* a valid bech32 (P2WPKH) or base58check (P2PKH / P2SH) Bitcoin
* address; the optional label is a free-form rig identifier. */
const char *dot = strchr(worker, '.');
size_t addr_len = dot ? (size_t)(dot - worker) : strlen(worker);
if (addr_len == 0 || addr_len >= sizeof(c->payout_address)) {
if (s->cfg.on_reject) {
s->cfg.on_reject(s->cfg.ctx, worker, now_ms(),
"stratum username must start with a bitcoin address");
}
cJSON *err = make_error(24,
"stratum username must be <bitcoin_address>[.<rig_label>]");
return emit_response(buf, len, id, NULL, err);
}
memcpy(c->payout_address, worker, addr_len);
c->payout_address[addr_len] = '\0';
char derr[128] = {0};
if (s->cfg.pps_enabled) {
/* Thunder address: 20-byte hash160 in plain base58. The
* 's<n>_<base58>_<hex6>' deposit-format wrapper is rejected (see
* thunder.c). We don't need the decoded bytes here — the coinbase
* pays the pool's BTC wallet, not the miner — but we validate so a
* typo'd username can't accrue unpayable PPS. */
uint8_t th[20];
if (thunder_address_decode(c->payout_address, th, derr, sizeof derr) < 0) {
if (s->cfg.on_reject) {
char rmsg[192];
snprintf(rmsg, sizeof rmsg, "invalid thunder address: %s", derr);
s->cfg.on_reject(s->cfg.ctx, worker, now_ms(), rmsg);
}
c->payout_address[0] = '\0';
char emsg[192];
snprintf(emsg, sizeof emsg,
"invalid thunder address in stratum username: %s", derr);
cJSON *err = make_error(24, emsg);
return emit_response(buf, len, id, NULL, err);
}
} else {
uint8_t spk[64];
size_t spk_len = 0;
if (coinbase_address_to_script(c->payout_address, spk, sizeof spk,
&spk_len, derr, sizeof derr) < 0) {
if (s->cfg.on_reject) {
char rmsg[192];
snprintf(rmsg, sizeof rmsg, "invalid payout address: %s", derr);
s->cfg.on_reject(s->cfg.ctx, worker, now_ms(), rmsg);
}
c->payout_address[0] = '\0';
char emsg[192];
snprintf(emsg, sizeof emsg,
"invalid payout address in stratum username: %s", derr);
cJSON *err = make_error(24, emsg);
return emit_response(buf, len, id, NULL, err);
}
}
sanitize_worker(worker, c->worker_name, sizeof(c->worker_name));
c->authorized = 1;
if (c->difficulty <= 0) c->difficulty = s->cfg.initial_diff;
/* Same clamp as vardiff: a starting difficulty above the network
* difficulty would make the miner discard valid blocks locally. */
double net_diff = current_net_diff(s);
if (net_diff > 0.0 && c->difficulty > net_diff) c->difficulty = net_diff;
/* Arm vardiff window for this connection. */
c->vd_window_start_ms = now_ms();
c->vd_window_shares = 0;
/* respond true */
emit_response(buf, len, id, cJSON_CreateTrue(), NULL);
/* Then push initial set_difficulty + notify (renders this conn's
* coinbase against the current job using its payout address). */
send_set_difficulty(buf, len, c->difficulty);
send_current_notify(s, c, buf, len, 1);
return 0;
}
static int dedupe_check_and_add(stratum_conn_t *c, const char *jid,
const char *en2, const char *ntime,
const char *nonce, uint32_t version) {
char key[256];
snprintf(key, sizeof(key), "%s|%s|%s|%s|%08x",
jid ? jid : "", en2 ? en2 : "", ntime ? ntime : "", nonce ? nonce : "",
version);
uint64_t h = fnv1a(key);
for (size_t i = 0; i < DEDUPE_RING; ++i) {
if (c->dedupe[i] == h) return 1;
}
c->dedupe[c->dedupe_head] = h;
c->dedupe_head = (c->dedupe_head + 1) % DEDUPE_RING;
return 0;
}
/* Build full block hex from job + coinbase + nonce/ntime. Returns malloc'd
* NUL-terminated string, or NULL on OOM. */
static char *assemble_block_hex(const stratum_job_t *j,
const uint8_t *coinbase_tx, size_t cb_len,
const uint8_t header[80]) {
/* header(80) | varint(1+tx_count) | coinbase | concat(template_txs raw) */
size_t tx_count = j->tx_count + 1; /* +1 coinbase */
uint8_t *block = NULL;
size_t cap = 0, len = 0;
bytes_append(&block, &cap, &len, header, 80);
varint_append(&block, &cap, &len, tx_count);
if (j->coinbase_has_witness && cb_len >= 8) {
/* coinbase_tx is the legacy serialization:
* version(4) | inputs | outputs | locktime(4)
* The block's coinbase must carry its witness so the segwit
* commitment validates. Re-serialize in segwit form: insert the
* marker+flag after the version and the single-input witness (one
* 32-byte reserved value, all zero — matching the commitment the
* backend computed) just before the locktime. */
static const uint8_t marker_flag[2] = { 0x00, 0x01 };
static const uint8_t witness[34] = { 0x01, 0x20 }; /* 1 item, 32 bytes, all zero */
bytes_append(&block, &cap, &len, coinbase_tx, 4); /* version */
bytes_append(&block, &cap, &len, marker_flag, 2);
bytes_append(&block, &cap, &len, coinbase_tx + 4, cb_len - 8); /* inputs + outputs */
bytes_append(&block, &cap, &len, witness, sizeof witness);
bytes_append(&block, &cap, &len, coinbase_tx + cb_len - 4, 4); /* locktime */
} else {
bytes_append(&block, &cap, &len, coinbase_tx, cb_len);
}
for (size_t i = 0; i < j->tx_count; ++i) {
size_t txn = 0;
uint8_t *txb = hex_to_bytes_alloc(j->tx_hex_list[i], &txn);
if (!txb) { free(block); return NULL; }
bytes_append(&block, &cap, &len, txb, txn);
free(txb);
}
char *out = malloc(len * 2 + 1);
if (!out) { free(block); return NULL; }
bytes_to_hex(block, len, out);
free(block);
return out;
}
static int handle_submit(stratum_server_t *s, stratum_conn_t *c, cJSON *id,
cJSON *params, char **buf, size_t *len) {
if (!c->authorized) {
cJSON *err = make_error(24, "unauthorized");
return emit_response(buf, len, id, NULL, err);
}
if (!cJSON_IsArray(params) || cJSON_GetArraySize(params) < 5) {
cJSON *err = make_error(20, "bad params");
return emit_response(buf, len, id, NULL, err);
}
const char *worker = cJSON_GetArrayItem(params, 0)->valuestring;
const char *jid = cJSON_GetArrayItem(params, 1)->valuestring;
const char *en2 = cJSON_GetArrayItem(params, 2)->valuestring;
const char *ntime = cJSON_GetArrayItem(params, 3)->valuestring;
const char *nonce = cJSON_GetArrayItem(params, 4)->valuestring;
(void)worker;
stratum_job_t *job = find_job(s, jid);
if (!job) {
if (s->cfg.on_reject) {
s->cfg.on_reject(s->cfg.ctx, c->worker_name, now_ms(),
"stale or unknown job");
}
cJSON *err = make_error(21, "stale or unknown job");
return emit_response(buf, len, id, NULL, err);
}
/* Version rolling (BIP310): the optional 6th submit param carries the
* version the miner actually hashed. Keep the job's version bits outside
* the negotiated mask and take the miner's bits inside it; with no param
* (or no negotiation) this leaves job->version unchanged. We fall back to
* the standard BIP320 mask if a version arrives without prior configure,
* so miners that roll by default still verify correctly. */
int32_t submit_version = job->version;
if (cJSON_GetArraySize(params) >= 6) {
cJSON *v = cJSON_GetArrayItem(params, 5);
uint32_t rolled = 0;
if (!cJSON_IsString(v) || parse_u32_hex(v->valuestring, &rolled) != 0) {
cJSON *err = make_error(20, "bad version hex");
return emit_response(buf, len, id, NULL, err);
}
uint32_t mask = c->version_mask ? c->version_mask : VERSION_ROLLING_MASK;
submit_version =
(int32_t)(((uint32_t)job->version & ~mask) | (rolled & mask));
}
if (dedupe_check_and_add(c, jid, en2, ntime, nonce,
(uint32_t)submit_version)) {
if (s->cfg.on_reject) {
s->cfg.on_reject(s->cfg.ctx, c->worker_name, now_ms(),
"duplicate share");
}
cJSON *err = make_error(22, "duplicate share");
return emit_response(buf, len, id, NULL, err);
}
uint32_t ntime_v, nonce_v;
if (parse_u32_hex(ntime, &ntime_v) != 0 || parse_u32_hex(nonce, &nonce_v) != 0) {
cJSON *err = make_error(20, "bad ntime/nonce hex");
return emit_response(buf, len, id, NULL, err);
}
size_t en2_len = 0;
uint8_t *en2_bytes = hex_to_bytes_alloc(en2, &en2_len);
if (!en2_bytes) {
cJSON *err = make_error(20, "bad extranonce2 hex");
return emit_response(buf, len, id, NULL, err);
}
/* Render this connection's coinbase for `job` if not cached. The
* cache is keyed on job_id; submits against an older job retired into