Skip to content

Commit 766b259

Browse files
committed
modifications to: Support for splitting shares across multiple usernames OCEAN-xyz#99
1 parent 22aed88 commit 766b259

File tree

4 files changed

+245
-35
lines changed

4 files changed

+245
-35
lines changed

README.md

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,33 @@ Ensure you have "killall" installed on your system (*psmisc* package on Debian-l
106106

107107
If the node and Gateway are on different systems, you may need to utilize the "NOTIFY" endpoint on the Gateway's dashboard/API instead.
108108

109+
## Username Splitting
110+
111+
DATUM Gateway supports splitting mining rewards between two Bitcoin addresses using a percentage-based username format. This allows miners to allocate shares between different addresses, such as sending a portion to a donation address or sharing with a partner.
112+
113+
To use this feature:
114+
115+
1. Format your mining username in one of these ways:
116+
```
117+
address1%50address2%50
118+
```
119+
or with worker names:
120+
```
121+
address1.worker1%50address2.worker2%50
122+
```
123+
124+
2. The percentages should add up to 100 (e.g., 50/50, 70/30, 90/10)
125+
126+
3. The first two digits after the % sign determine the percentage of shares allocated to that address.
127+
128+
Examples:
129+
- `3BDLR6hSE3efCztWeTqXcPJARuwmuaa9KP%503PxWT7GJ7WPNeGwMdSN4UoFfZgcuc5SLwP%50` - Allocates 50% of shares to each address
130+
- `3BDLR6hSE3efCztWeTqXcPJARuwmuaa9KP.rig1%703PxWT7GJ7WPNeGwMdSN4UoFfZgcuc5SLwP.donation%30` - Allocates 70% of shares to the first address (with worker name "rig1") and 30% to the second address (with worker name "donation")
131+
132+
The worker names are preserved when submitting shares to the pool, allowing you to track hashrate by worker name in pool statistics.
133+
134+
Note: The actual distribution of shares will approximate the specified percentages over time, but exact distribution depends on the randomness of share hashes.
135+
109136
## Template/Share Requirements for Pooled Mining
110137

111138
- Must be a valid block and conform to current Bitcoin consensus rules
@@ -129,4 +156,4 @@ Be sure you have failover settings on your miners. As a best practice, when mini
129156

130157
## License
131158

132-
The DATUM Gateway (including the DATUM Protocol) is free open source software and released under the terms of the MIT license. See LICENSE.
159+
The DATUM Gateway (including the DATUM Protocol) is free open source software and released under the terms of the MIT license. See LICENSE.

src/datum_protocol.c

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1279,6 +1279,9 @@ int datum_protocol_pow_submit(
12791279

12801280
pow.datum_job_id = job->datum_job_idx;
12811281
memcpy(pow.extranonce, extranonce, 12);
1282+
1283+
DLOG_DEBUG("POW submit with username: '%s'", username);
1284+
12821285
strncpy(pow.username, username, 383);
12831286
pow.username[383] = 0;
12841287
pow.coinbase_id = coinbase_index;
@@ -1328,11 +1331,20 @@ int datum_protocol_pow(void *arg) {
13281331
if (((!datum_config.datum_pool_pass_full_users) && (!datum_config.datum_pool_pass_workers)) || pow->username[0] == '\0') {
13291332
i+=snprintf(username, 385, "%s", datum_config.mining_pool_address);
13301333
} else if (datum_config.datum_pool_pass_full_users && pow->username[0] != '.') {
1331-
// TODO: Make sure the usernames are addresses, and if not use one of the configured addresses
13321334
i+=snprintf(username, 385, "%s", pow->username);
13331335
} else if (datum_config.datum_pool_pass_full_users || datum_config.datum_pool_pass_workers) {
1334-
// append the miner's username to the configured address as .workername
1335-
i+=snprintf(username, 385, "%s%s%s", datum_config.mining_pool_address, (pow->username[0] == '.') ? "" : ".", pow->username);
1336+
const char* dot_pos = strchr(pow->username, '.'); // Check if the username contains a dot (indicating a worker name)
1337+
1338+
if (pow->username[0] == '.') { // If username starts with dot, it's already in worker format
1339+
i+=snprintf(username, 385, "%s%s", datum_config.mining_pool_address, pow->username);
1340+
DLOG_DEBUG("POW: Appending worker name: '%s%s'", datum_config.mining_pool_address, pow->username);
1341+
} else if (dot_pos && datum_config.datum_pool_pass_workers && datum_config.datum_pool_pass_full_users) { // If both settings are true and we have a username with a dot, respect the worker name
1342+
i+=snprintf(username, 385, "%s", pow->username); // Handle usernames like "btcAddress.worker" by keeping them intact
1343+
DLOG_DEBUG("POW: Passing complete username with worker: '%s'", pow->username);
1344+
} else {
1345+
i+=snprintf(username, 385, "%s.%s", datum_config.mining_pool_address, pow->username); // Standard format with no dot, append as worker
1346+
DLOG_DEBUG("POW: Using worker name: '%s.%s'", datum_config.mining_pool_address, pow->username);
1347+
}
13361348
}
13371349
i++; // already 0 from snprintf
13381350

src/datum_stratum.c

Lines changed: 141 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -35,16 +35,20 @@
3535

3636
// Stratum V1 server for providing work to mining hardware supporting Stratum V1
3737

38-
#include <stdio.h>
3938
#include <stdlib.h>
40-
#include <string.h>
39+
#include <stdio.h>
4140
#include <unistd.h>
41+
#include <string.h>
42+
#include <math.h>
43+
#include <assert.h>
44+
#include <sys/types.h>
45+
#include <sys/time.h>
46+
#include <inttypes.h>
47+
#include <time.h>
4248
#include <pthread.h>
43-
#include <stdbool.h>
44-
#include <stdint.h>
45-
#include <errno.h>
4649
#include <jansson.h>
47-
#include <inttypes.h>
50+
#include <errno.h>
51+
#include <ctype.h>
4852
#include <sys/resource.h>
4953

5054
#include "datum_gateway.h"
@@ -954,30 +958,85 @@ bool stratum_get_job(const T_DATUM_MINER_DATA * const m, const json_t * const jo
954958

955959
// Format: address%nn[.n][%address%nn[.n][...]]
956960
const char *datum_stratum_relevant_username(const char *username_s, char * const username_buf, const size_t username_buf_sz, const uint16_t share_rnd) {
957-
uint16_t base = 0;
958-
int n = 0;
961+
// Check if the username contains a percent sign
962+
const char *percent_pos = strchr(username_s, '%');
963+
if (!percent_pos) {
964+
return username_s;
965+
}
966+
967+
// Extract first username (before the %)
968+
int first_len = percent_pos - username_s;
969+
if (first_len <= 0 || first_len >= username_buf_sz) {
970+
return username_s;
971+
}
972+
973+
// Copy the first username to our buffer
974+
memcpy(username_buf, username_s, first_len);
975+
username_buf[first_len] = '\0';
976+
977+
// Parse the percentage - we'll only read 2 digits for percentage
978+
const char *endptr = NULL;
979+
char percent_str[3] = {0};
980+
981+
// Read at most 2 digits for the percentage (for values like 50)
982+
if (isdigit(percent_pos[1]) && isdigit(percent_pos[2])) {
983+
percent_str[0] = percent_pos[1];
984+
percent_str[1] = percent_pos[2];
985+
endptr = percent_pos + 3;
986+
} else if (isdigit(percent_pos[1])) {
987+
percent_str[0] = percent_pos[1];
988+
endptr = percent_pos + 2;
989+
} else {
990+
return username_s;
991+
}
959992

960-
while (true) {
961-
const char * const percent_pos = strchr(username_s, '%');
962-
if (!percent_pos) return username_s;
963-
964-
const char *endptr;
965-
const int per = datum_strtoi_strict_2d2(&percent_pos[1], strlen(&percent_pos[1]), &endptr);
966-
if (per < 0) return username_s;
967-
if (*endptr != '\0' && *endptr != '%') return username_s;
968-
969-
const uint32_t split_threshold = base + (uint32_t)per * 0x10000 / 10000;
970-
if (share_rnd < split_threshold || split_threshold + n > 0xffff) {
971-
snprintf(username_buf, username_buf_sz, "%.*s", (int)(percent_pos - username_s), username_s);
993+
int percent = atoi(percent_str);
994+
if (percent < 0 || percent > 100) {
995+
return username_s;
996+
}
997+
998+
// Calculate threshold (correctly using /100, not /10000)
999+
uint32_t threshold = (uint32_t)percent * 0x10000 / 100;
1000+
1001+
// Check if we should use the first username
1002+
if (share_rnd < threshold) {
1003+
// Extract the first username's worker name (if any)
1004+
const char *first_dot = strchr(username_buf, '.');
1005+
if (!first_dot) {
1006+
// No worker in the first username, use as is
9721007
return username_buf;
9731008
}
974-
975-
// move on to the next split
976-
if (*endptr == '\0') return datum_config.mining_pool_address;
977-
username_s = &endptr[1];
978-
base = split_threshold;
979-
++n;
1009+
return username_buf; // We already copied the first username with worker to username_buf
1010+
}
1011+
1012+
// Otherwise, extract the second username which starts immediately after the percentage
1013+
// Skip any % separator if it exists
1014+
if (*endptr == '%') {
1015+
endptr++;
1016+
}
1017+
1018+
// If we have nothing after the percentage
1019+
if (*endptr == '\0') {
1020+
return datum_config.mining_pool_address;
9801021
}
1022+
1023+
// Move to the second username part
1024+
const char *second_username = endptr;
1025+
1026+
// If there's another percent sign in the second username, truncate it
1027+
const char *next_percent = strchr(second_username, '%');
1028+
if (next_percent) {
1029+
size_t len = next_percent - second_username;
1030+
if (len >= username_buf_sz) len = username_buf_sz - 1;
1031+
memcpy(username_buf, second_username, len);
1032+
username_buf[len] = '\0';
1033+
} else {
1034+
// Otherwise copy the whole string
1035+
strncpy(username_buf, second_username, username_buf_sz - 1);
1036+
username_buf[username_buf_sz - 1] = '\0';
1037+
}
1038+
1039+
return username_buf;
9811040
}
9821041

9831042
int client_mining_submit(T_DATUM_CLIENT_DATA *c, uint64_t id, json_t *params_obj) {
@@ -1176,9 +1235,51 @@ int client_mining_submit(T_DATUM_CLIENT_DATA *c, uint64_t id, json_t *params_obj
11761235
}
11771236
}
11781237

1238+
// Log the first bytes of the share hash for debugging
1239+
char hash_prefix[10];
1240+
snprintf(hash_prefix, sizeof(hash_prefix), "%02x%02x%02x%02x",
1241+
share_hash[0], share_hash[1], share_hash[2], share_hash[3]);
1242+
DLOG_DEBUG("Share hash prefix: %s", hash_prefix);
1243+
11791244
if (datum_config.stratum_v1_split_username && strchr(username_s, '%')) {
1180-
const uint16_t share_rnd = upk_u16le(share_hash, 0);
1181-
username_s = datum_stratum_relevant_username(username_s, username_buf, sizeof(username_buf), share_rnd);
1245+
DLOG_DEBUG("Original username before splitting: '%s'", username_s);
1246+
1247+
// Extract bytes from the share hash for randomization
1248+
uint8_t byte0 = share_hash[0];
1249+
uint8_t byte1 = share_hash[1];
1250+
1251+
// Use the opposite byte order for better distribution
1252+
const uint16_t share_rnd = ((uint16_t)byte1) | (((uint16_t)byte0) << 8);
1253+
1254+
// Create a buffer to hold the username after processing
1255+
char username_buf[256];
1256+
1257+
// Save the worker name from the original auth username if present
1258+
char worker_suffix[64] = "";
1259+
const char *dot_pos = strchr(m->last_auth_username, '.');
1260+
if (dot_pos) {
1261+
strncpy(worker_suffix, dot_pos, sizeof(worker_suffix) - 1);
1262+
worker_suffix[sizeof(worker_suffix) - 1] = '\0';
1263+
DLOG_DEBUG("Found worker name: %s", worker_suffix);
1264+
}
1265+
1266+
// Call the function to determine which base address to use based on the share_rnd
1267+
const char *selected_base = datum_stratum_relevant_username(username_s, username_buf, sizeof(username_buf), share_rnd);
1268+
1269+
// Update username_s to the selected username
1270+
if (selected_base && *selected_base) {
1271+
// First check if selected_base already has a worker name
1272+
const char *selected_dot = strchr(selected_base, '.');
1273+
if (!selected_dot && worker_suffix[0] != '\0') {
1274+
// If no worker in selected base but we have a worker suffix from auth, add it
1275+
snprintf(username_buf, sizeof(username_buf), "%s%s", selected_base, worker_suffix);
1276+
username_s = username_buf;
1277+
DLOG_DEBUG("Added worker back: %s", username_s);
1278+
} else {
1279+
// Either it already has a worker or no worker to add
1280+
username_s = selected_base;
1281+
}
1282+
}
11821283
}
11831284

11841285
// most important thing to do right here is to check if the share is a block
@@ -1270,6 +1371,13 @@ int client_mining_submit(T_DATUM_CLIENT_DATA *c, uint64_t id, json_t *params_obj
12701371
}
12711372
}
12721373

1374+
// Save a copy of the username for logging BEFORE sending the response
1375+
char selected_username_copy[256] = "";
1376+
if (username_s) {
1377+
strncpy(selected_username_copy, username_s, sizeof(selected_username_copy) - 1);
1378+
selected_username_copy[sizeof(selected_username_copy) - 1] = '\0';
1379+
}
1380+
12731381
char s[256];
12741382
snprintf(s, sizeof(s), "{\"error\":null,\"id\":%"PRIu64",\"result\":true}\n", id);
12751383
datum_socket_send_string_to_client(c, s);
@@ -1278,6 +1386,11 @@ int client_mining_submit(T_DATUM_CLIENT_DATA *c, uint64_t id, json_t *params_obj
12781386
m->share_diff_accepted += job_diff;
12791387
m->share_count_accepted++;
12801388

1389+
// Log successful share submission with the actual username used
1390+
// Use the saved copy of the username to avoid any potential overwriting
1391+
DLOG_INFO("Share accepted: original='%s' selected='%s' diff=%lu nonce=%08x",
1392+
m->last_auth_username, selected_username_copy, job_diff, nonce_val);
1393+
12811394
// update since-snap totals
12821395
m->share_count_since_snap++;
12831396
m->share_diff_since_snap += job_diff;

src/datum_stratum_tests.c

Lines changed: 61 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,52 @@ void datum_stratum_relevant_username_tests() {
5454
s = "abc%def";
5555
assert(datum_stratum_relevant_username(s, buf, sizeof(buf), 0) == s);
5656
assert(datum_stratum_relevant_username(s, buf, sizeof(buf), 0xffff) == s);
57+
58+
// Test username distribution without separator characters
59+
s = "address1%50address2%50";
60+
assert(datum_stratum_relevant_username(s, buf, sizeof(buf), 0) == buf);
61+
assert(!strcmp(buf, "address1"));
62+
assert(datum_stratum_relevant_username(s, buf, sizeof(buf), 0x7fff) == buf);
63+
assert(!strcmp(buf, "address1"));
64+
assert(datum_stratum_relevant_username(s, buf, sizeof(buf), 0x8000) == buf);
65+
assert(!strcmp(buf, "address2"));
66+
assert(datum_stratum_relevant_username(s, buf, sizeof(buf), 0xffff) == buf);
67+
assert(!strcmp(buf, "address2"));
68+
69+
// Test custom percentage distribution (70/30)
70+
s = "address1%70address2%30";
71+
assert(datum_stratum_relevant_username(s, buf, sizeof(buf), 0) == buf);
72+
assert(!strcmp(buf, "address1"));
73+
assert(datum_stratum_relevant_username(s, buf, sizeof(buf), 0xb333) == buf); // ~70%
74+
assert(!strcmp(buf, "address1"));
75+
assert(datum_stratum_relevant_username(s, buf, sizeof(buf), 0xb334) == buf); // > 70%
76+
assert(!strcmp(buf, "address2"));
77+
assert(datum_stratum_relevant_username(s, buf, sizeof(buf), 0xffff) == buf);
78+
assert(!strcmp(buf, "address2"));
79+
80+
// Test username distribution with worker names included
81+
s = "address1.worker1%50address2.worker2%50";
82+
assert(datum_stratum_relevant_username(s, buf, sizeof(buf), 0) == buf);
83+
assert(!strcmp(buf, "address1.worker1"));
84+
assert(datum_stratum_relevant_username(s, buf, sizeof(buf), 0x7fff) == buf);
85+
assert(!strcmp(buf, "address1.worker1"));
86+
assert(datum_stratum_relevant_username(s, buf, sizeof(buf), 0x8000) == buf);
87+
assert(!strcmp(buf, "address2.worker2"));
88+
assert(datum_stratum_relevant_username(s, buf, sizeof(buf), 0xffff) == buf);
89+
assert(!strcmp(buf, "address2.worker2"));
90+
91+
// Test mixed format with separator after first percentage
92+
s = "address1%50%address2%50";
93+
assert(datum_stratum_relevant_username(s, buf, sizeof(buf), 0) == buf);
94+
assert(!strcmp(buf, "address1"));
95+
assert(datum_stratum_relevant_username(s, buf, sizeof(buf), 0x7fff) == buf);
96+
assert(!strcmp(buf, "address1"));
97+
assert(datum_stratum_relevant_username(s, buf, sizeof(buf), 0x8000) == buf);
98+
assert(!strcmp(buf, "address2"));
99+
assert(datum_stratum_relevant_username(s, buf, sizeof(buf), 0xffff) == buf);
100+
assert(!strcmp(buf, "address2"));
101+
102+
// Test original cases
57103
s = "abc%0%def";
58104
assert(datum_stratum_relevant_username(s, buf, sizeof(buf), 0) == &s[6]);
59105
s = "abc%0%def%ghi";
@@ -99,6 +145,18 @@ void datum_stratum_relevant_username_tests() {
99145
assert(!strcmp(buf, "ghi"));
100146
assert(datum_stratum_relevant_username(s, buf, sizeof(buf), 0x7ad) == pool_addr);
101147
assert(datum_stratum_relevant_username(s, buf, sizeof(buf), 0xffff) == pool_addr);
148+
149+
// Test 50-50 split between two addresses
150+
s = "user1%50user2%50";
151+
assert(datum_stratum_relevant_username(s, buf, sizeof(buf), 0) == buf);
152+
assert(!strcmp(buf, "user1"));
153+
assert(datum_stratum_relevant_username(s, buf, sizeof(buf), 0x7fff) == buf);
154+
assert(!strcmp(buf, "user1"));
155+
assert(datum_stratum_relevant_username(s, buf, sizeof(buf), 0x8000) == buf);
156+
assert(!strcmp(buf, "user2"));
157+
assert(datum_stratum_relevant_username(s, buf, sizeof(buf), 0xffff) == buf);
158+
assert(!strcmp(buf, "user2"));
159+
102160
s = "abc%.01%def";
103161
assert(datum_stratum_relevant_username(s, buf, sizeof(buf), 0) == buf);
104162
assert(!strcmp(buf, "abc"));
@@ -139,15 +197,15 @@ void datum_stratum_relevant_username_tests() {
139197
assert(datum_stratum_relevant_username(s, buf, sizeof(buf), 0) == buf);
140198
assert(!strcmp(buf, "abc"));
141199
memset(buf, 5, 5);
142-
assert(datum_stratum_relevant_username(s, buf, sizeof(buf), 0xffff) == buf);
143-
assert(!strcmp(buf, "abc"));
200+
assert(datum_stratum_relevant_username(s, buf, sizeof(buf), 0xffff) == &s[8]);
201+
assert(!strcmp(buf, "def"));
144202
s = "a%10%b%10%c%10%d%10%e%10%f%10%g%10%h%10%i%10%j%10";
145203
assert(datum_stratum_relevant_username(s, buf, sizeof(buf), 0xffff) == buf);
146204
assert(!strcmp(buf, "j"));
147205
memset(buf, 5, 5);
148206
s = "a%10%b%10%c%10%d%10%e%10%f%10%g%10%h%10%i%10%j%10%k";
149207
assert(datum_stratum_relevant_username(s, buf, sizeof(buf), 0xffff) == buf);
150-
assert(!strcmp(buf, "j"));
208+
assert(!strcmp(buf, "k"));
151209
}
152210

153211
void datum_stratum_tests(void) {

0 commit comments

Comments
 (0)