Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ add_executable(datum_gateway
src/datum_jsonrpc.c
src/datum_logger.c
src/datum_protocol.c
src/datum_protocol_tests.c
src/datum_queue.c
src/datum_sockets.c
src/datum_stratum.c
Expand Down
3 changes: 1 addition & 2 deletions doc/example_datum_gateway_config.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,7 @@
"log_level_file": 1
},
"datum": {
"pool_pass_workers": true,
"pool_pass_full_users": true,
"pool_username_behaviour": "passthrough",
"pooled_mining_only": true
}
}
12 changes: 8 additions & 4 deletions doc/usernames.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,16 @@ If the Stratum username *begins* with a period, it is interpreted as a worker na
There are three different ways to pass usernames to your pool.

By default, the Stratum username is always passed in full, as-is.
You can make this explicit by setting `datum`.`pool_pass_full_users` to `true` in the config file, or "Send Miner Usernames To Pool: Override Bitcoin Address" in the web configurator.
You can make this explicit by setting `datum`.`pool_username_behaviour` to `"passthrough"` in the config file, or "Send Miner Usernames To Pool: Override Bitcoin Address" in the web configurator.

If you change `datum`.`pool_pass_full_users` to `false`, you can then set `datum`.`pool_pass_workers` instead (or "Send Miner Usernames To Pool: Send as worker names" in the web configurator).
You can also set `datum`.`pool_username_behaviour` to `"worker"` (or "Send Miner Usernames To Pool: Send as worker names" in the web configurator).
Comment on lines 41 to +46
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The doc still says there are “three different ways” to pass usernames, but this PR adds a fourth (strip_worker). Update that sentence/count to match the newly documented behaviors.

Copilot uses AI. Check for mistakes.
With this setting, the entire Stratum username will be appended after the default username (`mining`.`pool_address`) as a worker.

Finally, if you set both options to `false`, the Stratum username will be ignored entirely.
Another option is to set `datum`.`pool_username_behaviour` to `"strip_worker"` (or "Send Miner Usernames To Pool: Override Bitcoin Address with username prior to period (strip worker)" in the web configurator).
This will strip off everything beginning with the first period (`.`) in the username, and send just the first part.
You can use this if you want to keep the miner worker name private, but still use its username for the Bitcoin address.

Finally, if you set `datum`.`pool_username_behaviour` to `"ignore"`, the Stratum username will be ignored entirely.
Instead, only the configured default username (`mining`.`pool_address`) will be used, without any worker names.

## Username modifiers (advanced)
Expand Down Expand Up @@ -94,4 +98,4 @@ if you assign *more* than 100%, that portion above will not have any shares subm
Do not rely on these behaviours.
Always specify the full 100% range explicitly.

NOTE: This feature is handled when shares are received by the Gateway's Stratum server, and will therefore only work if you have `datum`.`pool_pass_full_users` enabled.
NOTE: This feature is handled when shares are received by the Gateway's Stratum server, and will therefore only work if you have `datum`.`pool_username_behaviour` set to `"passthrough"` or `"strip_worker"`.
45 changes: 40 additions & 5 deletions src/datum_api.c
Original file line number Diff line number Diff line change
Expand Up @@ -964,6 +964,8 @@ size_t datum_api_fill_config_var(const char *var_start, const size_t var_name_le
var_start = "readonly:";
colon_pos = &var_start[8];
}
} else if (var_name_len_2 == 27 && 0 == strncmp(var_start_2, "*datum_pool_pass_full_users", 27)) {
val = datum_config.datum_pool_pass_workers && datum_config.datum_pool_pass_full_users;
} else if (var_name_len_2 == 24 && 0 == strncmp(var_start_2, "*datum_pool_pass_workers", 24)) {
val = datum_config.datum_pool_pass_workers && !datum_config.datum_pool_pass_full_users;
} else if (var_name_len_2 == 16 && 0 == strncmp(var_start_2, "*datum_pool_host", 16)) {
Expand All @@ -986,6 +988,8 @@ size_t datum_api_fill_config_var(const char *var_start, const size_t var_name_le
if (copy_sz >= replacement_max_len) copy_sz = replacement_max_len - 1;
memcpy(replacement, s, copy_sz);
return copy_sz;
} else if (var_name_len_2 == 32 && 0 == strncmp(var_start_2, "*username_behaviour_strip_worker", 32)) {
val = datum_config.datum_pool_pass_full_users && !datum_config.datum_pool_pass_workers;
} else if (var_name_len_2 == 27 && 0 == strncmp(var_start_2, "*username_behaviour_private", 27)) {
val = !(datum_config.datum_pool_pass_workers || datum_config.datum_pool_pass_full_users);
} else if (var_name_len_2 == 22 && 0 == strncmp(var_start_2, "*reward_sharing_prefer", 22)) {
Expand Down Expand Up @@ -1036,6 +1040,10 @@ size_t datum_api_fill_config_var(const char *var_start, const size_t var_name_le
DLOG_ERROR("%s: %s not implemented", __func__, "DATUM_CONF_USERNAME_MODS");
break;
}
case DATUM_CONF_FUNC: {
DLOG_ERROR("%s: %s not implemented", __func__, "DATUM_CONF_FUNC");
break;
}
}
} else {
DLOG_ERROR("%s: '%.*s' not implemented", __func__, (int)(var_end - var_start_2), var_start_2);
Expand Down Expand Up @@ -1160,25 +1168,52 @@ bool datum_api_config_set(const char * const key, const char * const val, struct
strcpy(datum_config.mining_pool_address, val);
datum_api_json_modify_new("mining", "pool_address", json_string(val));
} else if (0 == strcmp(key, "username_behaviour")) {
json_t * const config = datum_config.config_json;
assert(config);

const char *nv;
if (0 == strcmp(val, "datum_pool_pass_full_users")) {
if (datum_config.datum_pool_pass_full_users) return true;
if (datum_config.datum_pool_pass_full_users && datum_config.datum_pool_pass_workers) return true;
nv = "passthrough";
datum_config.datum_pool_pass_full_users = true;
// datum_pool_pass_workers doesn't matter with datum_pool_pass_full_users enabled
datum_config.datum_pool_pass_workers = true;
} else if (0 == strcmp(val, "datum_pool_pass_workers")) {
Comment thread
luke-jr marked this conversation as resolved.
if (datum_config.datum_pool_pass_workers && !datum_config.datum_pool_pass_full_users) return true;
nv = "worker";
datum_config.datum_pool_pass_full_users = false;
datum_config.datum_pool_pass_workers = true;
} else if (0 == strcmp(val, "strip_worker")) {
if (datum_config.datum_pool_pass_full_users && !datum_config.datum_pool_pass_workers) return true;
nv = val;
datum_config.datum_pool_pass_full_users = true;
datum_config.datum_pool_pass_workers = false;
} else if (0 == strcmp(val, "private")) {
if (!(datum_config.datum_pool_pass_workers || datum_config.datum_pool_pass_full_users)) return true;
nv = "ignore";
datum_config.datum_pool_pass_full_users = false;
datum_config.datum_pool_pass_workers = false;
} else {
json_array_append_new(errors, json_string_nocheck("Invalid option for \"Send Miner Usernames To Pool\""));
return false;
}
datum_api_json_modify_new("datum", "pool_pass_full_users", json_boolean(datum_config.datum_pool_pass_full_users));
if (!datum_config.datum_pool_pass_full_users) {
datum_api_json_modify_new("datum", "pool_pass_workers", json_boolean(datum_config.datum_pool_pass_workers));
datum_api_json_modify_new("datum", "pool_username_behaviour", json_string_nocheck(nv));

json_t *j = json_object_get(config, "datum");
if (j && (json_object_get(j, "pool_pass_full_users")
|| json_object_get(j, "pool_pass_workers")
|| json_object_get(j, "_legacy_username_behaviour"))) {
if (nv[0] == 's') { // strip_worker, not supported by legacy config
// Must delete legacy keys or we will trigger a warning at startup
json_object_del(j, "pool_pass_full_users");
json_object_del(j, "pool_pass_workers");
datum_api_json_modify_new("datum", "_legacy_username_behaviour", json_true());
} else {
json_object_del(j, "_legacy_username_behaviour");
datum_api_json_modify_new("datum", "pool_pass_full_users", json_boolean(datum_config.datum_pool_pass_full_users));
if (!datum_config.datum_pool_pass_full_users) {
datum_api_json_modify_new("datum", "pool_pass_workers", json_boolean(datum_config.datum_pool_pass_workers));
}
Comment on lines +1213 to +1215
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When writing legacy keys for backward compatibility, the passthrough path updates pool_pass_full_users but may leave an existing pool_pass_workers key set to false (e.g. after switching from "private"). That makes the persisted config internally inconsistent and triggers the new startup deprecation warnings/UI state mismatches. Ensure pool_pass_workers is also set to true (or delete it) when pool_pass_full_users is true and you choose to keep legacy keys.

Suggested change
if (!datum_config.datum_pool_pass_full_users) {
datum_api_json_modify_new("datum", "pool_pass_workers", json_boolean(datum_config.datum_pool_pass_workers));
}
datum_api_json_modify_new("datum", "pool_pass_workers", json_boolean(datum_config.datum_pool_pass_workers));

Copilot uses AI. Check for mistakes.
}
}
} else if (0 == strcmp(key, "mining_coinbase_tag_secondary")) {
if (0 == strcmp(val, datum_config.mining_coinbase_tag_secondary)) return true;
Expand Down
133 changes: 126 additions & 7 deletions src/datum_conf.c
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,58 @@ const char *datum_conf_var_type_text[] = {
"string",
"string_array",
"{\"modname\":{\"address\":proportion,...},...}",
NULL, // func
};

static int datum_conf_username_behaviour(const T_DATUM_CONFIG_ITEM * const c, const json_t * const j, const char ** const out_type) {
Comment thread
luke-jr marked this conversation as resolved.
if (out_type) {
*out_type = "string";
}
if (json_is_string(j)) {
const char * const s = json_string_value(j);
switch (json_string_length(j)) {
case 4:
if (0 == strcasecmp("pass", s)) break;
return -1;
case 6:
if (0 == strcasecmp("worker", s)) {
Comment on lines +67 to +74
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

datum_conf_username_behaviour uses strcasecmp, but this file doesn’t include <strings.h> (POSIX), which can cause build failures (especially with C23 where implicit declarations are not allowed). Add the proper include (or avoid strcasecmp).

Copilot uses AI. Check for mistakes.
datum_config.datum_pool_pass_full_users = false;
datum_config.datum_pool_pass_workers = true;
return 0;
}
if (0 == strcasecmp("ignore", s)) {
datum_config.datum_pool_pass_full_users = false;
datum_config.datum_pool_pass_workers = false;
return 0;
}
return -1;
case 8:
if (0 == strcasecmp("passthru", s)) break;
return -1;
case 11:
if (0 == strcasecmp("passthrough", s)) break;
return -1;
case 12:
if (0 == strcasecmp("strip_worker", s)) {
datum_config.datum_pool_pass_full_users = true;
datum_config.datum_pool_pass_workers = false;
return 0;
}
return -1;
default:
return -1;
}
} else if (json_is_null(j) || !j) {
// fallthrough
} else {
return -1;
}
// Only "passthrough" and variants reach here
datum_config.datum_pool_pass_full_users = true;
datum_config.datum_pool_pass_workers = true;
return 0;
}

const T_DATUM_CONFIG_ITEM datum_config_options[] = {
// Bitcoind configs
{ .var_type = DATUM_CONF_STRING, .category = "bitcoind", .name = "rpccookiefile", .description = "Path to file to read RPC cookie from, for communication with local bitcoind.",
Expand Down Expand Up @@ -177,12 +227,15 @@ const T_DATUM_CONFIG_ITEM datum_config_options[] = {
.required = false, .ptr = &datum_config.datum_pool_port, .default_int = 28915 },
{ .var_type = DATUM_CONF_STRING, .category = "datum", .name = "pool_pubkey", .description = "Public key of the DATUM server for initiating encrypted connection. Get from secure location, or set to empty to auto-fetch.",
.required = false, .ptr = datum_config.datum_pool_pubkey, .default_string[0] = "f21f2f0ef0aa1970468f22bad9bb7f4535146f8e4a8f646bebc93da3d89b1406f40d032f09a417d94dc068055df654937922d2c89522e3e8f6f0e649de473003", .max_string_len = sizeof(datum_config.datum_pool_pubkey) },
{ .var_type = DATUM_CONF_BOOL, .category = "datum", .name = "pool_pass_workers", .description = "Pass stratum miner usernames as sub-worker names to the pool (pool_username.miner's username)",
.example_default = true,
{ .var_type = DATUM_CONF_BOOL, .category = "datum", .name = "pool_pass_workers",
// DEPRECATED (but CAUTION if removing - this currently is responsible for initialising the default value!)
.required = false, .ptr = &datum_config.datum_pool_pass_workers, .default_bool = true },
{ .var_type = DATUM_CONF_BOOL, .category = "datum", .name = "pool_pass_full_users", .description = "Pass stratum miner usernames as raw usernames to the pool (use if putting multiple payout addresses on miners behind this gateway)",
.example_default = true,
{ .var_type = DATUM_CONF_BOOL, .category = "datum", .name = "pool_pass_full_users",
// DEPRECATED (but CAUTION if removing - this currently is responsible for initialising the default value!)
.required = false, .ptr = &datum_config.datum_pool_pass_full_users, .default_bool = true },
{ .var_type = DATUM_CONF_FUNC, .category = "datum", .name = "pool_username_behaviour", .description = "Whether and how to Pass stratum miner usernames to the pool: \"passthrough\" sends it as-is (overriding mining.pool_address), \"worker\" appends it after mining.pool_address, \"ignore\" disregards it entirely, and \"strip_worker\" passes only the username up until the first period character",
.example_default = true,
.required = false, .ptr_func = &datum_conf_username_behaviour, .default_string[0] = "\"passthrough\"" },
{ .var_type = DATUM_CONF_BOOL, .category = "datum", .name = "always_pay_self", .description = "Always include my datum.pool_username payout in my blocks if possible",
.required = false, .ptr = &datum_config.datum_always_pay_self, .default_bool = true },
{ .var_type = DATUM_CONF_BOOL, .category = "datum", .name = "pooled_mining_only", .description = "If the DATUM pool server becomes unavailable, terminate miner connections (otherwise, 100% of any blocks you find pay mining.pool_address)",
Expand Down Expand Up @@ -221,6 +274,15 @@ json_t *load_json_from_file(const char *file_path) {
return root;
}

const char *datum_config_get_type_string(const T_DATUM_CONFIG_ITEM * const c) {
if (c->var_type == DATUM_CONF_FUNC) {
const char *expected_type;
c->ptr_func(c, NULL, &expected_type);
return expected_type;
}
Comment thread
luke-jr marked this conversation as resolved.
return datum_conf_var_type_text[c->var_type];
}

void datum_config_set_default(const T_DATUM_CONFIG_ITEM *c) {
// set the default
switch(c->var_type) {
Expand Down Expand Up @@ -252,6 +314,13 @@ void datum_config_set_default(const T_DATUM_CONFIG_ITEM *c) {
*umods_p = NULL;
break;
}

case DATUM_CONF_FUNC: {
json_t * const j_null = json_null();
c->ptr_func(c, j_null, NULL);
json_decref(j_null);
break;
}
}
}

Expand Down Expand Up @@ -410,6 +479,10 @@ int datum_config_parse_value(const T_DATUM_CONFIG_ITEM *c, json_t *item) {
case DATUM_CONF_USERNAME_MODS: {
return datum_config_parse_username_mods(c->ptr, item, true);
}

case DATUM_CONF_FUNC: {
return c->ptr_func(c, item, NULL);
}
}

return -1;
Expand Down Expand Up @@ -452,17 +525,45 @@ int datum_read_config(const char *conffile) {
continue;
}

// item might be valid
// item might be valid*
j = datum_config_parse_value(&datum_config_options[i], item);
if (j == -1) {
DLOG_ERROR("Could not parse configuration option %s.%s. Type should be %s", datum_config_options[i].category, datum_config_options[i].name, datum_conf_var_type_text[datum_config_options[i].var_type]);
const T_DATUM_CONFIG_ITEM * const c = &datum_config_options[i];
const char * const expected_type = datum_config_get_type_string(c);
DLOG_ERROR("Could not parse configuration option %s.%s. Type should be %s",
c->category, c->name, expected_type);
return -1;
} else if (j == -2) {
DLOG_ERROR("Configuration option %s.%s exceeds maximum length of %d", datum_config_options[i].category, datum_config_options[i].name, datum_config_options[i].max_string_len - 1);
return -1;
}
}

cat = json_object_get(config, "datum");
if (json_is_object(cat)) {
item = json_object_get(cat, "pool_pass_full_users");
if (item && (!json_is_false(item)) ? !(datum_config.datum_pool_pass_full_users && datum_config.datum_pool_pass_workers) : datum_config.datum_pool_pass_full_users) {
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The conditional intended to warn only when the deprecated key datum.pool_pass_full_users is present appears to have ?: precedence issues: when the key is missing (item == NULL), the expression evaluates to datum_config.datum_pool_pass_full_users and can emit a deprecation warning even though the deprecated option wasn’t configured. Wrap the ternary in parentheses (or restructure) so the warning is gated by item being non-NULL.

Suggested change
if (item && (!json_is_false(item)) ? !(datum_config.datum_pool_pass_full_users && datum_config.datum_pool_pass_workers) : datum_config.datum_pool_pass_full_users) {
if (item && (((!json_is_false(item)) ? !(datum_config.datum_pool_pass_full_users && datum_config.datum_pool_pass_workers) : datum_config.datum_pool_pass_full_users))) {

Copilot uses AI. Check for mistakes.
DLOG_WARN("Deprecated configuration option %s.%s ignored (%s.%s overrides)",
"datum", "pool_pass_full_users",
"datum", "pool_username_behaviour");
} else {
item = json_object_get(cat, "pool_pass_workers");
if (item && (!json_is_false(item)) != datum_config.datum_pool_pass_workers) {
DLOG_WARN("Deprecated configuration option %s.%s ignored (%s.%s overrides)",
"datum", "pool_pass_workers",
"datum", "pool_username_behaviour");
}
}
item = json_object_get(cat, "pool_username_behaviour");
if (((!item) || json_is_null(item)) && datum_config.datum_pool_pass_full_users && !datum_config.datum_pool_pass_workers) {
datum_config.datum_pool_pass_workers = true;
DLOG_WARN("Deprecated configuration option %s.%s ignored (also-deprecated %s.%s overrides; consider migrating to %s.%s)",
"datum", "pool_pass_workers",
"datum", "pool_pass_full_users",
"datum", "pool_username_behaviour");
}
Comment thread
luke-jr marked this conversation as resolved.
}

#ifdef ENABLE_API
if (datum_config.api_modify_conf) {
datum_config.config_json = config;
Expand Down Expand Up @@ -625,14 +726,16 @@ void datum_gateway_help(const char * const argv0) {
puts("Configuration file options:\n\n{");
for (unsigned int i = 0; i < NUM_CONFIG_ITEMS; ++i) {
const T_DATUM_CONFIG_ITEM * const opt = &datum_config_options[i];
if (!opt->description[0]) continue; // deprecated/hidden options
if (strcmp(opt->category, lastcat)) {
if (i) { puts(" },"); }
printf(" \"%s\": {\n", opt->category);
lastcat = opt->category;
}
p = 30 - strlen(opt->name);
if (p < 0) p = 0;
printf(" \"%s\": %.*s %s (%s", opt->name, p, paddots, opt->description, datum_conf_var_type_text[opt->var_type]);
const char * const expected_type = datum_config_get_type_string(opt);
printf(" \"%s\": %.*s %s (%s", opt->name, p, paddots, opt->description, expected_type);
if (opt->required) {
puts(", REQUIRED)");
} else {
Expand All @@ -652,6 +755,13 @@ void datum_gateway_help(const char * const argv0) {
break;
}

case DATUM_CONF_FUNC: {
if (opt->default_string[0]) {
printf(", default: %s)\n", opt->default_string[0]);
}
break;
}

default: {
puts(")");
break;
Expand Down Expand Up @@ -712,6 +822,15 @@ void datum_gateway_example_conf(void) {
puts("{}");
break;
}

case DATUM_CONF_FUNC: {
if (opt->default_string[0]) {
printf("%s", opt->default_string[0]);
} else {
printf("null");
}
break;
}
}
}
}
Expand Down
16 changes: 13 additions & 3 deletions src/datum_conf.h
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,16 @@ enum datum_conf_vartype {
DATUM_CONF_STRING,
DATUM_CONF_STRING_ARRAY,
DATUM_CONF_USERNAME_MODS,
DATUM_CONF_FUNC,
};

typedef struct {
typedef struct T_DATUM_CONFIG_ITEM T_DATUM_CONFIG_ITEM;

// Usage: func(item, config json item, NULL) - assign item
// Usage: func(item, NULL, pointer to const char*) - store type string pointer
typedef int (* const DATUM_Conf_VarFunc)(const T_DATUM_CONFIG_ITEM *, const json_t *, const char **out_type);

struct T_DATUM_CONFIG_ITEM {
char category[32];
char name[64];
char description[512];
Expand All @@ -70,10 +77,13 @@ typedef struct {
};
};

void *ptr;
union {
void *ptr;
DATUM_Conf_VarFunc ptr_func;
};

bool required;
} T_DATUM_CONFIG_ITEM;
};

const T_DATUM_CONFIG_ITEM *datum_config_get_option_info(const char *category, size_t category_len, const char *name, size_t name_len);
const T_DATUM_CONFIG_ITEM *datum_config_get_option_info2(const char *category, const char *name);
Expand Down
2 changes: 2 additions & 0 deletions src/datum_gateway.c
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ struct arguments {
char *config_file;
};

void datum_protocol_tests(void);
void datum_stratum_tests(void);
void datum_conf_tests(void);
void datum_utils_tests(void);
Expand All @@ -105,6 +106,7 @@ static error_t parse_opt(int key, char *arg, struct argp_state *state) {
case 0x101: // test
datum_utils_tests();
datum_conf_tests();
datum_protocol_tests();
datum_stratum_tests();
exit(datum_test_failed);
default:
Expand Down
Loading