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
29 changes: 2 additions & 27 deletions src/datum_api.c
Original file line number Diff line number Diff line change
Expand Up @@ -64,15 +64,6 @@ const char * const homepage_html_end = "</body></html>";

#define DATUM_API_HOMEPAGE_MAX_SIZE 128000

const char *cbnames[] = {
"Blank",
"Tiny",
"Default",
"Respect",
"Yuge",
"Antmain2"
};

typedef struct MHD_Response *(*create_response_func_t)();

static struct MHD_Response *datum_api_create_empty_mhd_response() {
Expand Down Expand Up @@ -923,7 +914,7 @@ int datum_api_client_dashboard(struct MHD_Connection *connection) {
return MHD_YES;
}

sz += snprintf(&output[sz], max_sz-1-sz, "<form action='/cmd' method='post'><input type='hidden' name='csrf' value='%s' /><TABLE><TR><TD><U>TID/CID</U></TD> <TD><U>RemHost</U></TD> <TD><U>Auth Username</U></TD> <TD><U>Subbed</U></TD> <TD><U>Last Accepted</U></TD> <TD><U>VDiff</U></TD> <TD><U>DiffA (A)</U></TD> <TD><U>DiffR (R)</U></TD> <TD><U>Hashrate (age)</U></TD> <TD><U>Coinbase</U></TD> <TD><U>UserAgent</U> </TD><TD><U>Command</U></TD></TR>", datum_config.api_csrf_token);
sz += snprintf(&output[sz], max_sz-1-sz, "<form action='/cmd' method='post'><input type='hidden' name='csrf' value='%s' /><TABLE><TR><TD><U>TID/CID</U></TD> <TD><U>RemHost</U></TD> <TD><U>Auth Username</U></TD> <TD><U>Subbed</U></TD> <TD><U>Last Accepted</U></TD> <TD><U>VDiff</U></TD> <TD><U>DiffA (A)</U></TD> <TD><U>DiffR (R)</U></TD> <TD><U>Hashrate (age)</U></TD> <TD><U>UserAgent</U> </TD><TD><U>Command</U></TD></TR>", datum_config.api_csrf_token);

for (j = 0; j < max_threads; ++j) {
for(ii=0;ii<global_stratum_app->max_clients_thread;ii++) {
Expand Down Expand Up @@ -969,17 +960,11 @@ int datum_api_client_dashboard(struct MHD_Connection *connection) {
sz += snprintf(&output[sz], max_sz-1-sz, "<TD>N/A</TD>");
}

if (m->coinbase_selection < (sizeof(cbnames) / sizeof(cbnames[0]))) {
sz += snprintf(&output[sz], max_sz-1-sz, "<TD>%s</TD>", cbnames[m->coinbase_selection]);
} else {
sz += snprintf(&output[sz], max_sz-1-sz, "<TD>Unknown</TD>");
}

sz += snprintf(&output[sz], max_sz-1-sz, "<TD>");
sz += strncpy_html_escape(&output[sz], m->useragent, max_sz-1-sz);
sz += snprintf(&output[sz], max_sz-1-sz, "</TD>");
} else {
sz += snprintf(&output[sz], max_sz-1-sz, "<TD COLSPAN=\"8\">Not Subscribed</TD>");
sz += snprintf(&output[sz], max_sz-1-sz, "<TD COLSPAN=\"7\">Not Subscribed</TD>");
}

sz += snprintf(&output[sz], max_sz-1-sz, "<TD><button name='kill_client' value='%d_%d_%lu_%lu' onclick=\"sendPostRequest('/cmd', {cmd:'kill_client',tid:%d,cid:%d,t:%lu,id:%lu}); return false;\">Kick</button></TD></TR>", j, ii, (unsigned long)m->connect_tsms, (unsigned long)m->unique_id, j, ii, (unsigned long)m->connect_tsms, (unsigned long)m->unique_id);
Expand Down Expand Up @@ -1344,16 +1329,6 @@ bool datum_api_config_set(const char * const key, const char * const val, struct
// TODO: apply change without restarting
// TODO: switch pools smoother (keep old connection alive for share submissions until those jobs expire)
status->need_restart = true;
} else if (0 == strcmp(key, "stratum_fingerprint_miners")) {
bool val_bool;
if (!datum_str_to_bool_strict(val, &val_bool)) {
json_array_append_new(errors, json_string_nocheck("\"Fingerprint and workaround known miner bugs\" must be 0 or 1"));
return false;
}
if (val_bool == datum_config.stratum_v1_fingerprint_miners) return true;
datum_config.stratum_v1_fingerprint_miners = val_bool;
datum_api_json_modify_new("stratum", "fingerprint_miners", json_boolean(val_bool));
// TODO: apply change to connected miners?
} else if (0 == strcmp(key, "datum_always_pay_self")) {
bool val_bool;
if (!datum_str_to_bool_strict(val, &val_bool)) {
Expand Down
164 changes: 65 additions & 99 deletions src/datum_coinbaser.c

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/datum_coinbaser.h
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ typedef struct T_DATUM_STRATUM_JOB T_DATUM_STRATUM_JOB;

int datum_coinbaser_init(void);
int generate_coinbase_input(int height, char *cb, int *target_pot_index);
void generate_coinbase_txns_for_stratum_job_subtypebysize(T_DATUM_STRATUM_JOB *s, int coinbase_index, int remaining_size, bool space_for_en_in_coinbase, int *cb1idx, int *cb2idx, bool special_coinb1);
void generate_coinbase_txns_for_stratum_job_subtypebysize(T_DATUM_STRATUM_JOB *s, int coinbase_index, int remaining_size, bool space_for_en_in_coinbase, int *cb1idx, int *cb2idx);
void generate_coinbase_txns_for_stratum_job(T_DATUM_STRATUM_JOB *s, bool empty_only);
void generate_base_coinbase_txns_for_stratum_job(T_DATUM_STRATUM_JOB *s, bool new_block);
int datum_coinbaser_v2_parse(T_DATUM_STRATUM_JOB *s, unsigned char *coinbaser, int cblen, bool must_free);
Expand Down
123 changes: 123 additions & 0 deletions src/datum_coinbaser_tests.c
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,14 @@
*
*/

#include <stdlib.h>
#include <string.h>

#include "datum_conf.h"
#include "datum_stratum.h"
#include "datum_coinbaser.h"
#include "datum_utils.h"
#include "datum_pow.h"

int datum_stratum_coinbase_fit_to_template(
int max_sz, int fixed_bytes, T_DATUM_STRATUM_JOB *s);
Expand Down Expand Up @@ -73,9 +75,130 @@ static void datum_blake2b_coinbase_limit_tests(void) {

/* The 164-byte header shrinks the coinbase leftover by 84 bytes. */
datum_test(datum_stratum_coinbase_fit_to_template(1000, 0, &job) == 866);

/* The weight limit: the header and a five-byte count at four units a byte,
* the coinbase's 36 witness bytes, then 950 bytes of coinbase at four
* each. With the 80-byte header's 340 the leftover was 1000 (unbound). */
tdata.sizelimit = 4000000;
tdata.weightlimit = ((DATUM_BLAKE2B_BLOCK_HEADER_SIZE + 5) * 4) + 36 + (4 * 950);
datum_test(datum_stratum_coinbase_fit_to_template(1000, 0, &job) == 950);
/* The transactions' weight counts the same way. */
tdata.txn_total_weight = 4000;
tdata.weightlimit += 4000;
datum_test(datum_stratum_coinbase_fit_to_template(1000, 0, &job) == 950);
/* Fixed bytes are subtracted from the leftover. */
datum_test(datum_stratum_coinbase_fit_to_template(1000, 100, &job) == 850);
}

/* P2PKH: OP_DUP OP_HASH160 <20 bytes> OP_EQUALVERIFY OP_CHECKSIG, 25 bytes. */
static const unsigned char datum_test_p2pkh_script[25] = {0x76, 0xa9, 0x14, [23] = 0x88, 0xac};
/* P2WPKH: OP_0 <20 bytes>, 22 bytes. */
static const unsigned char datum_test_p2wpkh_script[22] = {0x00, 0x14};

/* Builds one coinbase class with the template's used sigop cost set to
* sigops_used and the pool script set to P2PKH or P2WPKH, and returns the hex
* output count that follows the 8-character sequence at the start of coinb2.
* The count covers the included outputs plus the pool output and the witness
* commitment. The job's candidate outputs are two P2PKH outputs (cost 4 each)
* and one P2WPKH output (cost 0). */
static const char *datum_coinbase_output_count_hex(T_DATUM_STRATUM_JOB *job, uint32_t sigops_used, bool pool_p2pkh) {
int cb1idx[MAX_COINBASE_TYPES] = {0};
int cb2idx[MAX_COINBASE_TYPES] = {0};

job->block_template->txn_total_sigops = sigops_used;
if (pool_p2pkh) {
memcpy(job->pool_addr_script, datum_test_p2pkh_script, sizeof(datum_test_p2pkh_script));
job->pool_addr_script_len = sizeof(datum_test_p2pkh_script);
} else {
memcpy(job->pool_addr_script, datum_test_p2wpkh_script, sizeof(datum_test_p2wpkh_script));
job->pool_addr_script_len = sizeof(datum_test_p2wpkh_script);
}
memset(job->coinbase[1].coinb2, 0, sizeof(job->coinbase[1].coinb2));
generate_coinbase_txns_for_stratum_job_subtypebysize(job, 1, 1000, true, cb1idx, cb2idx);
return job->coinbase[1].coinb2 + 8;
}

static void datum_blake2b_coinbase_sigops_tests(void) {
T_DATUM_TEMPLATE_DATA tdata;
T_DATUM_STRATUM_JOB *job = calloc(1, sizeof(*job));
int k;

datum_test(job != NULL);
if (!job) return;
memset(&tdata, 0, sizeof(tdata));
tdata.sigoplimit = 80000;
job->block_template = &tdata;
job->coinbase_value = 5000000000ULL;
for (k = 0; k < 3; k++) {
job->available_coinbase_outputs[k].value_sats = 100000000;
if (k < 2) {
memcpy(job->available_coinbase_outputs[k].output_script, datum_test_p2pkh_script, sizeof(datum_test_p2pkh_script));
job->available_coinbase_outputs[k].output_script_len = sizeof(datum_test_p2pkh_script);
job->available_coinbase_outputs[k].sigops = 4;
} else {
memcpy(job->available_coinbase_outputs[k].output_script, datum_test_p2wpkh_script, sizeof(datum_test_p2wpkh_script));
job->available_coinbase_outputs[k].output_script_len = sizeof(datum_test_p2wpkh_script);
job->available_coinbase_outputs[k].sigops = 0;
}
}
job->available_coinbase_outputs_count = 3;

/* Space for every output: all three, the pool output and the witness commitment. */
datum_test(!strncmp(datum_coinbase_output_count_hex(job, 0, false), "05", 2));
/* Budget for one P2PKH output: the first P2PKH is included, the second is
* skipped, and the P2WPKH is included. */
datum_test(!strncmp(datum_coinbase_output_count_hex(job, 80000 - 4, false), "04", 2));
/* Budget of 0: only the P2WPKH output is included. */
datum_test(!strncmp(datum_coinbase_output_count_hex(job, 80000, false), "03", 2));
/* A P2PKH pool output takes the remaining 4 units of the budget, so neither
* P2PKH candidate is included. */
datum_test(!strncmp(datum_coinbase_output_count_hex(job, 80000 - 4, true), "03", 2));
free(job);
}

/* A split of MAX_COINBASER_OUTPUTS P2WPKH outputs, 31 bytes each, fits one
* coinbase: the output count is written as a three-byte varint and the hex of
* coinb2 stays inside STRATUM_COINBASE2_MAX_LEN. */
static void datum_blake2b_large_coinbase_tests(void) {
T_DATUM_TEMPLATE_DATA tdata;
T_DATUM_STRATUM_JOB *job = calloc(1, sizeof(*job));
int cb1idx[MAX_COINBASE_TYPES] = {0};
int cb2idx[MAX_COINBASE_TYPES] = {0};
size_t cb_bytes;
int k;

datum_test(job != NULL);
if (!job) return;
memset(&tdata, 0, sizeof(tdata));
tdata.sigoplimit = 80000;
job->block_template = &tdata;
job->coinbase_value = 5000000000ULL;
memcpy(job->pool_addr_script, datum_test_p2wpkh_script, sizeof(datum_test_p2wpkh_script));
job->pool_addr_script_len = sizeof(datum_test_p2wpkh_script);
for (k = 0; k < MAX_COINBASER_OUTPUTS; k++) {
job->available_coinbase_outputs[k].value_sats = 1000;
memcpy(job->available_coinbase_outputs[k].output_script, datum_test_p2wpkh_script, sizeof(datum_test_p2wpkh_script));
job->available_coinbase_outputs[k].output_script_len = sizeof(datum_test_p2wpkh_script);
job->available_coinbase_outputs[k].sigops = 0;
}
job->available_coinbase_outputs_count = MAX_COINBASER_OUTPUTS;

generate_coinbase_txns_for_stratum_job_subtypebysize(
job, COINBASE_TYPE_YUGE, MAX_COINBASER_OUTPUTS * 31, true, cb1idx, cb2idx);

/* MAX_COINBASER_OUTPUTS dictated outputs, the pool output and the witness
* commitment: 1026, written as fd followed by 0x0402 little-endian. */
datum_test(!strncmp(job->coinbase[COINBASE_TYPE_YUGE].coinb2 + 8, "fd0204", 6));
datum_test(strlen(job->coinbase[COINBASE_TYPE_YUGE].coinb2) < STRATUM_COINBASE2_MAX_LEN);
cb_bytes = (strlen(job->coinbase[COINBASE_TYPE_YUGE].coinb1) +
strlen(job->coinbase[COINBASE_TYPE_YUGE].coinb2)) / 2 + 12;
datum_test(cb_bytes > 30000 && cb_bytes <= MAX_DICTATED_COINBASE_SIZE);
free(job);
}

void datum_coinbaser_tests(void) {
datum_prime_id_64bit_tests();
datum_blake2b_coinbase_limit_tests();
datum_blake2b_coinbase_sigops_tests();
datum_blake2b_large_coinbase_tests();
}
2 changes: 0 additions & 2 deletions src/datum_conf.c
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,6 @@ const T_DATUM_CONFIG_ITEM datum_config_options[] = {
.required = false, .ptr = &datum_config.stratum_v1_vardiff_quickdiff_delta, .default_int = 8 },
{ .var_type = DATUM_CONF_INT, .category = "stratum", .name = "share_stale_seconds", .description = "How many seconds after a job is generated before a share submission is considered stale?",
.required = false, .ptr = &datum_config.stratum_v1_share_stale_seconds, .default_int = 120 },
{ .var_type = DATUM_CONF_BOOL, .category = "stratum", .name = "fingerprint_miners", .description = "Attempt to fingerprint miners for better use of coinbase space",
.required = false, .ptr = &datum_config.stratum_v1_fingerprint_miners, .default_bool = true },
{ .var_type = DATUM_CONF_INT, .category = "stratum", .name = "idle_timeout_no_subscribe",.description = "Seconds we allow a connection to be idle without seeing a work subscription? (0 disables)",
.required = false, .ptr = &datum_config.stratum_v1_idle_timeout_no_subscribe, .default_int = 15 },
{ .var_type = DATUM_CONF_INT, .category = "stratum", .name = "idle_timeout_no_shares", .description = "Seconds we allow a subscribed connection to be idle without seeing at least one accepted share? (0 disables)",
Expand Down
1 change: 0 additions & 1 deletion src/datum_conf.h
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,6 @@ typedef struct {
int stratum_v1_vardiff_quickdiff_count;
int stratum_v1_vardiff_quickdiff_delta;
int stratum_v1_share_stale_seconds;
bool stratum_v1_fingerprint_miners;
int stratum_v1_idle_timeout_no_subscribe;
int stratum_v1_idle_timeout_no_share;
int stratum_v1_idle_timeout_max_last_work;
Expand Down
15 changes: 12 additions & 3 deletions src/datum_gateway.h
Original file line number Diff line number Diff line change
Expand Up @@ -45,16 +45,25 @@
#endif

// For SV1
// client buffer must be large enough to hold entire coinbase in hex at max size
// BLAKE2b work sends the miner a 39-byte coinb1 and an empty coinb2, so this
// no longer has to hold a coinbase in hex.
// TODO: Make somewhat more dynamic without having to hammer [cm]alloc
#define CLIENT_BUFFER ((16384*3)+1024)

// in ascii hex
// in ascii hex. coinb1 holds the fixed prefix; every dictated output goes in
// coinb2, so only coinb2 grows with the payout count. The share's section
// (0x02) writes each length as a uint16, which caps either part at 65535 bytes;
// coinb2 here is at most 32767 bytes.
#define STRATUM_COINBASE1_MAX_LEN 1024
#define STRATUM_COINBASE2_MAX_LEN 32768
#define STRATUM_COINBASE2_MAX_LEN 65536

#define MAX_COINBASE_TXN_SIZE_BYTES (((STRATUM_COINBASE1_MAX_LEN+STRATUM_COINBASE2_MAX_LEN)>>1)+64)

// A share message (0x27) with its job and coinbase sections: the coinbase's two
// parts reach MAX_COINBASE_TXN_SIZE_BYTES, the rest is the merkle branches, the
// username and the pad.
#define MAX_POW_MESSAGE_SIZE (MAX_COINBASE_TXN_SIZE_BYTES+4096)

#define STRATUM_JOB_INDEX_XOR ((uint16_t)0xC0DE)

void datum_print_banner(void);
Expand Down
10 changes: 5 additions & 5 deletions src/datum_protocol.c
Original file line number Diff line number Diff line change
Expand Up @@ -471,7 +471,7 @@ void datum_protocol_replay_clear(void) {
T_DATUM_REPLAY_PENDING *datum_protocol_replay_add(
const T_DATUM_PROTOCOL_POW *pow, const unsigned char *message,
size_t message_size) {
if (!pow || !message || !message_size || message_size > 32768)
if (!pow || !message || !message_size || message_size > MAX_POW_MESSAGE_SIZE)
return NULL;

T_DATUM_REPLAY_PENDING *pending = calloc(1, sizeof(*pending));
Expand Down Expand Up @@ -1218,7 +1218,7 @@ int datum_protocol_abw_reveal(int len, unsigned char *data) {

pthread_mutex_t datum_protocol_coinbaser_fetch_mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t datum_protocol_coinbaser_fetch_cond = PTHREAD_COND_INITIALIZER;
unsigned char datum_coinbaser_v2_response_buf[2][32768] = { 0 };
unsigned char datum_coinbaser_v2_response_buf[2][DATUM_PROTOCOL_COINBASER_BUFFER_SIZE] = { 0 };
unsigned char *datum_coinbaser_v2_response = NULL;
unsigned char datum_coinbaser_v2_response_buf_idx = 0;
uint64_t datum_coinbaser_v2_response_value[2] = { 0, 0 };
Expand Down Expand Up @@ -1263,7 +1263,7 @@ int datum_protocol_coinbaser_fetch_response(int len, unsigned char *data) {
v = upk_u64le(data, 0);
x = upk_u32le(data, 8);

if ((x > 32768-1) || (x<1) || x > (unsigned int)(len - 12)) {
if ((x > DATUM_PROTOCOL_COINBASER_BUFFER_SIZE-1) || (x<1) || x > (unsigned int)(len - 12)) {
DLOG_DEBUG("Invalid coinbaser received! %lu %lu", (unsigned long)x, (unsigned long)(len-12));
return 0;
}
Expand Down Expand Up @@ -2867,8 +2867,8 @@ static void datum_protocol_pow_forget_failed_send(
// {"params": ["mzjP9Hn7aqaCLM5pSgMSQzgs3gnxSFv91B", "662599770700", "f40c000000000000", "66259976", "48220d13", "00d30000"], "id": 182, "method": "mining.submit"}
int datum_protocol_pow(void *arg) {
T_DATUM_PROTOCOL_POW *pow = arg;
unsigned char msg[32768 + crypto_box_MACBYTES];
unsigned char replay_message[32768];
unsigned char msg[MAX_POW_MESSAGE_SIZE + crypto_box_MACBYTES];
unsigned char replay_message[MAX_POW_MESSAGE_SIZE];
T_DATUM_REPLAY_PENDING *pending;
int i, j;

Expand Down
6 changes: 6 additions & 0 deletions src/datum_protocol.h
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,12 @@
#define DATUM_PROTOCOL_CONNECT_TIMEOUT 30

#define DATUM_PROTOCOL_MAX_CMD_DATA_SIZE 4194304 // 2^22 - protocol limit!

// The coinbaser blob (server subcmd 0x10 response) the gateway accepts. Its
// wire length field is a uint32, so this buffer is the only limit; at 43 bytes
// an output it holds MAX_COINBASER_OUTPUTS taproot payouts. Matches RATUM's
// MAX_COINBASER_BLOB_LEN + 1.
#define DATUM_PROTOCOL_COINBASER_BUFFER_SIZE 65536
#define DATUM_PROTOCOL_BUFFER_SIZE (DATUM_PROTOCOL_MAX_CMD_DATA_SIZE*3)
#define DATUM_PROTOCOL_MAX_USERNAME_LEN 384

Expand Down
14 changes: 7 additions & 7 deletions src/datum_protocol_tests.c
Original file line number Diff line number Diff line change
Expand Up @@ -699,10 +699,10 @@ static void datum_pow_recycled_protocol_job_test(void) {
job->datum_coinbaser_id = (unsigned char)i;
job->prevhash_bin[0] = (unsigned char)(0xa0 + i);
job->nbits_bin[0] = (unsigned char)(0xb0 + i);
job->coinbase[2].coinb1_len = 1;
job->coinbase[2].coinb2_len = 1;
job->coinbase[2].coinb1_bin[0] = (unsigned char)(0xc0 + i);
job->coinbase[2].coinb2_bin[0] = (unsigned char)(0xd0 + i);
job->coinbase[COINBASE_TYPE_YUGE].coinb1_len = 1;
job->coinbase[COINBASE_TYPE_YUGE].coinb2_len = 1;
job->coinbase[COINBASE_TYPE_YUGE].coinb1_bin[0] = (unsigned char)(0xc0 + i);
job->coinbase[COINBASE_TYPE_YUGE].coinb2_bin[0] = (unsigned char)(0xd0 + i);
job->subsidy_only_coinbase.coinb1_len = 1;
job->subsidy_only_coinbase.coinb2_len = 1;
job->subsidy_only_coinbase.coinb1_bin[0] = (unsigned char)(0xe0 + i);
Expand All @@ -713,7 +713,7 @@ static void datum_pow_recycled_protocol_job_test(void) {
pow.datum_job_id = datum_protocol_setup_new_job_idx(&jobs[0]);
pow.sjob = &jobs[0];
memcpy(pow.stratum_job_id, jobs[0].job_id, sizeof(pow.stratum_job_id));
pow.coinbase_id = 2;
pow.coinbase_id = COINBASE_TYPE_YUGE;
pow.blake2b_use_time_offset = true;
pow.ntime = UINT64_C(0x1817161514131211);
pow.nonce = UINT64_C(0x0807060504030201);
Expand Down Expand Up @@ -755,7 +755,7 @@ static void datum_pow_recycled_protocol_job_test(void) {
// Malformed local state must not index beyond the six generated variants.
pow.coinbase_id = MAX_COINBASE_TYPES;
datum_test(datum_protocol_pow_build_message(&pow, msg, sizeof(msg)) == 0);
pow.coinbase_id = 2;
pow.coinbase_id = COINBASE_TYPE_YUGE;
pow.subsidy_only = true;
datum_test(datum_protocol_pow_build_message(&pow, msg, sizeof(msg)) == 0);
pow.coinbase_id = DATUM_COINBASE_ID_EMPTY;
Expand All @@ -765,7 +765,7 @@ static void datum_pow_recycled_protocol_job_test(void) {
datum_test(msg[70] == 0xe0 && msg[71] == 0xf0);
datum_test(datum_jobs[0].server_has_coinbase_empty);
pow.subsidy_only = false;
pow.coinbase_id = 2;
pow.coinbase_id = COINBASE_TYPE_YUGE;

// snprintf returns the untruncated length. Ensure a long address+worker is
// capped to the actual bytes in the protocol username field.
Expand Down
Loading