diff --git a/src/datum_coinbaser.c b/src/datum_coinbaser.c index 9ff4f019..a867fb37 100644 --- a/src/datum_coinbaser.c +++ b/src/datum_coinbaser.c @@ -206,9 +206,21 @@ void generate_coinbase_txns_for_stratum_job_subtypebysize(T_DATUM_STRATUM_JOB *s // technically an output script could be > 0x4B, meaning an extra byte would be eaten here... but that's not currently the standard // this needs to match the loop lower in this function, as the count will get thrown off if it does not. - // TODO: Enforce max sigops! Note: This is not currently enforced in eloipool, either, so punting for now and will monitor network stats to determine priority. + // The sigop cost available to these outputs: the template's sigoplimit + // (from GBT, in sigop cost units, where one legacy CHECKSIG counts 4) minus + // the cost of the template's transactions and minus the cost of the pool's + // own output. available_coinbase_outputs[].sigops is set by the coinbaser + // parser: 4 for a script whose first byte is OP_DUP (0x76, P2PKH) and 0 + // for every other script. The pool output is charged the same way. An + // output whose cost exceeds the remaining budget is skipped, the same as an + // output that exceeds the remaining size, in both this counting pass and + // the writing pass below. + int64_t sigops_budget = (int64_t)s->block_template->sigoplimit - (int64_t)s->block_template->txn_total_sigops; + if ((s->pool_addr_script_len > 0) && (s->pool_addr_script[0] == 0x76)) sigops_budget -= 4; + if (sigops_budget < 0) sigops_budget = 0; + int64_t sigops_left = sigops_budget; for(k=0;kavailable_coinbase_outputs_count;k++) { - if (((s->available_coinbase_outputs[k].output_script_len+9) <= i) && ((mval + s->available_coinbase_outputs[k].value_sats) <= s->coinbase_value)) { + if (((s->available_coinbase_outputs[k].output_script_len+9) <= i) && ((mval + s->available_coinbase_outputs[k].value_sats) <= s->coinbase_value) && (s->available_coinbase_outputs[k].sigops <= sigops_left)) { if ((special_coinb1) && (!c1full) && ((s->available_coinbase_outputs[k].output_script_len+9) <= i2)) { i2 -= (s->available_coinbase_outputs[k].output_script_len+9); c1cnt++; @@ -217,6 +229,7 @@ void generate_coinbase_txns_for_stratum_job_subtypebysize(T_DATUM_STRATUM_JOB *s } i -= (s->available_coinbase_outputs[k].output_script_len+9); + sigops_left -= s->available_coinbase_outputs[k].sigops; m++; mval += s->available_coinbase_outputs[k].value_sats; if (i < 30) break; @@ -244,9 +257,11 @@ void generate_coinbase_txns_for_stratum_job_subtypebysize(T_DATUM_STRATUM_JOB *s // append "m" payouts. find them the same way we did before mval = 0; + sigops_left = sigops_budget; for(k=0;kavailable_coinbase_outputs_count;k++) { - if (((s->available_coinbase_outputs[k].output_script_len+9) <= j) && ((mval + s->available_coinbase_outputs[k].value_sats) <= s->coinbase_value)) { + if (((s->available_coinbase_outputs[k].output_script_len+9) <= j) && ((mval + s->available_coinbase_outputs[k].value_sats) <= s->coinbase_value) && (s->available_coinbase_outputs[k].sigops <= sigops_left)) { j -= (s->available_coinbase_outputs[k].output_script_len+9); + sigops_left -= s->available_coinbase_outputs[k].sigops; m--; mval += s->available_coinbase_outputs[k].value_sats; @@ -326,8 +341,13 @@ int datum_stratum_coinbase_fit_to_template(int max_sz, int fixed_bytes, T_DATUM_ msz1 = j; } - if (((i<<2)+s->block_template->txn_total_weight+340+36) > s->block_template->weightlimit) { - j = ((s->block_template->weightlimit - (s->block_template->txn_total_weight+340+36))>>2) - fixed_bytes; + // Block weight: four units a byte for the header and the transaction count + // (at most five bytes), four a byte for the coinbase (no witness data of + // its own) plus the 36 bytes of witness the node adds to it (marker, flag, + // one 32-byte item), and the template's transactions at their weight. The + // original 340 covered the 80-byte SHA256d header. + if (((i<<2)+s->block_template->txn_total_weight+((DATUM_BLAKE2B_BLOCK_HEADER_SIZE+5)<<2)+36) > s->block_template->weightlimit) { + j = ((s->block_template->weightlimit - (s->block_template->txn_total_weight+((DATUM_BLAKE2B_BLOCK_HEADER_SIZE+5)<<2)+36))>>2) - fixed_bytes; if (j < 0) return 0; msz1 = j; } @@ -687,7 +707,7 @@ void generate_coinbase_txns_for_stratum_job(T_DATUM_STRATUM_JOB *s, bool empty_o // ok, let's figure out how much space, if any, we have for miner payout outputs // we first need to figure out how much space we are using for each type after required data, so let's do that - // witness output = 46 bytes + // witness commitment output = 47 bytes (8 value, 1 length, 38 script) // pool output = pool_addr_script_len + 9 // coinbase itself = cb_input_sz // coinbase len = 1 @@ -695,17 +715,22 @@ void generate_coinbase_txns_for_stratum_job(T_DATUM_STRATUM_JOB *s, bool empty_o // lock time = 4 bytes // "sequence" = 4 bytes // extranonce size = 15 bytes (w/len push needed for either coinbase or OP_RETURN formats) - // output count... could technically be up to three bytes for types 3 + 4, most likely 1 byte for 0,1,2. - // --- lets give ourselves the wiggle room and say 3 bytes + // output count: one byte up to 252 outputs, three bytes past that (the + // largest class holds several hundred); counted at three so the coinbase + // never exceeds what datum_stratum_coinbase_fit_to_template allowed. // - // total static bytes = 46+9+1+41+4+3+4+15 = 123 bytes + // total static bytes = 47+9+1+41+4+3+4+15 = 124 bytes // not-static bytes = pool_addr_script_len + cb_input_sz + (space_for_en_in_coinbase?0:10) // --- it costs 10 extra bytes to do the OP_RETURN based extranonce + // This was 119, three bytes under the transaction with a one-byte output + // count and five under it with a three-byte count; a coinbase built to a + // template's room then exceeded the block's weight limit by up to 20 + // weight units. if (!space_for_en_in_coinbase) { - cb_req_sz[1] = cb_req_sz[2] = cb_req_sz[3] = cb_req_sz[4] = cb_req_sz[5] = 119 + s->pool_addr_script_len + cb_input_sz + 10; + cb_req_sz[1] = cb_req_sz[2] = cb_req_sz[3] = cb_req_sz[4] = cb_req_sz[5] = 124 + s->pool_addr_script_len + cb_input_sz + 10; } else { - cb_req_sz[1] = cb_req_sz[2] = cb_req_sz[3] = cb_req_sz[4] = cb_req_sz[5] = 119 + s->pool_addr_script_len + cb_input_sz; + cb_req_sz[1] = cb_req_sz[2] = cb_req_sz[3] = cb_req_sz[4] = cb_req_sz[5] = 124 + s->pool_addr_script_len + cb_input_sz; cb_req_sz[2] += 10; // always OP_RETURN extranonce for type 2 } diff --git a/src/datum_coinbaser_tests.c b/src/datum_coinbaser_tests.c index d6b1dbcb..efed78a3 100644 --- a/src/datum_coinbaser_tests.c +++ b/src/datum_coinbaser_tests.c @@ -33,11 +33,13 @@ * */ +#include #include #include "datum_conf.h" #include "datum_stratum.h" #include "datum_coinbaser.h" +#include "datum_pow.h" #include "datum_utils.h" int datum_stratum_coinbase_fit_to_template( @@ -48,7 +50,7 @@ static void datum_prime_id_64bit_tests(void) { const uint16_t saved_unique_id = datum_config.coinbase_unique_id; char coinbase_input[1024] = {0}; int target_pot_index = -1; - + datum_config.prime_id = UINT64_C(0x887766555d965e4e); datum_config.coinbase_unique_id = 0x1234; const int coinbase_input_size = generate_coinbase_input( @@ -64,18 +66,88 @@ static void datum_prime_id_64bit_tests(void) { static void datum_blake2b_coinbase_limit_tests(void) { T_DATUM_TEMPLATE_DATA tdata; T_DATUM_STRATUM_JOB job; - + memset(&tdata, 0, sizeof(tdata)); memset(&job, 0, sizeof(job)); job.block_template = &tdata; tdata.sizelimit = 85 + 36 + 950; tdata.weightlimit = 4000000; - + /* 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); + tdata.txn_total_weight = 4000; + tdata.weightlimit += 4000; + datum_test(datum_stratum_coinbase_fit_to_template(1000, 0, &job) == 950); + 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 }; + +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, false); + 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: first P2PKH in, second skipped, P2WPKH in. */ + 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)); + /* P2PKH pool output takes the remaining 4 units, so neither P2PKH candidate is included. */ + datum_test(!strncmp(datum_coinbase_output_count_hex(job, 80000 - 4, true), "03", 2)); + free(job); } void datum_coinbaser_tests(void) { datum_prime_id_64bit_tests(); datum_blake2b_coinbase_limit_tests(); + datum_blake2b_coinbase_sigops_tests(); } diff --git a/src/datum_conf.c b/src/datum_conf.c index 6801cb1c..18ca6a6c 100644 --- a/src/datum_conf.c +++ b/src/datum_conf.c @@ -125,8 +125,16 @@ const T_DATUM_CONFIG_ITEM datum_config_options[] = { .required = false, .ptr = datum_config.mining_coinbase_tag_secondary, .default_string[0] = "DATUM User", .max_string_len = sizeof(datum_config.mining_coinbase_tag_secondary) }, { .var_type = DATUM_CONF_INT, .category = "mining", .name = "coinbase_unique_id", .description = "A unique ID between 1 and 65535. This is appended to the coinbase. Make unique per instance of datum with the same coinbase tags.", .required = false, .ptr = &datum_config.coinbase_unique_id, .default_int = 4242 }, - { .var_type = DATUM_CONF_STRING, .category = "mining", .name = "save_submitblocks_dir", .description = "Directory to save all submitted blocks to as submitblock JSON files", + { .var_type = DATUM_CONF_STRING, .category = "mining", .name = "save_submitblocks_dir", .description = "Directory to save submitted blocks as submitblock JSON (datum_submitblock__.json and datum_submitblock_last.json). Empty disables.", .required = false, .ptr = datum_config.mining_save_submitblocks_dir, .default_string[0] = "", .max_string_len = sizeof(datum_config.mining_save_submitblocks_dir) }, + { .var_type = DATUM_CONF_INT, .category = "mining", .name = "share_node_check_missingzeros", .description = "If >= 0, only log SHARE lines and node-check shares with missingzeros <= this value (overrides share_node_check_every). -1 = infinity (log all, use every-N).", + .required = false, .ptr = &datum_config.mining_share_node_check_missingzeros, .default_int = -1 }, + { .var_type = DATUM_CONF_BOOL, .category = "mining", .name = "validate_shares_on_node", .description = "Ask the local node about sampled accepted shares (proposal or submitblock). Real blocks are always submitted separately.", + .required = false, .ptr = &datum_config.mining_validate_shares_on_node, .default_bool = false }, + { .var_type = DATUM_CONF_STRING, .category = "mining", .name = "share_node_check", .description = "How to ask the node about a share: proposal (GBT mode=proposal, skips PoW) or submitblock (PoW checked first, usually high-hash).", + .required = false, .ptr = datum_config.mining_share_node_check, .default_string[0] = "proposal", .max_string_len = sizeof(datum_config.mining_share_node_check) }, + { .var_type = DATUM_CONF_INT, .category = "mining", .name = "share_node_check_every", .description = "Validate 1 of every N accepted shares on the node (1 = every share). Real blocks are always submitted separately.", + .required = false, .ptr = &datum_config.mining_share_node_check_every, .default_int = 16 }, { .var_type = DATUM_CONF_BOOL, .category = "mining", .name = "allow_hasher_time_rolling", .description = "Allow hasher time rolling for BLAKE2b jobs", .required = false, .ptr = &datum_config.mining_allow_hasher_time_rolling, .default_bool = false }, { .var_type = DATUM_CONF_BOOL, .category = "mining", .name = "abw_verify_all_shares_on_disclosure", .description = "Retain ABW proofs until key disclosure and alarm if the pool ignored a block", @@ -175,6 +183,10 @@ const T_DATUM_CONFIG_ITEM datum_config_options[] = { { .var_type = DATUM_CONF_INT, .category = "logger", .name = "log_level_file", .description = "Minimum log level for log file messages (0=All, 1=Debug, 2=Info, 3=Warn, 4=Error, 5=Fatal)", .example_default = true, .required = false, .ptr = &datum_config.clog_level_file, .default_int = 1 }, + { .var_type = DATUM_CONF_BOOL, .category = "logger", .name = "log_shares", .description = "Log the result of every incoming share at INFO (level 2): accepted or rejected with reason", + .required = false, .ptr = &datum_config.logger_log_shares, .default_bool = false }, + { .var_type = DATUM_CONF_BOOL, .category = "logger", .name = "console_collapse_job_updates", .description = "Collapse repeating stratum job update logs into a single line counter to prevent console flooding.", + .required = false, .ptr = &datum_config.clog_console_collapse_jobs, .default_bool = false }, // datum options { .var_type = DATUM_CONF_STRING, .category = "datum", .name = "pool_host", .description = "Remote DATUM server host/ip to use for decentralized pooled mining (set to \"\" to disable pooled mining)", diff --git a/src/datum_conf.h b/src/datum_conf.h index 85f014e2..2d442b9a 100644 --- a/src/datum_conf.h +++ b/src/datum_conf.h @@ -131,6 +131,10 @@ typedef struct { char mining_coinbase_tag_primary[64]; char mining_coinbase_tag_secondary[64]; char mining_save_submitblocks_dir[256]; + int mining_share_node_check_missingzeros; + bool mining_validate_shares_on_node; + char mining_share_node_check[32]; + int mining_share_node_check_every; bool mining_allow_hasher_time_rolling; bool mining_abw_verify_all_shares_on_disclosure; int coinbase_unique_id; @@ -155,6 +159,8 @@ typedef struct { bool clog_to_stderr; bool clog_rotate_daily; char clog_file[1024]; + bool logger_log_shares; + bool clog_console_collapse_jobs; char datum_pool_host[1024]; int datum_pool_port; diff --git a/src/datum_gateway.c b/src/datum_gateway.c index 88e786c5..baf52096 100644 --- a/src/datum_gateway.c +++ b/src/datum_gateway.c @@ -197,6 +197,13 @@ int main(const int argc, const char * const * const argv) { // Initialize logger thread datum_logger_init(); + if (datum_config.mining_save_submitblocks_dir[0]) { + char gitshort[8]; + memset(gitshort, 0, sizeof(gitshort)); + memcpy(gitshort, GIT_COMMIT_HASH, 7); + DLOG_INFO("submitblock save dir: %s (datum_submitblock__%s.json + datum_submitblock_last.json)", + datum_config.mining_save_submitblocks_dir, gitshort[0] ? gitshort : "unknown"); + } if (datum_protocol_init()) { DLOG_FATAL("Error initializing the DATUM protocol!"); diff --git a/src/datum_logger.c b/src/datum_logger.c index 8f3af90e..8306dd69 100644 --- a/src/datum_logger.c +++ b/src/datum_logger.c @@ -51,9 +51,12 @@ #include #include #include +#include +#include #include "datum_logger.h" #include "datum_utils.h" +#include "datum_conf.h" const char *level_text[] = { " ALL", "DEBUG", " INFO", " WARN", "ERROR", "FATAL" }; @@ -70,6 +73,126 @@ bool log_to_stderr = false; bool log_rotate_daily = true; char log_file[1024] = { 0 }; +static int console_collapse_kind = 0; +static int console_collapse_count = 0; +static int console_collapse_open = 0; + +static int datum_console_job_kind(const char *msg) +{ + if (!msg) return 0; + if (!strncmp(msg, "Updating standard stratum job for block", 39)) return 1; + if (!strncmp(msg, "Updating priority stratum job for block", 39)) return 2; + if (!strcmp(msg, "NEW NETWORK BLOCK NOTIFICATION RECEIVED")) return 3; + if (!strncmp(msg, "NEW NETWORK BLOCK:", 18)) return 4; + return 0; +} + +static int datum_console_is_tty(FILE *out) +{ + int fd; + if (!out) return 0; + fd = fileno(out); + if (fd < 0) return 0; + return isatty(fd); +} + +static int datum_console_cols(FILE *out) +{ + struct winsize ws; + if (ioctl(fileno(out), TIOCGWINSZ, &ws) != 0) return 0; + return (int)ws.ws_col; +} + +static void datum_console_put_inplace(FILE *out, const char *line, int cols) +{ + size_t n; + char buf[1200]; + if (!line) return; + snprintf(buf, sizeof(buf), "%s", line); + n = strlen(buf); + if (n && buf[n - 1] == '\n') buf[--n] = 0; + /* Stay on one visual row so \\r overwrites in tmux. CUU (1A) is wrong + * once the previous line wrapped. */ + if (cols > 8 && (int)n >= cols) { + buf[cols - 1] = 0; + n = (size_t)(cols - 1); + } + if (console_collapse_open) { + fputc('\r', out); + } + fputs(buf, out); + fputs("\033[K", out); + fflush(out); + console_collapse_open = 1; +} + +static void datum_console_write(FILE *out, const char *line_with_nl, const char *msg) +{ + int kind = datum_console_job_kind(msg); + int tty = datum_console_is_tty(out); + int cols = tty ? datum_console_cols(out) : 0; + char rebuilt[1200]; + int can_collapse = datum_config.clog_console_collapse_jobs && tty && kind; + + if (can_collapse && cols <= 0) cols = 80; + + if (!can_collapse) { + if (console_collapse_open) { + fputc('\n', out); + console_collapse_open = 0; + } + console_collapse_kind = 0; + console_collapse_count = 0; + fputs(line_with_nl, out); + return; + } + + if (kind == 4 && console_collapse_kind == 3 && console_collapse_count > 0) { + char base[1200]; + size_t n; + snprintf(base, sizeof(base), "%s", line_with_nl); + n = strlen(base); + if (n && base[n-1] == '\n') base[--n] = 0; + { + const char *colon = strstr(base, ": "); + if (colon) { + size_t pre = (size_t)(colon + 2 - base); + snprintf(rebuilt, sizeof(rebuilt), "%.*sx%d NOTIFICATION + %s", (int)pre, base, console_collapse_count, colon + 2); + } else { + snprintf(rebuilt, sizeof(rebuilt), "x%d NOTIFICATION + %s", console_collapse_count, base); + } + } + datum_console_put_inplace(out, rebuilt, cols); + console_collapse_kind = 4; + console_collapse_count = 1; + return; + } + + if (console_collapse_kind == kind && console_collapse_count > 0 && kind != 4) { + const char *colon; + console_collapse_count++; + colon = strstr(line_with_nl, ": "); + if (colon) { + size_t pre = (size_t)(colon + 2 - line_with_nl); + snprintf(rebuilt, sizeof(rebuilt), "%.*sx%d %s", (int)pre, line_with_nl, console_collapse_count, colon + 2); + } else { + snprintf(rebuilt, sizeof(rebuilt), "%s", line_with_nl); + } + datum_console_put_inplace(out, rebuilt, cols); + return; + } + + if (console_collapse_open) { + fputc('\n', out); + console_collapse_open = 0; + } + console_collapse_kind = kind; + console_collapse_count = 1; + /* First line of a kind is also in-place (no trailing newline) so a + * wrapped job-update cannot sit under a later \\r. */ + datum_console_put_inplace(out, line_with_nl, cols); +} + int dlog_queue_max_entries = 0; int msg_buf_maxsz = DLOG_MSG_BUF_SIZE; @@ -362,7 +485,7 @@ void * datum_logger_thread(void *ptr) { log_line[1199] = 0; if ((log_to_console) && (msg->level >= log_level_console)) { - fprintf(log_to_stderr?stderr:stdout, "%s", log_line); + datum_console_write(log_to_stderr?stderr:stdout, log_line, msg->msg); } if ((log_to_file) && (msg->level >= log_level_file)) { diff --git a/src/datum_stratum.c b/src/datum_stratum.c index a1818faf..c1efbce5 100644 --- a/src/datum_stratum.c +++ b/src/datum_stratum.c @@ -46,6 +46,8 @@ #include #include #include +#include +#include #include "datum_gateway.h" #include "datum_stratum.h" @@ -740,35 +742,153 @@ void send_error_to_client(T_DATUM_CLIENT_DATA *c, uint64_t id, char *e) { stratum_rpc_id_clear(c); } + +static unsigned datum_leading_zero_bits_le(const unsigned char *hash_le) +{ + unsigned n = 0; + int i, b; + if (!hash_le) return 0; + for (i = 31; i >= 0; i--) { + if (hash_le[i] == 0) { + n += 8; + continue; + } + for (b = 7; b >= 0; b--) { + if (hash_le[i] & (unsigned char)(1u << b)) return n; + n++; + } + } + return n; +} + +static void datum_shr_le256(unsigned char *h, unsigned k) +{ + unsigned bytes, bits, i, v, carry, mask; + if (!h || k == 0) return; + if (k >= 256) { + memset(h, 0, 32); + return; + } + bytes = k / 8; + bits = k % 8; + if (bytes) { + memmove(h, h + bytes, 32 - bytes); + memset(h + (32 - bytes), 0, bytes); + } + if (!bits) return; + mask = (1u << bits) - 1u; + carry = 0; + for (i = 31; i != (unsigned)-1; i--) { + v = h[i]; + h[i] = (unsigned char)((v >> bits) | carry); + carry = (v & mask) << (8 - bits); + } +} + +static unsigned datum_missing_block_zero_bits(const unsigned char *share_hash_le, const unsigned char *block_target_le, bool meets_block_target) +{ + unsigned char h[32]; + unsigned lz_h, lz_t, k; + if (meets_block_target) return 0; + if (!share_hash_le || !block_target_le) return 0; + if (compare_hashes(share_hash_le, block_target_le) <= 0) return 0; + lz_h = datum_leading_zero_bits_le(share_hash_le); + lz_t = datum_leading_zero_bits_le(block_target_le); + k = (lz_t > lz_h) ? (lz_t - lz_h) : 0; + memcpy(h, share_hash_le, 32); + datum_shr_le256(h, k); + if (compare_hashes(h, block_target_le) <= 0) return k; + return k + 1; +} + +static void datum_share_hash_to_hex(char *out, const unsigned char *hash_le) { + int i; + if (!hash_le) { out[0] = 0; return; } + for (i = 0; i < 32; i++) { + uchar_to_hex(&out[(31-i)*2], hash_le[i]); + } + out[64] = 0; +} + +static void datum_maybe_validate_share_on_node(uint8_t *block_header, uint8_t *coinbase_txn, size_t coinbase_txn_size, T_DATUM_STRATUM_JOB *job, bool empty_work, const unsigned char *extranonce, const unsigned char *share_hash_le, uint64_t diff, bool was_block, int missing_zeros); +static void stratum_log_share_result(const T_DATUM_CLIENT_DATA *c, const char *username, bool accepted, const char *reason, uint64_t job_diff, uint64_t vardiff, uint64_t blockdiff, int missing_zeros) +{ + const char *host; + const char *user; + char mult[32]; + char diffs[96]; + if (!datum_config.logger_log_shares) return; + if (datum_config.mining_share_node_check_missingzeros >= 0) { + if (missing_zeros < 0 || missing_zeros > datum_config.mining_share_node_check_missingzeros) return; + } + host = (c && c->rem_host[0]) ? c->rem_host : "?"; + user = (username && username[0]) ? username : "?"; + mult[0] = 0; + if (job_diff > 0 && vardiff >= job_diff && (vardiff % job_diff) == 0) { + snprintf(mult, sizeof(mult), " %" PRIu64 "x", vardiff / job_diff); + } + if (vardiff || blockdiff) { + snprintf(diffs, sizeof(diffs), "%" PRIu64 "/%" PRIu64 "/%" PRIu64, job_diff, vardiff, blockdiff); + } else { + snprintf(diffs, sizeof(diffs), "%" PRIu64, job_diff); + } + if (missing_zeros >= 0) { + DLOG_INFO("SHARE%s %s %s @%s ?%s diff=%s missingzeros=%d", + mult, accepted ? "accepted" : "rejected", user, host, + reason ? reason : (accepted ? "ok" : "unknown"), diffs, missing_zeros); + } else { + DLOG_INFO("SHARE%s %s %s @%s ?%s diff=%s", + mult, accepted ? "accepted" : "rejected", user, host, + reason ? reason : (accepted ? "ok" : "unknown"), diffs); + } +} + +static const char *stratum_share_user_guess(T_DATUM_CLIENT_DATA *c) +{ + T_DATUM_MINER_DATA *m; + if (!c || !c->app_client_data) return NULL; + m = c->app_client_data; + if (m->last_auth_username[0]) return m->last_auth_username; + return NULL; +} + static inline void send_unknown_work_error(T_DATUM_CLIENT_DATA *c, uint64_t id) { + stratum_log_share_result(c, stratum_share_user_guess(c), false, "unknown-work", 0, 0, 0, -1); send_error_to_client(c, id, "[20,\"unknown-work\",null]"); } static inline void send_rejected_high_hash_error(T_DATUM_CLIENT_DATA *c, uint64_t id) { + stratum_log_share_result(c, stratum_share_user_guess(c), false, "high-hash", 0, 0, 0, -1); send_error_to_client(c, id, "[23,\"high-hash\",null]"); } static inline void send_rejected_stale(T_DATUM_CLIENT_DATA *c, uint64_t id) { + stratum_log_share_result(c, stratum_share_user_guess(c), false, "stale-work", 0, 0, 0, -1); send_error_to_client(c, id, "[21,\"stale-work\",null]"); } static inline void send_rejected_time_too_old(T_DATUM_CLIENT_DATA *c, uint64_t id) { + stratum_log_share_result(c, stratum_share_user_guess(c), false, "time-too-old", 0, 0, 0, -1); send_error_to_client(c, id, "[21,\"time-too-old\",null]"); } static inline void send_rejected_time_too_new(T_DATUM_CLIENT_DATA *c, uint64_t id) { + stratum_log_share_result(c, stratum_share_user_guess(c), false, "time-too-new", 0, 0, 0, -1); send_error_to_client(c, id, "[21,\"time-too-new\",null]"); } static inline void send_rejected_stale_block(T_DATUM_CLIENT_DATA *c, uint64_t id) { + stratum_log_share_result(c, stratum_share_user_guess(c), false, "stale-prevblk", 0, 0, 0, -1); send_error_to_client(c, id, "[21,\"stale-prevblk\",null]"); } static inline void send_rejected_hnotzero_error(T_DATUM_CLIENT_DATA *c, uint64_t id) { + stratum_log_share_result(c, stratum_share_user_guess(c), false, "H-not-zero", 0, 0, 0, -1); send_error_to_client(c, id, "[23,\"H-not-zero\",null]"); } static inline void send_rejected_duplicate(T_DATUM_CLIENT_DATA *c, uint64_t id) { + stratum_log_share_result(c, stratum_share_user_guess(c), false, "duplicate", 0, 0, 0, -1); send_error_to_client(c, id, "[22,\"duplicate\",null]"); } @@ -1357,6 +1477,19 @@ int client_mining_submit(T_DATUM_CLIENT_DATA *c, uint64_t id, json_t *params_obj // update connection and gateway-local totals stratum_note_share(m, true, job_diff); + { + const int missing_zeros = (int)datum_missing_block_zero_bits(share_hash, job->block_target, was_block); + const uint64_t vardiff = m->current_diff ? m->current_diff : job_diff; + long double bld = job->nbits[0] ? calc_network_difficulty(job->nbits) : 0.0L; + const uint64_t blockdiff = (bld > 0.0L && bld < (long double)UINT64_MAX) ? (uint64_t)(bld + 0.5L) : 0; + stratum_log_share_result(c, username_s, true, was_block ? "block" : "ok", job_diff, vardiff, blockdiff, missing_zeros); + if (was_block) { + char hash_hex[65]; + datum_share_hash_to_hex(hash_hex, share_hash); + DLOG_INFO("SHARE %s mode=block d=%" PRIu64 " => submitted", hash_hex, job_diff); + } + datum_maybe_validate_share_on_node(block_header, full_cb_txn, full_cb_txn_size, job, empty_work, extranonce_bin, share_hash, job_diff, was_block, missing_zeros); + } // update since-snap totals m->share_count_since_snap++; @@ -1450,15 +1583,20 @@ int client_mining_authorize(T_DATUM_CLIENT_DATA *c, uint64_t id, json_t *params_ return 0; } +// On Blake the miner never receives the generation tx (notify is a 39-byte +// stub). Coinbase class only decides how many of the pool's payout outputs +// land in the block. Per-miner type 2 was the TIDES truncate: leftover +// value dumped on pool_address. Once the coinbaser is ready, everyone +// gets YUGE. miner is unused; kept so call sites stay stable. unsigned int datum_stratum_coinbase_index( const T_DATUM_STRATUM_THREADPOOL_DATA *sdata, const T_DATUM_MINER_DATA *miner, bool new_block) { + (void)miner; if (new_block) return DATUM_COINBASE_ID_EMPTY; - if (!sdata || !miner || !sdata->cur_stratum_job || + if (!sdata || !sdata->cur_stratum_job || sdata->cur_stratum_job->job_state < JOB_STATE_FULL_PRIORITY_WAIT_COINBASER || - !sdata->full_coinbase_ready || - miner->coinbase_selection >= MAX_COINBASE_TYPES) return 0; - return miner->coinbase_selection; + !sdata->full_coinbase_ready) return 0; + return COINBASE_TYPE_YUGE; } int send_mining_notify(T_DATUM_CLIENT_DATA *c, bool clean, bool quickdiff, bool new_block) { @@ -1704,9 +1842,11 @@ int client_mining_subscribe(T_DATUM_CLIENT_DATA *c, uint64_t id, json_t *params_ // set default diff m->current_diff = datum_config.stratum_v1_vardiff_min; - // default to the antminer workaround, which appears to be universally compatible - // except for NiceHash. - m->coinbase_selection = 2; + // Blake miners are blind to generation-tx size. Default to YUGE so the + // API / logs match what datum_stratum_coinbase_index will actually + // commit once the coinbaser is ready. Fingerprinting may still raise + // NiceHash min-diff; class is forced back to YUGE after that. + m->coinbase_selection = COINBASE_TYPE_YUGE; m->useragent[0] = 0; if (params_obj) { @@ -1725,6 +1865,9 @@ int client_mining_subscribe(T_DATUM_CLIENT_DATA *c, uint64_t id, json_t *params_ m->current_diff = datum_config.stratum_v1_vardiff_min; } } + // Fingerprint may have overwritten class for SHA256d-era size limits. + // Those limits do not apply on Blake; keep NiceHash min-diff only. + m->coinbase_selection = COINBASE_TYPE_YUGE; // get a new unique session ID for this connection (extranonce1) sid = get_new_session_id(c); @@ -2327,6 +2470,198 @@ bool datum_stratum_abw_finalize_block_request(char *request, size_t request_size return true; } +struct datum_share_node_check_job { + char *req; + char mode[16]; + char hash[65]; + uint64_t diff; +}; + +static volatile int datum_share_node_check_in_flight = 0; + +static void datum_share_node_check_clear_in_flight(void) { + __atomic_store_n(&datum_share_node_check_in_flight, 0, __ATOMIC_RELEASE); +} + +static void *datum_share_node_check_thread(void *arg) { + struct datum_share_node_check_job *job = arg; + CURL *curl; + json_t *r = NULL, *result = NULL, *err = NULL; + char *dump; + + curl = curl_easy_init(); + if (!curl) { + DLOG_WARN("SHARE node-check could not init curl"); + goto out; + } + + r = bitcoind_json_rpc_call(curl, &datum_config, job->req); + curl_easy_cleanup(curl); + + if (r) { + result = json_object_get(r, "result"); + err = json_object_get(r, "error"); + } + + if (r && result && json_is_null(result) && (!err || json_is_null(err))) { + DLOG_INFO("SHARE %s mode=%s d=%" PRIu64 " => null (seems valid)", job->hash, job->mode, job->diff); + } else if (!r) { + DLOG_INFO("SHARE %s mode=%s d=%" PRIu64 " => transport/HTTP error (no JSON)", job->hash, job->mode, job->diff); + } else if (err && !json_is_null(err)) { + dump = json_dumps(err, JSON_ENCODE_ANY); + DLOG_INFO("SHARE %s mode=%s d=%" PRIu64 " => RPC error %s", job->hash, job->mode, job->diff, dump ? dump : "(unknown)"); + free(dump); + } else if (result && json_is_string(result)) { + DLOG_INFO("SHARE %s mode=%s d=%" PRIu64 " => %s", job->hash, job->mode, job->diff, json_string_value(result)); + } else { + dump = json_dumps(result, JSON_ENCODE_ANY); + DLOG_INFO("SHARE %s mode=%s d=%" PRIu64 " => %s", job->hash, job->mode, job->diff, dump ? dump : "(unknown)"); + free(dump); + } + + if (r) json_decref(r); +out: + free(job->req); + free(job); + datum_share_node_check_clear_in_flight(); + return NULL; +} + +static char *datum_write_assembled_block_hex(char *ptr, uint8_t *block_header, uint8_t *coinbase_txn, size_t coinbase_txn_size, T_DATUM_STRATUM_JOB *job, bool empty_work, const unsigned char *extranonce) { + size_t i; + unsigned char v2hdr[DATUM_BLAKE2B_BLOCK_HEADER_SIZE]; + unsigned char merkle[32]; + unsigned char en[12]; + unsigned char cbh[32]; + uint16_t txcount; + bool add_witness; + char cbhex[(MAX_COINBASE_TXN_SIZE_BYTES + 36) * 2 + 1]; + size_t cbhex_len; + const T_DATUM_TEMPLATE_DATA *td; + + td = job->block_template; + txcount = (uint16_t)((empty_work ? 0 : td->txn_count) + 1); + if ((job->merklebranch_count) && (!empty_work)) { + double_sha256(cbh, coinbase_txn, coinbase_txn_size); + stratum_job_merkle_root_calc(job, cbh, merkle); + } else { + double_sha256(merkle, coinbase_txn, coinbase_txn_size); + } + memset(en, 0, sizeof(en)); + if (extranonce) memcpy(en, extranonce, 12); + datum_blake2b_serialize_block_header(v2hdr, job->version_uint, job->prevhash_bin, merkle, job->blake2b_time_on_wire, job->nbits_uint, block_header + 32, block_header + 40, en, txcount, job->blake2b_flags, 0, (const unsigned char[16]){0}, (uint32_t)job->height, (const unsigned char[32]){0}); + for(i=0;iblock_template->txn_count + 1, ptr); + } else { + ptr += append_bitcoin_varint_hex(1, ptr); + } + add_witness = datum_stratum_block_needs_witness(job, empty_work); + cbhex_len = datum_stratum_coinbase_for_block_hex(cbhex, sizeof(cbhex), coinbase_txn, coinbase_txn_size, add_witness); + if (!cbhex_len) { + for(i=0;iblock_template->txn_count;i++) { + memcpy(ptr, job->block_template->txns[i].txn_data_hex, job->block_template->txns[i].size*2); + ptr += job->block_template->txns[i].size*2; + } + } + *ptr = 0; + return ptr; +} + +static void datum_maybe_validate_share_on_node(uint8_t *block_header, uint8_t *coinbase_txn, size_t coinbase_txn_size, T_DATUM_STRATUM_JOB *job, bool empty_work, const unsigned char *extranonce, const unsigned char *share_hash_le, uint64_t diff, bool was_block, int missing_zeros) { + static uint64_t accepted_seen = 0; + uint64_t n; + int every; + size_t hex_guess; + char *buf, *hex_end, *req; + struct datum_share_node_check_job *cj; + pthread_t thr; + pthread_attr_t attr; + const char *mode; + size_t i; + + if (was_block) return; + if (!datum_config.mining_validate_shares_on_node) return; + if (!job || !job->block_template || !block_header || !coinbase_txn) return; + + if (datum_config.mining_share_node_check_missingzeros >= 0) { + if (missing_zeros < 0 || missing_zeros > datum_config.mining_share_node_check_missingzeros) return; + } else { + every = datum_config.mining_share_node_check_every; + if (every < 1) every = 1; + n = __atomic_add_fetch(&accepted_seen, 1, __ATOMIC_RELAXED); + if ((n % (uint64_t)every) != 0) return; + } + if (!__sync_bool_compare_and_swap(&datum_share_node_check_in_flight, 0, 1)) { + DLOG_DEBUG("SHARE node-check skipped (already in flight)"); + return; + } + + mode = datum_config.mining_share_node_check[0] ? datum_config.mining_share_node_check : "proposal"; + hex_guess = 1024 + coinbase_txn_size * 2; + if (!empty_work) { + for (i = 0; i < job->block_template->txn_count; i++) { + hex_guess += job->block_template->txns[i].size * 2; + } + } + buf = malloc(hex_guess + 8); + if (!buf) { + datum_share_node_check_clear_in_flight(); + return; + } + hex_end = datum_write_assembled_block_hex(buf, block_header, coinbase_txn, coinbase_txn_size, job, empty_work, extranonce); + if (!hex_end) { + free(buf); + datum_share_node_check_clear_in_flight(); + return; + } + + req = malloc(strlen(buf) + 192); + if (!req) { + free(buf); + datum_share_node_check_clear_in_flight(); + return; + } + if (strcasecmp(mode, "submitblock") == 0) { + sprintf(req, "{\"jsonrpc\":\"1.0\",\"id\":\"sharecheck\",\"method\":\"submitblock\",\"params\":[\"%s\"]}", buf); + } else { + sprintf(req, "{\"jsonrpc\":\"1.0\",\"id\":\"sharecheck\",\"method\":\"getblocktemplate\",\"params\":[{\"mode\":\"proposal\",\"data\":\"%s\"}]}", buf); + } + free(buf); + + cj = calloc(1, sizeof(*cj)); + if (!cj) { + free(req); + datum_share_node_check_clear_in_flight(); + return; + } + cj->req = req; + strncpy(cj->mode, mode, sizeof(cj->mode) - 1); + datum_share_hash_to_hex(cj->hash, share_hash_le); + cj->diff = diff; + + pthread_attr_init(&attr); + pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); + if (pthread_create(&thr, &attr, datum_share_node_check_thread, cj) != 0) { + free(cj->req); + free(cj); + pthread_attr_destroy(&attr); + datum_share_node_check_clear_in_flight(); + return; + } + pthread_attr_destroy(&attr); +} + int assembleBlockAndSubmit(uint8_t *block_header, uint8_t *coinbase_txn, size_t coinbase_txn_size, T_DATUM_STRATUM_JOB *job, T_DATUM_STRATUM_THREADPOOL_DATA *sdata, const char *block_hash_hex, bool empty_work, const unsigned char *extranonce) { // TODO: Also submit directly to bitcoin P2P char *submitblock_req = NULL; @@ -2427,22 +2762,50 @@ int assembleBlockAndSubmit(uint8_t *block_header, uint8_t *coinbase_txn, size_t // this way we can have a (future) external watchdog monitoring the folder as a backup to submit the blocks if need be // for added security. The thread above should already be submitting this block anyway. if (datum_config.mining_save_submitblocks_dir[0] != 0) { - // save the block submission to a file named by the block's hash - char submitblockpath[384]; - int n = snprintf(submitblockpath, sizeof(submitblockpath), "%s/datum_submitblock_%s.json", datum_config.mining_save_submitblocks_dir, block_hash_hex); - - if (n >= sizeof(submitblockpath)) { + char unique_path[384]; + char last_path[384]; + const char *dir = datum_config.mining_save_submitblocks_dir; + size_t req_len = (size_t)(ptr - submitblock_req); + uint64_t height = job ? job->height : 0; + char gitshort[8]; + int n; + memset(gitshort, 0, sizeof(gitshort)); + memcpy(gitshort, GIT_COMMIT_HASH, 7); + n = snprintf(unique_path, sizeof(unique_path), "%s/datum_submitblock_%lu_%s.json", + dir, (unsigned long)height, gitshort[0] ? gitshort : "unknown"); + if (n < 0 || n >= (int)sizeof(unique_path)) { DLOG_ERROR("Overflow in construction of submitblock path!"); } else { - FILE *f; - f = fopen(submitblockpath, "w"); + FILE *f = fopen(unique_path, "w"); + size_t wr; + if (!f) { + DLOG_ERROR("Could not open %s for writing submitblock record: %s", unique_path, strerror(errno)); + } else { + wr = fwrite(submitblock_req, 1, req_len, f); + fflush(f); + if (fileno(f) >= 0) fsync(fileno(f)); + if (fclose(f) != 0 || wr != req_len) { + DLOG_ERROR("Could not write %s submitblock record: %s", unique_path, strerror(errno)); + } else { + DLOG_INFO("Wrote submitblock dump (%zu bytes) to %s", wr, unique_path); + } + } + } + n = snprintf(last_path, sizeof(last_path), "%s/datum_submitblock_last.json", dir); + if (n > 0 && n < (int)sizeof(last_path)) { + FILE *f = fopen(last_path, "w"); + size_t wr; if (!f) { - DLOG_ERROR("Could not open %s for writing submitblock record to disk: %s!", submitblockpath, strerror(errno)); + DLOG_ERROR("Could not open %s for writing submitblock record: %s", last_path, strerror(errno)); } else { - if (!fwrite(submitblock_req, ptr-submitblock_req, 1, f)) { - DLOG_ERROR("Could not write to %s when writing submitblock record to disk: %s!", submitblockpath, strerror(errno)); + wr = fwrite(submitblock_req, 1, req_len, f); + fflush(f); + if (fileno(f) >= 0) fsync(fileno(f)); + if (fclose(f) != 0 || wr != req_len) { + DLOG_ERROR("Could not write %s submitblock record: %s", last_path, strerror(errno)); + } else { + DLOG_INFO("Wrote submitblock dump (%zu bytes) to %s", wr, last_path); } - fclose(f); } } } diff --git a/src/datum_stratum_tests.c b/src/datum_stratum_tests.c index 17199bba..3f71064a 100644 --- a/src/datum_stratum_tests.c +++ b/src/datum_stratum_tests.c @@ -212,9 +212,11 @@ static void datum_blake2b_coinbase_selection_tests(void) { sdata->full_coinbase_ready = true; datum_test(datum_stratum_coinbase_index(sdata, &miner, false) == 0); job.job_state = JOB_STATE_FULL_PRIORITY_WAIT_COINBASER; - datum_test(datum_stratum_coinbase_index(sdata, &miner, false) == 3); + datum_test(datum_stratum_coinbase_index(sdata, &miner, false) == COINBASE_TYPE_YUGE); miner.coinbase_selection = MAX_COINBASE_TYPES; - datum_test(datum_stratum_coinbase_index(sdata, &miner, false) == 0); + datum_test(datum_stratum_coinbase_index(sdata, &miner, false) == COINBASE_TYPE_YUGE); + miner.coinbase_selection = 2; + datum_test(datum_stratum_coinbase_index(sdata, &miner, false) == COINBASE_TYPE_YUGE); free(sdata); }