diff --git a/src/datum_api.c b/src/datum_api.c index 7904f04f..0959bdb2 100644 --- a/src/datum_api.c +++ b/src/datum_api.c @@ -663,6 +663,165 @@ void datum_api_cmd_kill_client2(const char * const data, const size_t size, cons datum_api_cmd_kill_client(tid, cid); } +static int datum_api_send_json(struct MHD_Connection *connection, char *json, int must_free) +{ + struct MHD_Response *response; + if (!json) { + return datum_api_do_error(connection, MHD_HTTP_INTERNAL_SERVER_ERROR); + } + if (must_free) { + response = MHD_create_response_from_buffer(strlen(json), json, MHD_RESPMEM_MUST_FREE); + } else { + response = MHD_create_response_from_buffer(strlen(json), (void *)json, MHD_RESPMEM_MUST_COPY); + } + if (!response) { + if (must_free) { + free(json); + } + return MHD_NO; + } + MHD_add_response_header(response, "Content-Type", "application/json"); + return datum_api_submit_uncached_response(connection, MHD_HTTP_OK, response); +} + +static void json_escape_user(char *dst, size_t dst_len, const char *src) +{ + size_t i = 0, o = 0; + if (!dst || !dst_len) { + return; + } + dst[0] = 0; + if (!src) { + return; + } + for (; src[i] && o + 2 < dst_len; i++) { + if (src[i] == '"' || src[i] == '\\') { + if (o + 3 >= dst_len) { + break; + } + dst[o++] = '\\'; + dst[o++] = src[i]; + } else if ((unsigned char)src[i] < 32) { + continue; + } else { + dst[o++] = src[i]; + } + } + dst[o] = 0; +} + +static void datum_api_cmd_kill_client_checked(int tid, int cid, json_t *root) +{ + json_t *jt = json_object_get(root, "t"); + json_t *jid = json_object_get(root, "id"); + if (!jt) { + jt = json_object_get(root, "connect_tsms"); + } + if (!jid) { + jid = json_object_get(root, "unique_id"); + } + if (!global_stratum_app || tid < 0 || tid >= global_stratum_app->max_threads + || cid < 0 || cid >= global_stratum_app->max_clients_thread) { + return; + } + if (json_is_integer(jt) || json_is_integer(jid)) { + const T_DATUM_MINER_DATA * const m = global_stratum_app->datum_threads[tid].client_data[cid].app_client_data; + if (!m) { + return; + } + if (json_is_integer(jt) && (uint64_t)json_integer_value(jt) != m->connect_tsms) { + DLOG_WARN("API kill_client ignored; connect tsms mismatch %d/%d", tid, cid); + return; + } + if (json_is_integer(jid) && (uint64_t)json_integer_value(jid) != m->unique_id) { + DLOG_WARN("API kill_client ignored; unique id mismatch %d/%d", tid, cid); + return; + } + } + datum_api_cmd_kill_client(tid, cid); +} + +static int datum_api_cmd_list_clients(struct MHD_Connection *connection) +{ + char *output = NULL; + size_t cap, used = 0; + int j, ii, connected = 0; + uint64_t tsms; + const int max_threads = global_stratum_app ? global_stratum_app->max_threads : 0; + + if (global_stratum_app) { + for (j = 0; j < max_threads; ++j) { + connected += global_stratum_app->datum_threads[j].connected_clients; + } + } + if (connected < 0) { + connected = 0; + } + cap = 256 + ((size_t)connected + 1) * 256; + output = calloc(1, cap); + if (!output) { + return datum_api_do_error(connection, MHD_HTTP_INTERNAL_SERVER_ERROR); + } + used = (size_t)snprintf(output, cap, "{\"ok\":true,\"accept_sv1\":%s,\"clients\":[", + datum_stratum_accept_clients() ? "true" : "false"); + tsms = current_time_millis(); + for (j = 0; j < max_threads; ++j) { + for (ii = 0; ii < global_stratum_app->max_clients_thread; ii++) { + T_DATUM_MINER_DATA *m; + double hr = 0.0; + unsigned char astat; + char user[256]; + int n; + if (global_stratum_app->datum_threads[j].client_data[ii].fd <= 0) { + continue; + } + m = (T_DATUM_MINER_DATA *)global_stratum_app->datum_threads[j].client_data[ii].app_client_data; + if (!m) { + continue; + } + astat = m->stats.active_index ? 0 : 1; + if ((m->stats.last_swap_ms > 0) && (m->stats.diff_accepted[astat] > 0)) { + hr = ((double)m->stats.diff_accepted[astat] / (double)((double)m->stats.last_swap_ms / 1000.0)) * 0.004294967296; + } + if (((double)(tsms - m->stats.last_swap_tsms) / 1000.0) >= 180.0) { + hr = 0.0; + } + json_escape_user(user, sizeof user, m->last_auth_username); + if (used + 220 >= cap) { + size_t ncap = cap * 2; + char *nb = realloc(output, ncap); + if (!nb) { + free(output); + return datum_api_do_error(connection, MHD_HTTP_INTERNAL_SERVER_ERROR); + } + output = nb; + cap = ncap; + } + n = snprintf(output + used, cap - used, + "%s{\"tid\":%d,\"cid\":%d,\"connect_tsms\":%llu,\"unique_id\":%llu," + "\"hs\":%.8g,\"user\":\"%s\"}", + used > 60 && output[used - 1] != '[' ? "," : "", + j, ii, (unsigned long long)m->connect_tsms, + (unsigned long long)m->unique_id, hr * 1e12, user); + if (n < 0 || (size_t)n >= cap - used) { + free(output); + return datum_api_do_error(connection, MHD_HTTP_INTERNAL_SERVER_ERROR); + } + used += (size_t)n; + } + } + if (used + 3 >= cap) { + char *nb = realloc(output, used + 4); + if (!nb) { + free(output); + return datum_api_do_error(connection, MHD_HTTP_INTERNAL_SERVER_ERROR); + } + output = nb; + } + memcpy(output + used, "]}", 3); + return datum_api_send_json(connection, output, 1); +} + int datum_api_cmd(struct MHD_Connection *connection, char *post, int len) { struct MHD_Response *response; char output[1024]; @@ -707,13 +866,32 @@ int datum_api_cmd(struct MHD_Connection *connection, char *post, int len) { param = json_object_get(root, "cid"); if (json_is_integer(param)) { cid = json_integer_value(param); - datum_api_cmd_kill_client(tid,cid); + datum_api_cmd_kill_client_checked(tid, cid, root); } } break; } break; } + case 'l': { + if (!strcmp(cstr, "list_clients")) { + json_decref(root); + return datum_api_cmd_list_clients(connection); + } + break; + } + case 's': { + if (!strcmp(cstr, "set_accept_sv1")) { + param = json_object_get(root, "accept"); + datum_stratum_set_accept_clients(json_is_true(param)); + json_decref(root); + return datum_api_send_json(connection, + datum_stratum_accept_clients() + ? "{\"ok\":true,\"accept_sv1\":true}" + : "{\"ok\":true,\"accept_sv1\":false}", 0); + } + break; + } default: break; } } diff --git a/src/datum_coinbaser.c b/src/datum_coinbaser.c index 9ff4f019..adce0705 100644 --- a/src/datum_coinbaser.c +++ b/src/datum_coinbaser.c @@ -184,6 +184,53 @@ int generate_coinbase_input(int height, char *cb, int *target_pot_index) { return cb_input_sz; } +/* Bitcoin GetLegacySigOpCount on one scriptPubKey, times WITNESS_SCALE_FACTOR. + * CHECKSIG / CHECKSIGVERIFY cost 4. CHECKMULTISIG / CHECKMULTISIGVERIFY cost 80 + * (20 keys, not the accurate BIP16 count). Pushes are skipped so a CHECKSIG + * byte inside data is not counted. A truncated push stops the walk. */ +int datum_script_legacy_sigop_cost(const unsigned char *script, int len) { + size_t i = 0; + int n = 0; + const size_t nlen = (len > 0) ? (size_t)len : 0; + + if (!script || !nlen) return 0; + + while (i < nlen) { + const unsigned char opcode = script[i++]; + size_t push = 0; + + if (opcode <= 0x4b) { + push = opcode; + } else if (opcode == 0x4c) { // OP_PUSHDATA1 + if (i >= nlen) break; + push = script[i++]; + } else if (opcode == 0x4d) { // OP_PUSHDATA2 + if (i + 1 >= nlen) break; + push = (size_t)script[i] | ((size_t)script[i + 1] << 8); + i += 2; + } else if (opcode == 0x4e) { // OP_PUSHDATA4 + uint32_t npush; + if (i + 3 >= nlen) break; + npush = (uint32_t)script[i] | ((uint32_t)script[i + 1] << 8) | ((uint32_t)script[i + 2] << 16) | ((uint32_t)script[i + 3] << 24); + i += 4; + if (npush > nlen - i) break; + i += npush; + continue; + } else if ((opcode == 0xac) || (opcode == 0xad)) { // CHECKSIG / VERIFY + n++; + } else if ((opcode == 0xae) || (opcode == 0xaf)) { // CHECKMULTISIG / VERIFY + n += 20; + } + + if (push) { + if (push > nlen - i) break; + i += push; + } + } + + return n * 4; +} + 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) { // This function finishes off the stratum coinb1+coinb2 using the available outputs in the job and other flags specified. // it does not attempt to maximize coinb1's size to any specific size @@ -206,9 +253,23 @@ 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 from GetLegacySigOpCount * WITNESS_SCALE_FACTOR, not a first-byte + // P2PKH guess. 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) { + sigops_budget -= datum_script_legacy_sigop_cost(s->pool_addr_script, s->pool_addr_script_len); + } + 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 +278,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 +306,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; @@ -307,7 +371,7 @@ void generate_coinbase_txns_for_stratum_job_subtypebysize(T_DATUM_STRATUM_JOB *s cb2idx[coinbase_index] += sprintf(&s->coinbase[coinbase_index].coinb2[cb2idx[coinbase_index]], "0000000000000000036a0100"); // TODO: Is a naked OP_RETURN without any bytes after safe? Above TODO is probably better than investigating. } - // witness commit output costs 46 bytes + // witness commitment output costs 47 bytes (8 value, 1 length, 38 script) // append the default_witness_commitment cb2idx[coinbase_index] += sprintf(&s->coinbase[coinbase_index].coinb2[cb2idx[coinbase_index]], "0000000000000000%2.2x%s", (unsigned int)strlen(s->block_template->default_witness_commitment)>>1, s->block_template->default_witness_commitment); // lock time @@ -326,8 +390,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; } @@ -455,7 +524,7 @@ void generate_base_coinbase_txns_for_stratum_job(T_DATUM_STRATUM_JOB *s, bool ne k = cb2idx[0]; } - // witness commit output costs 46 bytes + // witness commitment output costs 47 bytes (8 value, 1 length, 38 script) // append the default_witness_commitment cb2idx[0] += sprintf(&s->coinbase[0].coinb2[cb2idx[0]], "0000000000000000%2.2x%s", (unsigned int)strlen(s->block_template->default_witness_commitment)>>1, s->block_template->default_witness_commitment); // lock time @@ -661,7 +730,7 @@ void generate_coinbase_txns_for_stratum_job(T_DATUM_STRATUM_JOB *s, bool empty_o k = cb2idx[0]; } - // witness commit output costs 46 bytes + // witness commitment output costs 47 bytes (8 value, 1 length, 38 script) // append the default_witness_commitment cb2idx[0] += sprintf(&s->coinbase[0].coinb2[cb2idx[0]], "0000000000000000%2.2x%s", (unsigned int)strlen(s->block_template->default_witness_commitment)>>1, s->block_template->default_witness_commitment); // lock time @@ -687,7 +756,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 +764,23 @@ 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 } @@ -783,6 +858,7 @@ int datum_coinbaser_v2_parse(T_DATUM_STRATUM_JOB *s, unsigned char *coinbaser, i // 0 outputs possible DLOG_WARN("Coinbaser length is invalid (too short). Using default/empty"); s->available_coinbase_outputs_count = 0; + if (must_free) free(coinbaser); return 0; } @@ -794,10 +870,11 @@ int datum_coinbaser_v2_parse(T_DATUM_STRATUM_JOB *s, unsigned char *coinbaser, i if (cidx + 8 + 1 > cblen) { DLOG_ERROR("Coinbaser length is invalid (mid-parsing). Using default/empty"); s->available_coinbase_outputs_count = 0; + if (must_free) free(coinbaser); return 0; } outval = upk_u64le(coinbaser, cidx); cidx+=8; - if ((outval + tally) > s->coinbase_value) { + if (outval > s->coinbase_value - tally) { // we can't include this value, since it would put us over our total available! // this shouldn't happen, however... break; @@ -806,6 +883,7 @@ int datum_coinbaser_v2_parse(T_DATUM_STRATUM_JOB *s, unsigned char *coinbaser, i if (slen < 2 || slen > 64 || cidx + slen > cblen) { DLOG_ERROR("Script length (%d) is invalid. Using default/empty", slen); s->available_coinbase_outputs_count = 0; + if (must_free) free(coinbaser); return 0; } @@ -813,11 +891,8 @@ int datum_coinbaser_v2_parse(T_DATUM_STRATUM_JOB *s, unsigned char *coinbaser, i memcpy(s->available_coinbase_outputs[cbvalid].output_script, &coinbaser[cidx], slen); cidx+=slen; // 64-bit value in sats is part of the output s->available_coinbase_outputs[cbvalid].value_sats = outval; - if (s->available_coinbase_outputs[cbvalid].output_script[0] == 0x76) { // kludge for checking for P2PKH output - s->available_coinbase_outputs[cbvalid].sigops = 4; - } else { - s->available_coinbase_outputs[cbvalid].sigops = 0; - } + s->available_coinbase_outputs[cbvalid].sigops = datum_script_legacy_sigop_cost( + s->available_coinbase_outputs[cbvalid].output_script, slen); s->available_coinbase_outputs[cbvalid].output_script_len = slen; diff --git a/src/datum_coinbaser.h b/src/datum_coinbaser.h index ad38683d..e64b0116 100644 --- a/src/datum_coinbaser.h +++ b/src/datum_coinbaser.h @@ -46,5 +46,6 @@ void generate_coinbase_txns_for_stratum_job_subtypebysize(T_DATUM_STRATUM_JOB *s 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); +int datum_script_legacy_sigop_cost(const unsigned char *script, int len); #endif diff --git a/src/datum_coinbaser_tests.c b/src/datum_coinbaser_tests.c index d6b1dbcb..4ab5af04 100644 --- a/src/datum_coinbaser_tests.c +++ b/src/datum_coinbaser_tests.c @@ -33,12 +33,14 @@ * */ +#include #include #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); @@ -73,9 +75,163 @@ 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); +} + +static void datum_coinbaser_value_overflow_tests(void) { + T_DATUM_STRATUM_JOB job = {.coinbase_value = UINT64_C(5000000000)}; + unsigned char response[] = { + 1, + 1, 0, 0, 0, 0, 0, 0, 0, 2, 0x51, 0x51, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 2, 0x51, 0x51, + }; + + datum_test(datum_coinbaser_v2_parse(&job, response, sizeof(response), false) == 1); + datum_test(job.available_coinbase_outputs_count == 1); + datum_test(job.available_coinbase_outputs[0].value_sats == 1); +} + +/* 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, 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: 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); +} + +/* Compressed P2PK: OP_DATA_33 <33 bytes> OP_CHECKSIG. First byte is 0x21, not + * OP_DUP, so the old first-byte guess charged 0. */ +static const unsigned char datum_test_p2pk_script[35] = {0x21, [34] = 0xac}; + +static void datum_script_legacy_sigop_cost_tests(void) { + const unsigned char p2sh[23] = {0xa9, 0x14, [22] = 0x87}; + const unsigned char p2tr[34] = {0x51, 0x20}; + const unsigned char checksig_in_push[2] = {0x01, 0xac}; + const unsigned char bare_multisig[3] = {0x51, 0x51, 0xae}; + const unsigned char two_checksig[2] = {0xac, 0xad}; + const unsigned char starts_dup_two_checksig[3] = {0x76, 0xac, 0xac}; + const unsigned char truncated_push[2] = {0x4c, 0x10}; + + datum_test(datum_script_legacy_sigop_cost(NULL, 25) == 0); + datum_test(datum_script_legacy_sigop_cost(datum_test_p2pkh_script, 0) == 0); + datum_test(datum_script_legacy_sigop_cost(datum_test_p2pkh_script, sizeof(datum_test_p2pkh_script)) == 4); + datum_test(datum_script_legacy_sigop_cost(datum_test_p2wpkh_script, sizeof(datum_test_p2wpkh_script)) == 0); + datum_test(datum_script_legacy_sigop_cost(p2sh, sizeof(p2sh)) == 0); + datum_test(datum_script_legacy_sigop_cost(p2tr, sizeof(p2tr)) == 0); + datum_test(datum_script_legacy_sigop_cost(datum_test_p2pk_script, sizeof(datum_test_p2pk_script)) == 4); + datum_test(datum_script_legacy_sigop_cost(checksig_in_push, sizeof(checksig_in_push)) == 0); + datum_test(datum_script_legacy_sigop_cost(bare_multisig, sizeof(bare_multisig)) == 80); + datum_test(datum_script_legacy_sigop_cost(two_checksig, sizeof(two_checksig)) == 8); + datum_test(datum_script_legacy_sigop_cost(starts_dup_two_checksig, sizeof(starts_dup_two_checksig)) == 8); + datum_test(datum_script_legacy_sigop_cost(truncated_push, sizeof(truncated_push)) == 0); +} + +static void datum_coinbaser_bare_p2pk_sigops_tests(void) { + T_DATUM_TEMPLATE_DATA tdata; + T_DATUM_STRATUM_JOB *job = calloc(1, sizeof(*job)); + unsigned char response[1 + 8 + 1 + sizeof(datum_test_p2pk_script)]; + + datum_test(job != NULL); + if (!job) return; + + job->coinbase_value = 5000000000ULL; + response[0] = 1; + response[1] = 1; + memset(response + 2, 0, 7); + response[9] = (unsigned char)sizeof(datum_test_p2pk_script); + memcpy(response + 10, datum_test_p2pk_script, sizeof(datum_test_p2pk_script)); + datum_test(datum_coinbaser_v2_parse(job, response, sizeof(response), false) == 1); + datum_test(job->available_coinbase_outputs[0].sigops == 4); + + memset(&tdata, 0, sizeof(tdata)); + tdata.sigoplimit = 80000; + job->block_template = &tdata; + job->available_coinbase_outputs_count = 1; + memcpy(job->available_coinbase_outputs[0].output_script, datum_test_p2pk_script, sizeof(datum_test_p2pk_script)); + job->available_coinbase_outputs[0].output_script_len = sizeof(datum_test_p2pk_script); + job->available_coinbase_outputs[0].value_sats = 100000000; + /* Budget of 0: the bare P2PK must be skipped (old first-byte guess packed it). */ + datum_test(!strncmp(datum_coinbase_output_count_hex(job, 80000, false), "02", 2)); + /* Budget of 4: the bare P2PK fits. */ + datum_test(!strncmp(datum_coinbase_output_count_hex(job, 80000 - 4, false), "03", 2)); + free(job); } void datum_coinbaser_tests(void) { datum_prime_id_64bit_tests(); datum_blake2b_coinbase_limit_tests(); + datum_blake2b_coinbase_sigops_tests(); + datum_script_legacy_sigop_cost_tests(); + datum_coinbaser_bare_p2pk_sigops_tests(); + datum_coinbaser_value_overflow_tests(); } diff --git a/src/datum_gateway.c b/src/datum_gateway.c index 88e786c5..70f6af39 100644 --- a/src/datum_gateway.c +++ b/src/datum_gateway.c @@ -90,6 +90,7 @@ void datum_pow_tests(void); void datum_protocol_tests(void); void datum_stratum_dupes_tests(void); void datum_utils_tests(void); +void datum_logger_tests(void); void datum_submitblock_tests(void); static error_t parse_opt(int key, char *arg, struct argp_state *state) { @@ -111,6 +112,7 @@ static error_t parse_opt(int key, char *arg, struct argp_state *state) { break; case 0x101: // test datum_utils_tests(); + datum_logger_tests(); datum_conf_tests(); datum_blocktemplates_tests(); datum_coinbaser_tests(); @@ -195,8 +197,12 @@ int main(const int argc, const char * const * const argv) { } datum_gateway_config_filename = arguments.config_file; - // Initialize logger thread - datum_logger_init(); + // Initialize logger thread (CONVOY #6: fail if setup fails) + if (datum_logger_init()) { + DLOG_FATAL("Error initializing the logger!"); + usleep(100000); + exit(1); + } if (datum_protocol_init()) { DLOG_FATAL("Error initializing the DATUM protocol!"); diff --git a/src/datum_jsonrpc.c b/src/datum_jsonrpc.c index 87ce13cc..8c77a8ae 100644 --- a/src/datum_jsonrpc.c +++ b/src/datum_jsonrpc.c @@ -142,6 +142,7 @@ char *basic_http_call(CURL *curl, const char *url) { json_t *json_rpc_call_full(CURL *curl, const char *url, const char *userpass, const char *rpc_req, const char *extra_header, long * const http_resp_code_out) { json_t *val, *err_val, *res_val; CURLcode rc; + long http_resp_code = 0; struct data_buffer all_data = { }; struct upload_buffer upload_data; json_error_t err = { }; @@ -151,7 +152,9 @@ json_t *json_rpc_call_full(CURL *curl, const char *url, const char *userpass, co curl_easy_setopt(curl, CURLOPT_URL, url); curl_easy_setopt(curl, CURLOPT_ENCODING, ""); - curl_easy_setopt(curl, CURLOPT_FAILONERROR, 1L); + // No CURLOPT_FAILONERROR: bitcoind answers an RPC error with HTTP 500 and + // the reason in the body, which FAILONERROR would discard unread. (CONVOY #4) + curl_easy_setopt(curl, CURLOPT_FAILONERROR, 0L); curl_easy_setopt(curl, CURLOPT_TCP_NODELAY, 1L); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, all_data_cb); curl_easy_setopt(curl, CURLOPT_WRITEDATA, &all_data); @@ -184,8 +187,9 @@ json_t *json_rpc_call_full(CURL *curl, const char *url, const char *userpass, co curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); rc = curl_easy_perform(curl); + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_resp_code); + if (http_resp_code_out) *http_resp_code_out = http_resp_code; if (rc) { - if (http_resp_code_out) curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, http_resp_code_out); DLOG_DEBUG("json_rpc_call: HTTP request failed: %s", curl_err_str); DLOG_DEBUG("json_rpc_call: Request was: %s",rpc_req); goto err_out; @@ -193,7 +197,13 @@ json_t *json_rpc_call_full(CURL *curl, const char *url, const char *userpass, co val = JSON_LOADS(all_data.buf, &err); if (!val) { - DLOG_DEBUG("JSON decode failed(%d): %s", err.line, err.text); + if (http_resp_code == 401) { + DLOG_DEBUG("json_rpc_call: HTTP 401 from %s (credentials refused)", url); + } else if (http_resp_code >= 400) { + DLOG_ERROR("json_rpc_call: HTTP %ld from %s", http_resp_code, url); + } else { + DLOG_DEBUG("JSON decode failed(%d): %s", err.line, err.text); + } goto err_out; } @@ -226,6 +236,16 @@ json_t *json_rpc_call_full(CURL *curl, const char *url, const char *userpass, co goto err_out; } } + + // Knots puts RPC errors in the JSON body and often answers with HTTP 500. + // If the body already parsed and passed the result/error gate, keep it. + if (http_resp_code >= 400) { + if (http_resp_code == 401) { + DLOG_DEBUG("json_rpc_call: HTTP 401 from %s (usable JSON body kept)", url); + } else { + DLOG_DEBUG("json_rpc_call: HTTP %ld from %s (usable JSON body kept)", http_resp_code, url); + } + } databuf_free(&all_data); curl_slist_free_all(headers); diff --git a/src/datum_logger.c b/src/datum_logger.c index 8f3af90e..bf13c26b 100644 --- a/src/datum_logger.c +++ b/src/datum_logger.c @@ -51,13 +51,16 @@ #include #include #include +#include +#include #include "datum_logger.h" #include "datum_utils.h" const char *level_text[] = { " ALL", "DEBUG", " INFO", " WARN", "ERROR", "FATAL" }; -volatile bool datum_logger_initialized = false; +static atomic_bool datum_logger_initialized = false; +static FILE *log_handle = NULL; volatile bool log_reopen_signal = false; // configurable options @@ -260,38 +263,9 @@ void * datum_logger_thread(void *ptr) { DLOG_MSG *msg; char time_buffer[20]; char log_line[1200]; - FILE *log_handle = NULL; time_t next_log_rotate = get_midnight_timestamp(); time_t log_file_opened = time(NULL); - msg_buffer[0] = calloc((DLOG_MSG_BUF_SIZE * 2) + (1024*8),1); - if (!msg_buffer[0]) { - DLOG(DLOG_LEVEL_FATAL, "Could not allocate memory for logger queue!"); - panic_from_thread(__LINE__); - } - // split the allocation in half for the double buffering - msg_buffer[1] = &msg_buffer[0][DLOG_MSG_BUF_SIZE + (1024*4)]; - - dlog_queue_max_entries = (DLOG_MSG_BUF_SIZE / sizeof(DLOG_MSG)) - 1; - if (dlog_queue_max_entries < 1024) dlog_queue_max_entries = 1024; - dlog_queue[0] = calloc(dlog_queue_max_entries * 2 * sizeof(DLOG_MSG),1); - if (!dlog_queue[0]) { - DLOG(DLOG_LEVEL_FATAL, "Could not allocate memory for logger queue list!"); - panic_from_thread(__LINE__); - } - dlog_queue[1] = &dlog_queue[0][dlog_queue_max_entries]; - - if ((log_to_file) && (log_file[0] != 0)) { - log_handle = fopen(log_file,"a"); - if (!log_handle) { - DLOG(DLOG_LEVEL_FATAL, "Could not open log file (%s): %s!", log_file, strerror(errno)); - panic_from_thread(__LINE__); - } - } - - // alert the masses. - datum_logger_initialized = true; - DLOG(DLOG_LEVEL_DEBUG, "Logging thread started! (Approximately %d MB of RAM allocated for up to %d entries per cycle)", (DLOG_MSG_BUF_SIZE * 4)/1024/1024, dlog_queue_max_entries); lflush = 0; @@ -455,15 +429,143 @@ void datum_logger_hup_signal(int signum) { log_reopen_signal = true; } -int datum_logger_init(void) { - const struct sigaction hup_sigaction = { .sa_handler = datum_logger_hup_signal, }; - if (0 != sigaction(SIGHUP, &hup_sigaction, NULL)) { - DLOG_ERROR("Failed to setup SIGHUP handler: %s", strerror(errno)); +static void datum_logger_init_cleanup(void) { + if (log_handle) fclose(log_handle); + log_handle = NULL; + free(dlog_queue[0]); + dlog_queue[0] = dlog_queue[1] = NULL; + free(msg_buffer[0]); + msg_buffer[0] = msg_buffer[1] = NULL; + dlog_queue_max_entries = 0; +} + +static int datum_logger_init_with_thread_create( + int (*thread_create)(pthread_t *, const pthread_attr_t *, void *(*)(void *), void *)) { + pthread_t pthread_datum_logger_thread; + + // Set the queue and the log file up here, before the writer thread + // exists, so a message logged right after init has somewhere to go. (CONVOY #6) + msg_buffer[0] = calloc((DLOG_MSG_BUF_SIZE * 2) + (1024*8),1); + if (!msg_buffer[0]) { + DLOG(DLOG_LEVEL_FATAL, "Could not allocate memory for logger queue!"); + return -1; } + msg_buffer[1] = &msg_buffer[0][DLOG_MSG_BUF_SIZE + (1024*4)]; - pthread_t pthread_datum_logger_thread; + dlog_queue_max_entries = (DLOG_MSG_BUF_SIZE / sizeof(DLOG_MSG)) - 1; + if (dlog_queue_max_entries < 1024) dlog_queue_max_entries = 1024; + dlog_queue[0] = calloc(dlog_queue_max_entries * 2 * sizeof(DLOG_MSG),1); + if (!dlog_queue[0]) { + DLOG(DLOG_LEVEL_FATAL, "Could not allocate memory for logger queue list!"); + goto fail; + } + dlog_queue[1] = &dlog_queue[0][dlog_queue_max_entries]; - pthread_create(&pthread_datum_logger_thread, NULL, datum_logger_thread, NULL); + if ((log_to_file) && (log_file[0] != 0)) { + log_handle = fopen(log_file,"a"); + if (!log_handle) { + DLOG(DLOG_LEVEL_FATAL, "Could not open log file (%s): %s!", log_file, strerror(errno)); + goto fail; + } + } + const int err = thread_create(&pthread_datum_logger_thread, NULL, datum_logger_thread, NULL); + if (err) { + DLOG_FATAL("Could not start logging thread: %s!", strerror(err)); + goto fail; + } + // Publish only once a writer exists. Until then, callers log synchronously + // and cannot access queues that a failed startup needs to free. + datum_logger_initialized = true; return 0; + +fail: + datum_logger_init_cleanup(); + return -1; +} + +int datum_logger_init(void) { + const struct sigaction hup_sigaction = { .sa_handler = datum_logger_hup_signal, }; + if (0 != sigaction(SIGHUP, &hup_sigaction, NULL)) { + DLOG_ERROR("Failed to setup SIGHUP handler: %s", strerror(errno)); + } + return datum_logger_init_with_thread_create(pthread_create); +} + +static int logger_test_create_result; +static int logger_test_file_fd; + +static int logger_test_thread_create(pthread_t *thread, const pthread_attr_t *attr, + void *(*start)(void *), void *arg) { + (void)thread; + (void)attr; + (void)arg; + datum_test(start == datum_logger_thread); + datum_test(!datum_logger_initialized); + datum_test(msg_buffer[0] && msg_buffer[1] && dlog_queue[0] && dlog_queue[1]); + datum_test(log_handle != NULL); + logger_test_file_fd = log_handle ? fileno(log_handle) : -1; + return logger_test_create_result; +} + +void datum_logger_tests(void) { + // Run before the production logger starts; injection never launches a writer. + datum_test(!datum_logger_initialized); + if (datum_logger_initialized) return; + char test_path[] = "/tmp/datum-logger-test-XXXXXX"; + const int test_fd = mkstemp(test_path); + FILE *capture = tmpfile(); + const int saved_stdout = dup(STDOUT_FILENO); + datum_test(test_fd >= 0 && capture && saved_stdout >= 0); + if (test_fd < 0 || !capture || saved_stdout < 0) { + if (test_fd >= 0) { close(test_fd); unlink(test_path); } + if (capture) fclose(capture); + if (saved_stdout >= 0) close(saved_stdout); + return; + } + close(test_fd); + const bool saved_file = log_to_file, saved_console = log_to_console, saved_stderr = log_to_stderr; + const int saved_level = log_level_console; + char saved_path[sizeof(log_file)]; + memcpy(saved_path, log_file, sizeof(saved_path)); + log_to_file = log_to_console = true; + log_to_stderr = false; + log_level_console = DLOG_LEVEL_ALL; + strcpy(log_file, test_path); + fflush(stdout); + datum_test(dup2(fileno(capture), STDOUT_FILENO) >= 0); + logger_test_create_result = EAGAIN; + datum_test(datum_logger_init_with_thread_create(logger_test_thread_create) == -1); + datum_test(!datum_logger_initialized && !log_handle); + datum_test(!msg_buffer[0] && !msg_buffer[1] && !dlog_queue[0] && !dlog_queue[1]); + datum_test(dlog_queue_max_entries == 0); + datum_test(fcntl(logger_test_file_fd, F_GETFD) == -1 && errno == EBADF); + DLOG_INFO("logger failure fallback test"); + datum_test(dlog_queue_next[0] == 0 && dlog_queue_next[1] == 0); + fflush(stdout); + rewind(capture); + char output[1024] = {0}; + fread(output, 1, sizeof(output) - 1, capture); + datum_test(strstr(output, "Could not start logging thread:") != NULL); + datum_test(strstr(output, "logger failure fallback test") != NULL); + datum_test(dup2(saved_stdout, STDOUT_FILENO) >= 0); + close(saved_stdout); + fclose(capture); + + // Successful init has usable queues immediately, before the writer runs. + logger_test_create_result = 0; + datum_test(datum_logger_init_with_thread_create(logger_test_thread_create) == 0); + datum_test(datum_logger_initialized); + DLOG_INFO("logger immediate queue test"); + datum_test(dlog_queue_next[0] == 1); + datum_test(!strcmp(dlog_queue[0][0].msg, "logger immediate queue test")); + datum_logger_initialized = false; + dlog_queue_next[0] = msg_buf_idx[0] = 0; + datum_logger_init_cleanup(); + log_to_file = saved_file; + log_to_console = saved_console; + log_to_stderr = saved_stderr; + log_level_console = saved_level; + memcpy(log_file, saved_path, sizeof(log_file)); + unlink(test_path); } diff --git a/src/datum_pow.c b/src/datum_pow.c index 8ce50f0a..594c1f74 100644 --- a/src/datum_pow.c +++ b/src/datum_pow.c @@ -40,24 +40,6 @@ #include "datum_pow.h" #include "datum_utils.h" -static int datum_hex_nibble(const char c) { - if (c >= '0' && c <= '9') return c - '0'; - if (c >= 'a' && c <= 'f') return c - 'a' + 10; - if (c >= 'A' && c <= 'F') return c - 'A' + 10; - return -1; -} - -bool datum_pow_decode_hex_exact(const char *hex, size_t out_len, unsigned char *out) { - size_t i; - if (!hex || !out) return false; - for (i = 0; i < out_len; i++) { - if (datum_hex_nibble(hex[i<<1]) < 0) return false; - if (datum_hex_nibble(hex[(i<<1)+1]) < 0) return false; - out[i] = hex2bin_uchar(&hex[i<<1]); - } - return hex[out_len<<1] == 0; -} - bool datum_blake2b_time_on_wire(uint32_t *out, uint64_t ntime, uint64_t offset, uint8_t flags) { if (!out) return false; if (ntime > UINT32_MAX) return false; @@ -224,13 +206,23 @@ void datum_blake2b_prevblock_hidden(unsigned char *out, const unsigned char *pre memset(out, 0, 6); } -void datum_blake2b_build_work_header(unsigned char *work, const unsigned char *prevhash, const unsigned char *nonce, const unsigned char *ntime, const unsigned char *root) { - datum_blake2b_prevblock_hidden(work, prevhash); +void datum_blake2b_build_work_header_from_hidden( + unsigned char *work, const unsigned char *prevhash_hidden, + const unsigned char *nonce, const unsigned char *ntime, + const unsigned char *root +) { + memcpy(work, prevhash_hidden, 32); memcpy(work + 32, nonce, 8); memcpy(work + 40, ntime, 8); memcpy(work + 48, root, 32); } +void datum_blake2b_build_work_header(unsigned char *work, const unsigned char *prevhash, const unsigned char *nonce, const unsigned char *ntime, const unsigned char *root) { + unsigned char prevhash_hidden[32]; + datum_blake2b_prevblock_hidden(prevhash_hidden, prevhash); + datum_blake2b_build_work_header_from_hidden(work, prevhash_hidden, nonce, ntime, root); +} + bool datum_blake2b_header_commitment_from_key_hash( unsigned char *commitment, uint32_t version, diff --git a/src/datum_pow.h b/src/datum_pow.h index 05db0e27..d6cfa5f2 100644 --- a/src/datum_pow.h +++ b/src/datum_pow.h @@ -50,8 +50,6 @@ #define DATUM_POW_RESERVED_BLAKE2B_USE_TIME_OFFSET 0x01 #define DATUM_POW_FLAG_BLAKE2B 0x08 -bool datum_pow_decode_hex_exact(const char *hex, size_t out_len, unsigned char *out); - bool datum_blake2b_time_on_wire(uint32_t *out, uint64_t ntime, uint64_t offset, uint8_t flags); /* The nTime a node reads from a header-v2 block built from this share: the * job's wire time plus the hasher's four time-offset bytes when the offset @@ -66,6 +64,11 @@ long double datum_blake2b_accounting_difficulty(long double x); bool datum_blake2b_256(unsigned char *out, const unsigned char *in, size_t len); void datum_blake2b_coinb1(unsigned char *out, const unsigned char *commitment); void datum_blake2b_prevblock_hidden(unsigned char *out, const unsigned char *prevhash); +void datum_blake2b_build_work_header_from_hidden( + unsigned char *work, const unsigned char *prevhash_hidden, + const unsigned char *nonce, const unsigned char *ntime, + const unsigned char *root +); void datum_blake2b_build_work_header(unsigned char *work, const unsigned char *prevhash, const unsigned char *nonce, const unsigned char *ntime, const unsigned char *root); /* version may be with or without 0x80000000; H1 always includes the v2 bit. */ diff --git a/src/datum_pow_tests.c b/src/datum_pow_tests.c index e9aa804e..48d8eeb1 100644 --- a/src/datum_pow_tests.c +++ b/src/datum_pow_tests.c @@ -126,7 +126,7 @@ static void datum_pow_blake2b_vector_tests(void) { "101112131415161718191a1b1c1d1e1f39300000" "808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f"; unsigned char merkle[32], xor_key[16], rhs[32], extranonce[12], prevhash[32]; - unsigned char nonce[8], ntime[8], commitment[32], root[32], work[80], hash_le[32]; + unsigned char nonce[8], ntime[8], commitment[32], root[32], work[80], cached_work[80], hash_le[32]; unsigned char share_target[32]; unsigned char coinb1[39], arbitrary_tx[51], leaf_preimage[52] = {0}; unsigned char header[DATUM_BLAKE2B_BLOCK_HEADER_SIZE], expected[DATUM_BLAKE2B_BLOCK_HEADER_SIZE]; @@ -187,10 +187,10 @@ static void datum_pow_blake2b_vector_tests(void) { datum_test(datum_blake2b_header_commitment(commitment, 0x20000000, prevhash, 12345, merkle, 0x6553412f, 0x207fffff, 3, DATUM_BLAKE2B_USE_TIME_OFFSET, 13, xor_key, rhs)); - datum_test(datum_pow_decode_hex_exact(expected_commitment_hex, 32, expected)); + datum_test(hex_to_bin_exact(expected_commitment_hex, expected, 32)); datum_test(!memcmp(commitment, expected, 32)); datum_test(datum_blake2b_work_root(root, commitment, extranonce)); - datum_test(datum_pow_decode_hex_exact(expected_root_hex, 32, expected)); + datum_test(hex_to_bin_exact(expected_root_hex, expected, 32)); datum_test(!memcmp(root, expected, 32)); datum_blake2b_coinb1(coinb1, commitment); memcpy(arbitrary_tx, coinb1, sizeof(coinb1)); @@ -199,19 +199,20 @@ static void datum_pow_blake2b_vector_tests(void) { datum_test(datum_blake2b_256(expected, leaf_preimage, sizeof(leaf_preimage))); datum_test(!memcmp(root, expected, 32)); datum_blake2b_build_work_header(work, prevhash, nonce, ntime, root); - datum_test(datum_pow_decode_hex_exact(expected_work_hex, sizeof(work), expected)); + datum_test(hex_to_bin_exact(expected_work_hex, expected, sizeof(work))); datum_test(!memcmp(work, expected, sizeof(work))); datum_blake2b_prevblock_hidden(expected, prevhash); datum_test(!memcmp(expected, work, 32)); + datum_blake2b_build_work_header_from_hidden(cached_work, expected, nonce, ntime, root); + datum_test(!memcmp(cached_work, work, sizeof(work))); datum_test(datum_blake2b_pow_hash_le(hash_le, work, xor_key, 13)); - datum_test(datum_pow_decode_hex_exact(expected_hash_le_hex, 32, expected)); + datum_test(hex_to_bin_exact(expected_hash_le_hex, expected, 32)); datum_test(!memcmp(hash_le, expected, 32)); datum_blake2b_serialize_block_header(header, 0x20000000, prevhash, merkle, 0x6553412f, 0x207fffff, nonce, ntime, extranonce, 3, DATUM_BLAKE2B_USE_TIME_OFFSET, 13, xor_key, 12345, rhs); - datum_test(datum_pow_decode_hex_exact(expected_header_hex, sizeof(header), expected)); + datum_test(hex_to_bin_exact(expected_header_hex, expected, sizeof(header))); datum_test(!memcmp(header, expected, sizeof(header))); - datum_test(!datum_pow_decode_hex_exact("xyz", 1, nonce)); /* Canonical profile-0 vector published with Knots' header-v2 implementation: * profile_0_time_offset from src/test/data/block_header_v2.json. Its "h2", @@ -240,9 +241,9 @@ static void datum_pow_blake2b_vector_tests(void) { unsigned char knots_prevhash[32], knots_merkle[32], knots_rhs[32]; unsigned char knots_nonce[8], knots_ntime[8]; - datum_test(datum_pow_decode_hex_exact(knots_prevhash_hex, 32, knots_prevhash)); - datum_test(datum_pow_decode_hex_exact(knots_merkle_hex, 32, knots_merkle)); - datum_test(datum_pow_decode_hex_exact(knots_rhs_hex, 32, knots_rhs)); + datum_test(hex_to_bin_exact(knots_prevhash_hex, knots_prevhash, 32)); + datum_test(hex_to_bin_exact(knots_merkle_hex, knots_merkle, 32)); + datum_test(hex_to_bin_exact(knots_rhs_hex, knots_rhs, 32)); memset(xor_key, 0, sizeof(xor_key)); pk_u32le(knots_nonce, 0, UINT32_C(0x0badf00d)); pk_u32le(knots_nonce, 4, UINT32_C(0x11223344)); @@ -252,16 +253,16 @@ static void datum_pow_blake2b_vector_tests(void) { datum_test(datum_blake2b_header_commitment(commitment, 0x20000000, knots_prevhash, 840000, knots_merkle, UINT32_C(2000000000) - 600, 0x1d00ffff, 3, 0x1c, 0, xor_key, knots_rhs)); - datum_test(datum_pow_decode_hex_exact(knots_commitment_hex, 32, expected)); + datum_test(hex_to_bin_exact(knots_commitment_hex, expected, 32)); datum_test(!memcmp(commitment, expected, 32)); - datum_test(datum_pow_decode_hex_exact(knots_root_hex, 32, root)); + datum_test(hex_to_bin_exact(knots_root_hex, root, 32)); datum_blake2b_build_work_header(work, knots_prevhash, knots_nonce, knots_ntime, root); - datum_test(datum_pow_decode_hex_exact(knots_work_hex, sizeof(work), expected)); + datum_test(hex_to_bin_exact(knots_work_hex, expected, sizeof(work))); datum_test(!memcmp(work, expected, sizeof(work))); datum_test(datum_blake2b_pow_hash_le(hash_le, work, xor_key, 0)); - datum_test(datum_pow_decode_hex_exact(knots_hash_le_hex, 32, expected)); + datum_test(hex_to_bin_exact(knots_hash_le_hex, expected, 32)); datum_test(!memcmp(hash_le, expected, 32)); } } diff --git a/src/datum_protocol.c b/src/datum_protocol.c index ff069ba1..17e8d06d 100644 --- a/src/datum_protocol.c +++ b/src/datum_protocol.c @@ -174,7 +174,7 @@ static atomic_bool datum_pool_abw_enabled = true; extern DATUM_QUEUE pow_queue; // may be used by this thread when crafting replies to server commands -unsigned char temp_data[DATUM_PROTOCOL_MAX_CMD_DATA_SIZE + 16384]; +unsigned char temp_data[DATUM_PROTOCOL_TEMP_DATA_SIZE]; unsigned char datum_protocol_setup_new_job_idx(void *sx) { // Called by the stratum job updater. Must be thread safe. @@ -202,10 +202,6 @@ unsigned char datum_protocol_setup_new_job_idx(void *sx) { return a; } -static inline void datum_xor_header_key(void *h, uint32_t key) { - *((uint32_t *)h) ^= key; -} - uint32_t datum_header_xor_feedback(const uint32_t i) { uint32_t s = 0xb10cfeed; uint32_t h = s; @@ -225,6 +221,30 @@ uint32_t datum_header_xor_feedback(const uint32_t i) { return h; } +void datum_header_pk(uint8_t * const dst, const size_t offset, const T_DATUM_PROTOCOL_HEADER * const h, uint32_t * const xor_key) { + uint32_t raw = (h->cmd_len & 0x3fffffUL) | + ((uint32_t)h->is_signed << 24) | + ((uint32_t)h->is_encrypted_pubkey << 25) | + ((uint32_t)h->is_encrypted_channel << 26) | + ((uint32_t)(h->proto_cmd & 0x1f) << 27); + raw ^= *xor_key; + *xor_key = datum_header_xor_feedback(*xor_key); + + pk_u32le(dst, offset, raw); +} + +void datum_header_upk(T_DATUM_PROTOCOL_HEADER * const h, const uint8_t * const src, const size_t offset, uint32_t * const xor_key) { + uint32_t raw = upk_u32le(src, offset); + raw ^= *xor_key; + *xor_key = datum_header_xor_feedback(*xor_key); + + h->cmd_len = raw & 0x003fffffUL; + h->is_signed = raw & 0x01000000UL; + h->is_encrypted_pubkey = raw & 0x02000000UL; + h->is_encrypted_channel = raw & 0x04000000UL; + h->proto_cmd = (raw >> 27) & 0x1f; +} + // Take the hexidecimal public key string and store it in a DATUM_ENC_KEYS int datum_pubkey_to_struct(const char *input, DATUM_ENC_KEYS *key) { int i; @@ -312,7 +332,7 @@ static int datum_protocol_encrypted_cmd(uint8_t proto_cmd, const void *data, h.proto_cmd = proto_cmd; h.cmd_len = len; h.cmd_len += crypto_box_MACBYTES; - const size_t frame_size = sizeof(T_DATUM_PROTOCOL_HEADER) + + const size_t frame_size = T_DATUM_PROTOCOL_HEADER_WIRE_BYTES + (size_t)len + crypto_box_MACBYTES; if (frame_size >= DATUM_PROTOCOL_BUFFER_SIZE) return -1; @@ -331,16 +351,13 @@ static int datum_protocol_encrypted_cmd(uint8_t proto_cmd, const void *data, } unsigned char *encrypted = server_send_buffer + server_out_buf + - sizeof(T_DATUM_PROTOCOL_HEADER); + T_DATUM_PROTOCOL_HEADER_WIRE_BYTES; crypto_box_easy_afternm(encrypted, data, len, session_nonce_sender, session_precomp.precomp_remote); //DLOG_DEBUG("mining cmd 5--- len %d, send header key %8.8x, raw %8.8lx", h.cmd_len, sending_header_key, (unsigned long)upk_u32le(h, 0)); - datum_xor_header_key(&h, sending_header_key); - sending_header_key = datum_header_xor_feedback(sending_header_key); + datum_header_pk(server_send_buffer, server_out_buf, &h, &sending_header_key); datum_increment_session_nonce(session_nonce_sender); - memcpy(server_send_buffer + server_out_buf, &h, - sizeof(T_DATUM_PROTOCOL_HEADER)); - server_out_buf += sizeof(T_DATUM_PROTOCOL_HEADER); + server_out_buf += T_DATUM_PROTOCOL_HEADER_WIRE_BYTES; server_out_buf += len + crypto_box_MACBYTES; pthread_mutex_unlock(&datum_protocol_send_buffer_lock); pthread_mutex_unlock(&datum_protocol_sender_stage1_lock); @@ -637,6 +654,8 @@ typedef struct { T_DATUM_ABW_TEMPLATE *block_template; bool subsidy_only; bool pool_handled; + char finder[DATUM_ABW_FINDER_LEN]; + uint64_t height; } T_DATUM_ABW_PENDING; static pthread_mutex_t datum_abw_mutex = PTHREAD_MUTEX_INITIALIZER; @@ -783,11 +802,13 @@ static bool datum_protocol_abw_template_matches_source( } // Caller holds datum_abw_mutex and transfers ownership of coinbase. +// finder describes the client for the log and is copied. static void datum_protocol_abw_populate_pending( T_DATUM_ABW_PENDING *pending, T_DATUM_ABW_TEMPLATE *block_template, const T_DATUM_PROTOCOL_POW *pow, unsigned char *coinbase, size_t coinbase_size, const unsigned char raw_pow_hash[32], - const unsigned char block_header[DATUM_BLAKE2B_BLOCK_HEADER_SIZE]) { + const unsigned char block_header[DATUM_BLAKE2B_BLOCK_HEADER_SIZE], + const char *finder) { pending->assignment_id = pow->abw_assignment_id; pending->nonce = (uint32_t)pow->nonce; pending->target_pot = pow->target_byte; @@ -798,6 +819,8 @@ static void datum_protocol_abw_populate_pending( DATUM_BLAKE2B_BLOCK_HEADER_SIZE); pending->coinbase = coinbase; pending->coinbase_size = coinbase_size; + snprintf(pending->finder, sizeof(pending->finder), "%s", finder ? finder : ""); + pending->height = pow->sjob->height; pending->block_template = block_template; pending->subsidy_only = pow->subsidy_only; if (block_template) block_template->refs++; @@ -805,7 +828,7 @@ static void datum_protocol_abw_populate_pending( bool datum_protocol_abw_cache_candidate(const T_DATUM_PROTOCOL_POW *pow, const unsigned char *full_cb_tx, size_t full_cb_tx_size, - const unsigned char *raw_pow_hash) { + const unsigned char *raw_pow_hash, const char *finder) { static const unsigned char no_xor_key[16] = {0}; if (!pow || !pow->sjob || !pow->sjob->block_template || !full_cb_tx || !raw_pow_hash || !pow->abw_assignment_id || @@ -868,13 +891,13 @@ bool datum_protocol_abw_cache_candidate(const T_DATUM_PROTOCOL_POW *pow, } if (pending && block_template) { datum_protocol_abw_populate_pending(pending, block_template, pow, - coinbase, full_cb_tx_size, raw_pow_hash, block_header); + coinbase, full_cb_tx_size, raw_pow_hash, block_header, finder); pthread_mutex_unlock(&datum_abw_mutex); return true; } if (pending && pow->subsidy_only) { datum_protocol_abw_populate_pending(pending, NULL, pow, - coinbase, full_cb_tx_size, raw_pow_hash, block_header); + coinbase, full_cb_tx_size, raw_pow_hash, block_header, finder); pthread_mutex_unlock(&datum_abw_mutex); return true; } @@ -967,7 +990,7 @@ bool datum_protocol_abw_cache_candidate(const T_DATUM_PROTOCOL_POW *pow, } free(transactions_hex); datum_protocol_abw_populate_pending(pending, block_template, pow, - coinbase, full_cb_tx_size, raw_pow_hash, block_header); + coinbase, full_cb_tx_size, raw_pow_hash, block_header, finder); pthread_mutex_unlock(&datum_abw_mutex); return true; } @@ -1083,7 +1106,7 @@ int datum_protocol_abw_assignment_notice(int len, unsigned char *data) { static char *datum_protocol_abw_take_revealed_candidate_locked( uint8_t assignment_id, const unsigned char xor_key[16], const unsigned char expected_pow_hash[32], char block_hash[65], - bool *pool_handled) { + bool *pool_handled, char *finder, size_t finder_size, uint64_t *height) { for (size_t i = 0; i < DATUM_ABW_PENDING_CACHE; ++i) { T_DATUM_ABW_PENDING *pending = &datum_abw_pending[i]; if (pending->assignment_id != assignment_id) continue; @@ -1133,6 +1156,10 @@ static char *datum_protocol_abw_take_revealed_candidate_locked( pending->raw_pow_hash, pending->xor_clear_bits, xor_key, actual_pow_hash, block_hash)) { if (pool_handled) *pool_handled = pending->pool_handled; + if (finder && finder_size) { + snprintf(finder, finder_size, "%s", pending->finder); + } + if (height) *height = pending->height; datum_protocol_abw_pending_clear(pending); return candidate; } @@ -1185,11 +1212,18 @@ int datum_protocol_abw_reveal(int len, unsigned char *data) { while (true) { char block_hash[65] = {0}; bool pool_handled = false; + char finder[DATUM_ABW_FINDER_LEN] = ""; + uint64_t height = 0; pthread_mutex_lock(&datum_abw_mutex); char *block_request = datum_protocol_abw_take_revealed_candidate_locked( - assignment_id, data + 2, NULL, block_hash, &pool_handled); + assignment_id, data + 2, NULL, block_hash, &pool_handled, finder, + sizeof(finder), &height); pthread_mutex_unlock(&datum_abw_mutex); if (!block_request) break; + if (finder[0]) { + DLOG_WARN("Block %s at height %llu found by %s", block_hash, + (unsigned long long)height, finder); + } if (datum_config.mining_abw_verify_all_shares_on_disclosure && !pool_handled) { ignored_block = true; @@ -1223,6 +1257,18 @@ 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 }; int datum_coinbaser_v2_response_len[2] = { 0, 0 }; +unsigned char datum_coinbaser_v2_response_prevhash[2][32]; +bool datum_coinbaser_v2_response_has_prevhash[2] = { false, false }; + +bool datum_protocol_coinbaser_reply_is_for_job(uint64_t value, const unsigned char prevhash[32]) { + const unsigned char idx = datum_coinbaser_v2_response_buf_idx; + + if (!datum_coinbaser_v2_response) return false; + if (datum_coinbaser_v2_response_value[idx] != value) return false; + if (!datum_coinbaser_v2_response_has_prevhash[idx]) return true; + if (!prevhash) return false; + return memcmp(datum_coinbaser_v2_response_prevhash[idx], prevhash, 32) == 0; +} static int datum_mutex_timedlock(pthread_mutex_t *mutex, const struct timespec *timeout) { #ifndef HAVE_PTHREAD_MUTEX_TIMEDLOCK @@ -1284,6 +1330,20 @@ int datum_protocol_coinbaser_fetch_response(int len, unsigned char *data) { memcpy(datum_coinbaser_v2_response, &data[12], x); datum_coinbaser_v2_response_value[datum_coinbaser_v2_response_buf_idx] = v; datum_coinbaser_v2_response_len[datum_coinbaser_v2_response_buf_idx] = x; + // Optional trailer after the blob: magic + request prevhash. Stock + // OCEAN/CONVOY replies are value + blob only. Take the trailer only + // when the tail is at least 36 bytes and the four bytes at the blob + // edge match; anything else, any tail length, is no prevhash. A + // padding server then collides once in 2^32 instead of every time. + if ((unsigned int)len >= 12u + x + DATUM_COINBASER_PREVHASH_TRAILER_LEN + && memcmp(&data[12 + x], DATUM_COINBASER_PREVHASH_MAGIC, + DATUM_COINBASER_PREVHASH_MAGIC_LEN) == 0) { + memcpy(datum_coinbaser_v2_response_prevhash[datum_coinbaser_v2_response_buf_idx], + &data[12 + x + DATUM_COINBASER_PREVHASH_MAGIC_LEN], 32); + datum_coinbaser_v2_response_has_prevhash[datum_coinbaser_v2_response_buf_idx] = true; + } else { + datum_coinbaser_v2_response_has_prevhash[datum_coinbaser_v2_response_buf_idx] = false; + } pthread_cond_signal(&datum_protocol_coinbaser_fetch_cond); // Signal the condition variable pthread_mutex_unlock(&datum_protocol_coinbaser_fetch_mutex); @@ -1327,31 +1387,43 @@ int datum_protocol_coinbaser_fetch(void *sptr) { return 0; } - if (datum_protocol_mining_cmd_for_session( - msg, i, session_generation) != 0) return 0; - - // spin here for up to 5 seconds while awaiting a coinbaser response from the DATUM server - clock_gettime(CLOCK_REALTIME, &ts); - ts.tv_sec += 5; // Set timeout to 5 seconds - + // Hold the mutex from before the request goes out until the reply is consumed, so the + // receive thread cannot store and signal a reply before this thread is waiting for it. + // The send only appends to the outgoing buffer; it never blocks on the socket. + // CONVOY #9: take the lock before send; wait for this job's value, not any wakeup. + // If the reply carries a magic prevhash trailer, require that too. pthread_mutex_lock(&datum_protocol_coinbaser_fetch_mutex); - - rc = pthread_cond_timedwait(&datum_protocol_coinbaser_fetch_cond, &datum_protocol_coinbaser_fetch_mutex, &ts); - if (rc == ETIMEDOUT) { + datum_coinbaser_v2_response = NULL; + + if (datum_protocol_mining_cmd_for_session( + msg, i, session_generation) != 0) { pthread_mutex_unlock(&datum_protocol_coinbaser_fetch_mutex); - DLOG_DEBUG("Timeout waiting for coinbaser response from DATUM server"); return 0; } - - if (rc != 0) { - DLOG_DEBUG("Error waiting for coinbaser response from DATUM server"); - pthread_mutex_unlock(&datum_protocol_coinbaser_fetch_mutex); - return 0; + + // wait here for up to 5 seconds for a coinbaser response from the DATUM server + clock_gettime(CLOCK_REALTIME, &ts); + ts.tv_sec += 5; // Set timeout to 5 seconds + + // Loop on the reply for this job: value, and prevhash when the server sent one. + while (!datum_protocol_coinbaser_reply_is_for_job(value, s->prevhash_bin)) { + rc = pthread_cond_timedwait(&datum_protocol_coinbaser_fetch_cond, &datum_protocol_coinbaser_fetch_mutex, &ts); + if (rc == ETIMEDOUT) { + pthread_mutex_unlock(&datum_protocol_coinbaser_fetch_mutex); + DLOG_DEBUG("Timeout waiting for coinbaser response from DATUM server"); + return 0; + } + + if (rc != 0) { + DLOG_DEBUG("Error waiting for coinbaser response from DATUM server"); + pthread_mutex_unlock(&datum_protocol_coinbaser_fetch_mutex); + return 0; + } } i = 0; // process received coinbase - if ((datum_coinbaser_v2_response) && (datum_coinbaser_v2_response_value[datum_coinbaser_v2_response_buf_idx] == value)) { + if (datum_protocol_coinbaser_reply_is_for_job(value, s->prevhash_bin)) { i = datum_coinbaser_v2_parse(s, datum_coinbaser_v2_response, datum_coinbaser_v2_response_len[datum_coinbaser_v2_response_buf_idx], false); } @@ -1469,7 +1541,8 @@ int datum_protocol_client_configure(int len, unsigned char *data) { return 1; } -int datum_protocol_job_validation_stxlist(unsigned char *data) { +int datum_protocol_job_validation_stxlist(int len, unsigned char *data) { + if (len < 1) return 0; // similar to compact blocks, we're going to send a list of short transaction IDs for the requested job unsigned char job_index = data[0]; T_DATUM_PROTOCOL_JOB *dj; @@ -1634,9 +1707,17 @@ int datum_protocol_job_validation_stxlist(unsigned char *data) { return 1; } -int datum_protocol_job_validation_stxlist_byid(unsigned char *data) { +// The 0x50 0x11 reply is built in temp_data and sent as one command, so it +// must stay under what datum_protocol_bulk_cmd accepts with room for the 0xFE +// terminator and up to 111 bytes of padding appended after the loop. +bool datum_protocol_stxlist_reply_fits(size_t offset, size_t txn_size) { + return offset + 3 + txn_size <= DATUM_STXLIST_REPLY_MAX; +} + +int datum_protocol_job_validation_stxlist_byid(int len, unsigned char *data) { // the server is requesting missing transactions // send them + if (len < 3) return 0; unsigned char job_index = data[0]; uint16_t req_count = upk_u16le(data, 1); @@ -1664,6 +1745,23 @@ int datum_protocol_job_validation_stxlist_byid(unsigned char *data) { return 1; } + if (3 + 2 * (int)req_count > len) { + // the index list is shorter than the count claims + // error response to 0x50 0x11 + msg[i] = 0x50; i++; + msg[i] = 0x91; i++; + msg[i] = job_index; i++; + msg[i] = 0xF4; i++; + + // pad with some randomness + j = 1 + (rand() % 100); + memset(&msg[i], rand(), j); + i+=j; + + datum_protocol_mining_cmd(msg, i); + return 1; + } + pthread_rwlock_rdlock(&datum_jobs_rwlock); dj = &datum_jobs[job_index]; @@ -1752,6 +1850,28 @@ int datum_protocol_job_validation_stxlist_byid(unsigned char *data) { return 1; } + // The count gate above does not stop a request that names the same + // index repeatedly, so bound the reply as it grows rather than trust + // the list to sum to at most one block. + if (!datum_protocol_stxlist_reply_fits((size_t)i, block_template->txns[req_id].size)) { + pthread_rwlock_unlock(&datum_jobs_rwlock); + DLOG_WARN("DATUM server requested %u transactions for job %d, more than one reply can carry; refusing", (unsigned)req_count, (int)job_index); + // error response to 0x50 0x11 + i = 0; // reset index + msg[i] = 0x50; i++; + msg[i] = 0x91; i++; + msg[i] = job_index; i++; + msg[i] = 0xF4; i++; + + // pad with some randomness + j = 1 + (rand() % 100); + memset(&msg[i], rand(), j); + i+=j; + + datum_protocol_mining_cmd(msg, i); + return 1; + } + // size is stored as 3 bytes for consistency. // this is technically redundant, as the server can derive this by decoding the transaction // however, we're future-proofing just a little here for a tiny bit of overhead. @@ -1781,7 +1901,8 @@ int datum_protocol_job_validation_stxlist_byid(unsigned char *data) { return 1; } -int datum_protocol_job_validation_sblock(unsigned char *data) { +int datum_protocol_job_validation_sblock(int len, unsigned char *data) { + if (len < 1) return 0; // the server decided our template probably is too unique from what it knows about, or was // otherwise not able to validate the block using faster negotiations. // It would like us to just send the entire transaction blob for validation as-is. @@ -1950,31 +2071,29 @@ static int datum_protocol_job_validation_parent_fetch( } int datum_protocol_job_validation_cmd(int len, unsigned char *data) { - unsigned char cmd = data[0]; - unsigned char *p = data; - if (len < 2) return 0; - p++; + const unsigned char cmd = data[0]; + unsigned char *p = data + 1; // sub sub cmd switch (cmd) { case 0x10: { // send short txn list - return datum_protocol_job_validation_stxlist(p); + return datum_protocol_job_validation_stxlist(len - 1, p); break; } case 0x11: { // send the requested txns // 16-bit indexes - return datum_protocol_job_validation_stxlist_byid(p); + return datum_protocol_job_validation_stxlist_byid(len - 1, p); break; } case 0x12: { // send the entire block, except the coinbase txn - return datum_protocol_job_validation_sblock(p); + return datum_protocol_job_validation_sblock(len - 1, p); break; } @@ -2379,16 +2498,14 @@ int datum_protocol_send_hello(int sockfd) { i+=crypto_sign_BYTES; // seal it up - crypto_box_seal(&enc_hello_msg[sizeof(T_DATUM_PROTOCOL_HEADER)], hello_msg, i, pool_keys.pk_x25519); + crypto_box_seal(&enc_hello_msg[T_DATUM_PROTOCOL_HEADER_WIRE_BYTES], hello_msg, i, pool_keys.pk_x25519); i+=crypto_box_SEALBYTES; h.cmd_len = i; - memcpy(enc_hello_msg, &h, sizeof(T_DATUM_PROTOCOL_HEADER)); - // apply our initial xor key to the header, just to obfuscate it a tiny bit // kinda pointless, but ok - datum_xor_header_key(&enc_hello_msg[0], sending_header_key); + datum_header_pk(enc_hello_msg, 0, &h, &sending_header_key); DLOG_DEBUG("Sending handshake init (%d bytes)", h.cmd_len); @@ -2411,7 +2528,7 @@ int datum_protocol_send_hello(int sockfd) { // FIXME: why is this mixed-endian? //DLOG_DEBUG("Session Nonce: %8.8X%8.8X%8.8X%8.8X%8.8X%8.8X", upk_u32le(session_nonce_receiver, 0), upk_u32le(session_nonce_receiver, 4), upk_u32le(session_nonce_receiver, 8), upk_u32le(session_nonce_receiver, 12), upk_u32le(session_nonce_receiver, 16), upk_u32le(session_nonce_receiver, 20)); - return datum_protocol_chars_to_server(enc_hello_msg, i+sizeof(T_DATUM_PROTOCOL_HEADER)); + return datum_protocol_chars_to_server(enc_hello_msg, i+T_DATUM_PROTOCOL_HEADER_WIRE_BYTES); } int datum_protocol_decrypt_sealed(T_DATUM_PROTOCOL_HEADER *h, unsigned char *data) { @@ -2668,8 +2785,12 @@ int datum_protocol_pow_submit( DLOG_ERROR("Could not submit POW for a disclosed anti-withholding assignment"); return -1; } + char finder[320] = ""; + if (pow.abw_assignment_id && c) { + datum_stratum_describe_block_finder(finder, sizeof(finder), c, username, subsidy_only); + } if (pow.abw_assignment_id && !datum_protocol_abw_cache_candidate( - &pow, full_cb_tx, full_cb_tx_size, raw_pow_hash)) { + &pow, full_cb_tx, full_cb_tx_size, raw_pow_hash, finder)) { DLOG_ERROR("BLAKE2b anti-withholding candidate cache is full"); return -1; } @@ -2938,6 +3059,7 @@ void *datum_protocol_client(void *args) { int pool_port; bool break_again = false; T_DATUM_PROTOCOL_HEADER s_header; + unsigned char s_header_wire[T_DATUM_PROTOCOL_HEADER_WIRE_BYTES]; datum_connection_configured = false; datum_protocol_abw_deactivate(); @@ -3191,7 +3313,7 @@ void *datum_protocol_client(void *args) { case 1: case 2: case 3: { - n = recv(sockfd, ((unsigned char *)&s_header) + (sizeof(T_DATUM_PROTOCOL_HEADER) - protocol_state), protocol_state, MSG_DONTWAIT); + n = recv(sockfd, s_header_wire + (T_DATUM_PROTOCOL_HEADER_WIRE_BYTES - protocol_state), protocol_state, MSG_DONTWAIT); if (n <= 0) { if ((n < 0) && ((errno == EAGAIN || errno == EWOULDBLOCK))) { continue; @@ -3200,13 +3322,13 @@ void *datum_protocol_client(void *args) { break_again = true; break; } - if ((n+(sizeof(T_DATUM_PROTOCOL_HEADER) - protocol_state)) != sizeof(T_DATUM_PROTOCOL_HEADER)) { + if ((n+(T_DATUM_PROTOCOL_HEADER_WIRE_BYTES - protocol_state)) != T_DATUM_PROTOCOL_HEADER_WIRE_BYTES) { if ((n+protocol_state) > 4) { DLOG_DEBUG("recv() issue. too many header bytes. protocol_state=%d, n=%d, errno=%d (%s)", protocol_state, n, errno, strerror(errno)); break_again = true; break; } - protocol_state = sizeof(T_DATUM_PROTOCOL_HEADER) - n - (sizeof(T_DATUM_PROTOCOL_HEADER) - protocol_state); // should give us a state equal to the number of. consoluted to show the process. (compiler optimizes) + protocol_state = T_DATUM_PROTOCOL_HEADER_WIRE_BYTES - n - (T_DATUM_PROTOCOL_HEADER_WIRE_BYTES - protocol_state); // should give us a state equal to the number of. consoluted to show the process. (compiler optimizes) continue; } @@ -3214,7 +3336,7 @@ void *datum_protocol_client(void *args) { continue; // cant fall through to 0, so loop around back to this to jump to 4 } case 0: { - n = recv(sockfd, &s_header, sizeof(T_DATUM_PROTOCOL_HEADER), MSG_DONTWAIT); + n = recv(sockfd, s_header_wire, T_DATUM_PROTOCOL_HEADER_WIRE_BYTES, MSG_DONTWAIT); if (n <= 0) { if ((n < 0) && ((errno == EAGAIN || errno == EWOULDBLOCK))) { continue; @@ -3222,12 +3344,12 @@ void *datum_protocol_client(void *args) { DLOG_DEBUG("recv() issue. protocol_state=%d, n=%d, errno=%d (%s)", protocol_state, n, errno, strerror(errno)); break_again = true; break; } - if (n != sizeof(T_DATUM_PROTOCOL_HEADER)) { + if (n != T_DATUM_PROTOCOL_HEADER_WIRE_BYTES) { if (n > 4) { DLOG_DEBUG("recv() issue. too many header bytes (B). protocol_state=%d, n=%d, errno=%d (%s)", protocol_state, n, errno, strerror(errno)); break_again = true; break; } - protocol_state = sizeof(T_DATUM_PROTOCOL_HEADER)-n; + protocol_state = T_DATUM_PROTOCOL_HEADER_WIRE_BYTES-n; continue; } @@ -3237,9 +3359,7 @@ void *datum_protocol_client(void *args) { } case 4: { - datum_xor_header_key(&s_header, receiving_header_key); - //DLOG_DEBUG("Server CMD: cmd=%u, len=%u, raw = %8.8x ... rkey = %8.8x", s_header.proto_cmd, s_header.cmd_len, upk_u32le(s_header, 0), receiving_header_key); - receiving_header_key = datum_header_xor_feedback(receiving_header_key); + datum_header_upk(&s_header, s_header_wire, 0, &receiving_header_key); protocol_state = 5; server_in_buf = 0; if (!s_header.cmd_len) { diff --git a/src/datum_protocol.h b/src/datum_protocol.h index 0ba64c03..5618b7ff 100644 --- a/src/datum_protocol.h +++ b/src/datum_protocol.h @@ -56,6 +56,13 @@ #define DATUM_PROTOCOL_VERSION "v0.4.1-beta" // this is sent to the server as a UA #define DATUM_PROTOCOL_CONNECT_TIMEOUT 30 +/* Optional 0x11 trailer after the coinbaser blob: 4-byte magic then the + * 32-byte request prevhash. Bytes are not all equal so a repeated-byte + * pad cannot match. Stock OCEAN/CONVOY replies stay value + blob. */ +#define DATUM_COINBASER_PREVHASH_MAGIC "CBPH" +#define DATUM_COINBASER_PREVHASH_MAGIC_LEN 4 +#define DATUM_COINBASER_PREVHASH_TRAILER_LEN 36 + #define DATUM_PROTOCOL_MAX_CMD_DATA_SIZE 4194304 // 2^22 - protocol limit! #define DATUM_PROTOCOL_BUFFER_SIZE (DATUM_PROTOCOL_MAX_CMD_DATA_SIZE*3) #define DATUM_PROTOCOL_MAX_USERNAME_LEN 384 @@ -72,14 +79,14 @@ // It's likely possible to brute force the XOR key to break packets down into individual commands, but the contents and nature of the // cmd is still obfuscated and unrecoverable without the session keys. -typedef struct __attribute__((packed)) T_DATUM_PROTOCOL_HEADER { +typedef struct T_DATUM_PROTOCOL_HEADER { uint32_t cmd_len:22; // max cmd size is 2^22 (~4MB), which is roughly the max block size for a raw submission or a raw template validation - uint8_t reserved:2; // save for later use - bool is_signed:1; - bool is_encrypted_pubkey:1; - bool is_encrypted_channel:1; + bool is_signed; + bool is_encrypted_pubkey; + bool is_encrypted_channel; uint8_t proto_cmd:5; // 32 protocol level commands } T_DATUM_PROTOCOL_HEADER; +#define T_DATUM_PROTOCOL_HEADER_WIRE_BYTES 4 typedef struct { bool is_remote; diff --git a/src/datum_protocol_internal.h b/src/datum_protocol_internal.h index b14c611a..65898d29 100644 --- a/src/datum_protocol_internal.h +++ b/src/datum_protocol_internal.h @@ -55,7 +55,17 @@ extern atomic_uint_fast64_t datum_session_generation; extern unsigned char datum_protocol_next_job_idx; extern T_DATUM_PROTOCOL_JOB datum_jobs[MAX_DATUM_PROTOCOL_JOBS]; +// Scratch buffer for replies built on the protocol thread, and the most a +// 0x50 0x11 reply may hold before its terminator and padding. +#define DATUM_PROTOCOL_TEMP_DATA_SIZE (DATUM_PROTOCOL_MAX_CMD_DATA_SIZE + 16384) +#define DATUM_STXLIST_REPLY_MAX (DATUM_PROTOCOL_MAX_CMD_DATA_SIZE - 113) +extern unsigned char temp_data[DATUM_PROTOCOL_TEMP_DATA_SIZE]; +bool datum_protocol_stxlist_reply_fits(size_t offset, size_t txn_size); +int datum_protocol_job_validation_stxlist_byid(int len, unsigned char *data); + uint32_t datum_header_xor_feedback(uint32_t i); +void datum_header_pk(uint8_t * const dst, const size_t offset, const T_DATUM_PROTOCOL_HEADER * const h, uint32_t * const xor_key); +void datum_header_upk(T_DATUM_PROTOCOL_HEADER * const h, const uint8_t * const src, const size_t offset, uint32_t * const xor_key); int datum_protocol_flush_socket(int sockfd); void datum_protocol_bulk_reset(void); int datum_protocol_bulk_cmd_for_session( @@ -73,11 +83,14 @@ T_DATUM_REPLAY_PENDING *datum_protocol_replay_add( void datum_protocol_replay_mark_responded_legacy( uint32_t nonce, uint8_t target_pot, uint8_t job_id); +// Room for the description of the client that found a pending ABW candidate +#define DATUM_ABW_FINDER_LEN 320 + void datum_protocol_abw_reset(void); bool datum_protocol_abw_assignment_revealed(uint8_t assignment_id); bool datum_protocol_abw_cache_candidate(const T_DATUM_PROTOCOL_POW *pow, const unsigned char *full_cb_tx, size_t full_cb_tx_size, - const unsigned char *raw_pow_hash); + const unsigned char *raw_pow_hash, const char *finder); int datum_protocol_abw_candidate_receipt(int len, unsigned char *data); int datum_protocol_abw_candidate_release(int len, unsigned char *data); int datum_protocol_abw_activation(int len, unsigned char *data); @@ -85,6 +98,8 @@ int datum_protocol_abw_assignment_notice(int len, unsigned char *data); int datum_protocol_abw_reveal(int len, unsigned char *data); int datum_protocol_mining_cmd(void *data, int len); +int datum_protocol_coinbaser_fetch_response(int len, unsigned char *data); +bool datum_protocol_coinbaser_reply_is_for_job(uint64_t value, const unsigned char prevhash[32]); int datum_protocol_client_configure(int len, unsigned char *data); int datum_protocol_mining_cmd5( T_DATUM_PROTOCOL_HEADER *header, unsigned char *data); diff --git a/src/datum_protocol_tests.c b/src/datum_protocol_tests.c index babc7eba..e1477f98 100644 --- a/src/datum_protocol_tests.c +++ b/src/datum_protocol_tests.c @@ -34,6 +34,7 @@ */ #include +#include #include #include #include @@ -128,11 +129,9 @@ static int datum_protocol_test_decrypt_frame(const unsigned char *wire, unsigned char nonce[crypto_box_NONCEBYTES], T_DATUM_PROTOCOL_HEADER *header, unsigned char *clear, size_t clear_size) { - if (*offset + sizeof(*header) > wire_size) return -1; - memcpy(header, wire + *offset, sizeof(*header)); - *((uint32_t *)header) ^= *header_key; - *header_key = datum_header_xor_feedback(*header_key); - *offset += sizeof(*header); + if (*offset + T_DATUM_PROTOCOL_HEADER_WIRE_BYTES > wire_size) return -1; + datum_header_upk(header, wire, *offset, header_key); + *offset += T_DATUM_PROTOCOL_HEADER_WIRE_BYTES; if (!header->is_encrypted_channel || header->cmd_len < crypto_box_MACBYTES || *offset + header->cmd_len > wire_size || header->cmd_len - crypto_box_MACBYTES > clear_size) return -1; @@ -179,7 +178,7 @@ static void datum_protocol_bulk_tests(void) { uint32_t receiver_header_key = sending_header_key; unsigned char receiver_nonce[crypto_box_NONCEBYTES]; memcpy(receiver_nonce, session_nonce_sender, sizeof(receiver_nonce)); - unsigned char wire[2 * (sizeof(T_DATUM_PROTOCOL_HEADER) + + unsigned char wire[2 * (T_DATUM_PROTOCOL_HEADER_WIRE_BYTES + DATUM_BULK_FRAGMENT_HEADER_SIZE + DATUM_BULK_FRAGMENT_DATA_SIZE + crypto_box_MACBYTES)]; unsigned char clear[DATUM_BULK_FRAGMENT_HEADER_SIZE + @@ -583,7 +582,7 @@ static void datum_protocol_abw_cache_tests(void) { pow.target_byte = 10; pow.nonce = 7; pow.ntime = 1000; - datum_test(datum_protocol_abw_cache_candidate(&pow, coinbase, sizeof(coinbase), raw_hash)); + datum_test(datum_protocol_abw_cache_candidate(&pow, coinbase, sizeof(coinbase), raw_hash, "tester from 192.0.2.1 (client 0/1, session 00400001, agent test)")); datum_config.mining_abw_verify_all_shares_on_disclosure = true; unsigned char receipt[35] = {DATUM_ABW_DRAFT_REVISION, 3}; @@ -595,7 +594,7 @@ static void datum_protocol_abw_cache_tests(void) { unsigned char second_hash[32]; memset(second_hash, 0xfe, sizeof(second_hash)); pow.nonce++; - datum_test(datum_protocol_abw_cache_candidate(&pow, coinbase, sizeof(coinbase), second_hash)); + datum_test(datum_protocol_abw_cache_candidate(&pow, coinbase, sizeof(coinbase), second_hash, NULL)); datum_config.mining_abw_verify_all_shares_on_disclosure = false; memcpy(pow.raw_pow_hash, second_hash, sizeof(pow.raw_pow_hash)); static const unsigned char replay_message[] = {0x27, 0xFE}; @@ -617,12 +616,12 @@ static void datum_protocol_abw_cache_tests(void) { memset(subsidy_hash, 0xfc, sizeof(subsidy_hash)); pow.subsidy_only = true; pow.nonce++; - datum_test(datum_protocol_abw_cache_candidate(&pow, coinbase, sizeof(coinbase), subsidy_hash)); + datum_test(datum_protocol_abw_cache_candidate(&pow, coinbase, sizeof(coinbase), subsidy_hash, NULL)); pow.subsidy_only = false; datum_test(datum_protocol_abw_reveal(sizeof(reveal), reveal)); datum_test(datum_protocol_abw_assignment_revealed(4)); - datum_test(!datum_protocol_abw_cache_candidate(&pow, coinbase, sizeof(coinbase), raw_hash)); + datum_test(!datum_protocol_abw_cache_candidate(&pow, coinbase, sizeof(coinbase), raw_hash, NULL)); reveal[18] = 0; datum_test(!datum_protocol_abw_reveal(sizeof(reveal), reveal)); reveal[18] = 0xFE; @@ -828,6 +827,168 @@ static void datum_pow_recycled_protocol_job_test(void) { free(jobs); } +static void datum_protocol_stxlist_byid_tests(void) { + // A 0x50 0x11 request names transaction indices with nothing against + // repeats, and the reply is built in the fixed temp_data buffer. The + // handler has to refuse a list the reply cannot hold instead of writing + // past the end of the buffer. + T_DATUM_STRATUM_JOB * const job = calloc(1, sizeof(*job)); + T_DATUM_TEMPLATE_DATA * const block_template = calloc(1, sizeof(*block_template)); + const uint32_t txn_size = 100000; + const uint32_t txn_count = 64; + T_DATUM_TEMPLATE_TXN * const txns = calloc(txn_count, sizeof(*txns)); + unsigned char * const txn_data = malloc(txn_size); + unsigned char * const tail = temp_data + DATUM_PROTOCOL_TEMP_DATA_SIZE - 64; + unsigned char request[3 + 2 * 64]; + unsigned char canary[64]; + const int saved_out = server_out_buf; + const uint32_t saved_header_key = sending_header_key; + unsigned char saved_nonce[sizeof(session_nonce_sender)]; + unsigned char job_index; + size_t k; + + memcpy(saved_nonce, session_nonce_sender, sizeof(saved_nonce)); + datum_test(job && block_template && txns && txn_data); + if (!job || !block_template || !txns || !txn_data) goto cleanup; + + memset(txn_data, 0x5a, txn_size); + for (k = 0; k < txn_count; ++k) { + txns[k].size = txn_size; + txns[k].txn_data_binary = txn_data; + } + block_template->txn_count = txn_count; + block_template->txns = txns; + job->block_template = block_template; + snprintf(job->job_id, sizeof(job->job_id), "stxlist"); + memset(datum_jobs, 0, sizeof(datum_jobs)); + datum_protocol_next_job_idx = 0; + job_index = datum_protocol_setup_new_job_idx(job); + datum_jobs[job_index].server_sjob = job; + memcpy(datum_jobs[job_index].server_job_id, job->job_id, + sizeof(datum_jobs[job_index].server_job_id)); + server_out_buf = 0; + memset(canary, 0xc7, sizeof(canary)); + memcpy(tail, canary, sizeof(canary)); + + // Two distinct transactions fit and are answered. + request[0] = job_index; + pk_u16le(request, 1, 2); + pk_u16le(request, 3, 0); + pk_u16le(request, 5, 1); + datum_test(datum_protocol_job_validation_stxlist_byid(7, request) == 1); + datum_test(temp_data[0] == 0x50 && temp_data[1] == 0x91); + datum_test(temp_data[2] == job_index && temp_data[3] == 0x01); + datum_test(upk_u16le(temp_data, 4) == 2); + datum_test(!memcmp(tail, canary, sizeof(canary))); + server_out_buf = 0; + + // The largest transaction 64 times over would need 6.4 MB. The count + // alone passes the txn_count gate; the reply must be refused. + pk_u16le(request, 1, 64); + for (k = 0; k < 64; ++k) pk_u16le(request, 3 + 2 * k, 0); + datum_test(datum_protocol_job_validation_stxlist_byid(3 + 2 * 64, request) == 1); + datum_test(temp_data[2] == job_index && temp_data[3] == 0xF4); + datum_test(!memcmp(tail, canary, sizeof(canary))); + server_out_buf = 0; + + // A list cut short of its count is refused before it is read. + pk_u16le(request, 1, 4); + temp_data[3] = 0; + datum_test(datum_protocol_job_validation_stxlist_byid(3 + 2 * 3, request) == 1); + datum_test(temp_data[2] == job_index && temp_data[3] == 0xF4); + datum_test(datum_protocol_job_validation_stxlist_byid(2, request) == 0); + + // The bound leaves room for the terminator and the padding. + datum_test(datum_protocol_stxlist_reply_fits(0, txn_size)); + datum_test(datum_protocol_stxlist_reply_fits(DATUM_STXLIST_REPLY_MAX - 3 - txn_size, txn_size)); + datum_test(!datum_protocol_stxlist_reply_fits(DATUM_STXLIST_REPLY_MAX - 2 - txn_size, txn_size)); + +cleanup: + datum_protocol_bulk_reset(); + memset(datum_jobs, 0, sizeof(datum_jobs)); + datum_protocol_next_job_idx = 0; + server_out_buf = saved_out; + sending_header_key = saved_header_key; + memcpy(session_nonce_sender, saved_nonce, sizeof(saved_nonce)); + free(txn_data); + free(txns); + free(block_template); + free(job); +} + +static void fill_stock_coinbaser(unsigned char *out, size_t out_len, uint64_t value) +{ + memset(out, 0, out_len); + pk_u64le(out, 0, value); + pk_u32le(out, 8, 1); + out[12] = 1; +} + +static void datum_protocol_coinbaser_prevhash_tests(void) { + unsigned char stock[12 + 1]; + unsigned char pad31[12 + 1 + 31]; + unsigned char pad32[12 + 1 + 32]; + unsigned char pad100[12 + 1 + 100]; + unsigned char magicked[12 + 1 + DATUM_COINBASER_PREVHASH_TRAILER_LEN]; + unsigned char wrong[12 + 1 + DATUM_COINBASER_PREVHASH_TRAILER_LEN]; + unsigned char parent_a[32]; + unsigned char parent_b[32]; + const uint64_t value = UINT64_C(3125000000); + + memset(parent_a, 0xa1, sizeof(parent_a)); + memset(parent_b, 0xb2, sizeof(parent_b)); + datum_test(!(DATUM_COINBASER_PREVHASH_MAGIC[0] == DATUM_COINBASER_PREVHASH_MAGIC[1] + && DATUM_COINBASER_PREVHASH_MAGIC[1] == DATUM_COINBASER_PREVHASH_MAGIC[2] + && DATUM_COINBASER_PREVHASH_MAGIC[2] == DATUM_COINBASER_PREVHASH_MAGIC[3])); + fill_stock_coinbaser(stock, sizeof(stock), value); + + /* Stock value + blob: any parent matches (OCEAN/CONVOY servers). */ + datum_test(datum_protocol_coinbaser_fetch_response((int)sizeof(stock), stock) == 1); + datum_test(datum_protocol_coinbaser_reply_is_for_job(value, parent_a)); + datum_test(datum_protocol_coinbaser_reply_is_for_job(value, parent_b)); + datum_test(!datum_protocol_coinbaser_reply_is_for_job(value + 1, parent_a)); + + /* Repeated-byte pads of 31, 32, and 100: no magic, so no prevhash. */ + fill_stock_coinbaser(pad31, sizeof(pad31), value); + memset(pad31 + 13, 0x7a, 31); + datum_test(datum_protocol_coinbaser_fetch_response((int)sizeof(pad31), pad31) == 1); + datum_test(datum_protocol_coinbaser_reply_is_for_job(value, parent_a)); + datum_test(datum_protocol_coinbaser_reply_is_for_job(value, parent_b)); + + fill_stock_coinbaser(pad32, sizeof(pad32), value); + memset(pad32 + 13, 0x7a, 32); + datum_test(datum_protocol_coinbaser_fetch_response((int)sizeof(pad32), pad32) == 1); + datum_test(datum_protocol_coinbaser_reply_is_for_job(value, parent_a)); + datum_test(datum_protocol_coinbaser_reply_is_for_job(value, parent_b)); + + fill_stock_coinbaser(pad100, sizeof(pad100), value); + memset(pad100 + 13, 0x7a, 100); + datum_test(datum_protocol_coinbaser_fetch_response((int)sizeof(pad100), pad100) == 1); + datum_test(datum_protocol_coinbaser_reply_is_for_job(value, parent_a)); + datum_test(datum_protocol_coinbaser_reply_is_for_job(value, parent_b)); + + /* Magic + parent binds the reply to that prevhash. */ + fill_stock_coinbaser(magicked, sizeof(magicked), value); + memcpy(magicked + 13, DATUM_COINBASER_PREVHASH_MAGIC, DATUM_COINBASER_PREVHASH_MAGIC_LEN); + memcpy(magicked + 13 + DATUM_COINBASER_PREVHASH_MAGIC_LEN, parent_a, 32); + datum_test(datum_protocol_coinbaser_fetch_response((int)sizeof(magicked), magicked) == 1); + datum_test(datum_protocol_coinbaser_reply_is_for_job(value, parent_a)); + datum_test(!datum_protocol_coinbaser_reply_is_for_job(value, parent_b)); + + memcpy(magicked + 13 + DATUM_COINBASER_PREVHASH_MAGIC_LEN, parent_b, 32); + datum_test(datum_protocol_coinbaser_fetch_response((int)sizeof(magicked), magicked) == 1); + datum_test(!datum_protocol_coinbaser_reply_is_for_job(value, parent_a)); + datum_test(datum_protocol_coinbaser_reply_is_for_job(value, parent_b)); + + /* Wrong magic + 32 bytes: ignore the tail, same as stock. */ + fill_stock_coinbaser(wrong, sizeof(wrong), value); + memcpy(wrong + 13, "XXXX", DATUM_COINBASER_PREVHASH_MAGIC_LEN); + memcpy(wrong + 13 + DATUM_COINBASER_PREVHASH_MAGIC_LEN, parent_a, 32); + datum_test(datum_protocol_coinbaser_fetch_response((int)sizeof(wrong), wrong) == 1); + datum_test(datum_protocol_coinbaser_reply_is_for_job(value, parent_a)); + datum_test(datum_protocol_coinbaser_reply_is_for_job(value, parent_b)); +} + void datum_protocol_tests(void) { datum_protocol_config_v3_tests(); datum_protocol_migration_tests(); @@ -836,4 +997,6 @@ void datum_protocol_tests(void) { datum_protocol_abw_cache_tests(); datum_pow_response_large_difficulty_test(); datum_pow_recycled_protocol_job_test(); + datum_protocol_stxlist_byid_tests(); + datum_protocol_coinbaser_prevhash_tests(); } diff --git a/src/datum_stratum.c b/src/datum_stratum.c index a1818faf..34a3480a 100644 --- a/src/datum_stratum.c +++ b/src/datum_stratum.c @@ -85,6 +85,28 @@ uint64_t stratum_client_accepted_share_diff = 0; uint64_t stratum_client_rejected_share_count = 0; uint64_t stratum_client_rejected_share_diff = 0; +static pthread_mutex_t stratum_accept_mu = PTHREAD_MUTEX_INITIALIZER; +static bool stratum_accept_clients = true; + +void datum_stratum_set_accept_clients(bool accept) +{ + pthread_mutex_lock(&stratum_accept_mu); + if (stratum_accept_clients != accept) { + DLOG_WARN("Public SV1 accept_clients now %s", accept ? "true" : "false"); + } + stratum_accept_clients = accept; + pthread_mutex_unlock(&stratum_accept_mu); +} + +bool datum_stratum_accept_clients(void) +{ + bool accept; + pthread_mutex_lock(&stratum_accept_mu); + accept = stratum_accept_clients; + pthread_mutex_unlock(&stratum_accept_mu); + return accept; +} + void stratum_latest_empty_increment_complete(uint64_t index, int clients_notified) { pthread_rwlock_wrlock(&stratum_global_latest_empty_stat); if ((stratum_latest_empty_job_index == index) && (!stratum_latest_empty_ready_for_full)) { @@ -966,6 +988,17 @@ static void stratum_note_share(T_DATUM_MINER_DATA *m, bool accepted, uint64_t di } } +// Name the client behind a block for the log. Used by the local path +// below and by the ABW reveal in datum_protocol.c. The username comes +// straight from mining.submit, so it is filtered here; the user agent was +// already filtered by strncpy_uachars at subscribe. +void datum_stratum_describe_block_finder(char *out, size_t outsz, const T_DATUM_CLIENT_DATA *c, const char *username, bool empty_work) { + const T_DATUM_MINER_DATA * const m = c->app_client_data; + char who[192]; + strncpy_printable(who, username ? username : "NULL", sizeof(who)); + snprintf(out, outsz, "%s from %s (client %d/%d, session %08x, agent %s%s)", who, c->rem_host, c->datum_thread->thread_id, c->cid, m->sid, m->useragent[0] ? m->useragent : "unknown", empty_work ? ", on empty work" : ""); +} + int client_mining_submit(T_DATUM_CLIENT_DATA *c, uint64_t id, json_t *params_obj) { // {"params": ["username", "job", "extranonce2", "time", "nonce"], "id": 1, "method": "mining.submit"} // 0 = username @@ -1005,6 +1038,7 @@ int client_mining_submit(T_DATUM_CLIENT_DATA *c, uint64_t id, json_t *params_obj unsigned char coinbase_index = 0; T_DATUM_STRATUM_COINBASE *cb = NULL; unsigned char extranonce_bin[12]; + unsigned char job_id_bin[8]; unsigned char block_header[80]; unsigned char share_hash[40]; @@ -1046,6 +1080,11 @@ int client_mining_submit(T_DATUM_CLIENT_DATA *c, uint64_t id, json_t *params_obj return 0; } } + if (!hex_to_bin_exact(job_id_s, job_id_bin, sizeof(job_id_bin))) { + send_unknown_work_error(c,id); + stratum_note_share(m, false, m->last_sent_diff); // guestimate here + return 0; + } // jobID is // 4 bytes time (who cares) @@ -1054,7 +1093,7 @@ int client_mining_submit(T_DATUM_CLIENT_DATA *c, uint64_t id, json_t *params_obj // 1 byte coinbase index used // 6625a3d53cc0e500 // 0123456789ABCDEF - g_job_index = (hex2bin_uchar(&job_id_s[0xA])<<8) | hex2bin_uchar(&job_id_s[0xC]); + g_job_index = (job_id_bin[5]<<8) | job_id_bin[6]; g_job_index ^= STRATUM_JOB_INDEX_XOR; if (g_job_index >= MAX_STRATUM_JOBS) { send_unknown_work_error(c,id); @@ -1101,12 +1140,19 @@ int client_mining_submit(T_DATUM_CLIENT_DATA *c, uint64_t id, json_t *params_obj stratum_note_share(m, false, job_diff); return 0; } - for(i=0;i<8;i++) { - extranonce_bin[i+4] = hex2bin_uchar(&extranonce2_s[i<<1]); + if (!hex_to_bin_exact(extranonce2_s, extranonce_bin + 4, 8)) { + send_unknown_work_error(c, id); + stratum_note_share(m, false, job_diff); + return 0; } // need to build the full coinbase txn - coinbase_index = hex2bin_uchar(&job_id_s[0xE]); + coinbase_index = job_id_bin[7]; + // Quick difficulty jobs use Q instead of N; the empty coinbase index + // still identifies subsidy-only work while the coinbaser is pending. + if (quickdiff && coinbase_index == DATUM_COINBASE_ID_EMPTY) { + empty_work = true; + } if (coinbase_index >= MAX_COINBASE_TYPES) { if (!(empty_work && coinbase_index == DATUM_COINBASE_ID_EMPTY)) { send_unknown_work_error(c, id); @@ -1169,10 +1215,18 @@ int client_mining_submit(T_DATUM_CLIENT_DATA *c, uint64_t id, json_t *params_obj return 0; } if (ntime_len == 8) { - ntime_val = (uint32_t)strtoul(ntime_s, NULL, 16); + if (!hex_to_u32(ntime_s, &ntime_val)) { + send_unknown_work_error(c, id); + stratum_note_share(m, false, job_diff); + return 0; + } pk_u32le(ntime8, 0, ntime_val); } else { - for(i=0;i<8;i++) ntime8[i] = hex2bin_uchar(&ntime_s[i << 1]); + if (!hex_to_bin_exact(ntime_s, ntime8, 8)) { + send_unknown_work_error(c, id); + stratum_note_share(m, false, job_diff); + return 0; + } ntime_val = upk_u32le(ntime8, 0); } ntime64 = upk_u64le(ntime8, 0); @@ -1197,10 +1251,18 @@ int client_mining_submit(T_DATUM_CLIENT_DATA *c, uint64_t id, json_t *params_obj return 0; } if (nonce_len == 8) { - nonce_val = (uint32_t)strtoul(nonce_s, NULL, 16); + if (!hex_to_u32(nonce_s, &nonce_val)) { + send_unknown_work_error(c, id); + stratum_note_share(m, false, job_diff); + return 0; + } pk_u32le(nonce8, 0, nonce_val); } else { - for(i=0;i<8;i++) nonce8[i] = hex2bin_uchar(&nonce_s[i << 1]); + if (!hex_to_bin_exact(nonce_s, nonce8, 8)) { + send_unknown_work_error(c, id); + stratum_note_share(m, false, job_diff); + return 0; + } nonce_val = upk_u32le(nonce8, 0); } nonce64 = upk_u64le(nonce8, 0); @@ -1219,7 +1281,7 @@ int client_mining_submit(T_DATUM_CLIENT_DATA *c, uint64_t id, json_t *params_obj stratum_note_share(m, false, job_diff); return 0; } - datum_blake2b_build_work_header(work, job->prevhash_bin, nonce8, ntime8, root); + datum_blake2b_build_work_header_from_hidden(work, job->blake2b_prevblock_hidden, nonce8, ntime8, root); memcpy(block_header, work, 80); if (!datum_blake2b_pow_hash_le(share_hash, work, (const unsigned char[16]){0}, 0)) { send_unknown_work_error(c, id); @@ -1261,6 +1323,11 @@ int client_mining_submit(T_DATUM_CLIENT_DATA *c, uint64_t id, json_t *params_obj DLOG_WARN("************************************************************************************************"); DLOG_WARN("******** BLOCK FOUND - %s ********", new_notify_blockhash); DLOG_WARN("************************************************************************************************"); + { + char finder[320]; + datum_stratum_describe_block_finder(finder, sizeof(finder), c, username_s, empty_work); + DLOG_WARN("Block %s at height %llu found by %s", new_notify_blockhash, (unsigned long long)job->height, finder); + } if (job->is_datum_job) { (void)datum_protocol_pow_submit(c, job, username_s, true, @@ -1438,6 +1505,38 @@ int client_mining_authorize(T_DATUM_CLIENT_DATA *c, uint64_t id, json_t *params_ strncpy(m->last_auth_username, username_s, sizeof(m->last_auth_username) - 1); m->last_auth_username[sizeof(m->last_auth_username)-1] = 0; + + if (!datum_stratum_accept_clients()) { + char idbuf[160]; + stratum_rpc_id_text(c, id, idbuf, sizeof(idbuf)); + snprintf(s, sizeof(s), + "{\"id\":%s,\"result\":null,\"error\":[24,\"pool-at-capacity\",null]}\n", + idbuf); + datum_socket_send_string_to_client(c, s); + stratum_rpc_id_clear(c); + return 0; + } + + if (datum_config.datum_pool_pass_full_users) { + char ident[256]; + unsigned char script[64]; + size_t n = 0; + while (username_s[n] && username_s[n] != '.' && n + 1 < sizeof ident) { + ident[n] = username_s[n]; + n++; + } + ident[n] = 0; + if (!n || !addr_2_output_script(ident, script, (int)sizeof script)) { + char idbuf[160]; + stratum_rpc_id_text(c, id, idbuf, sizeof(idbuf)); + snprintf(s, sizeof(s), + "{\"id\":%s,\"result\":null,\"error\":[24,\"invalid-payout-address\",null]}\n", + idbuf); + datum_socket_send_string_to_client(c, s); + stratum_rpc_id_clear(c); + return 0; + } + } char idbuf[160]; stratum_rpc_id_text(c, id, idbuf, sizeof(idbuf)); @@ -1450,15 +1549,21 @@ int client_mining_authorize(T_DATUM_CLIENT_DATA *c, uint64_t id, json_t *params_ return 0; } +// The coinbase a BLAKE2b job commits to, the same for every miner: the +// subsidy-only one for new-block work and while the coinbaser is late on a +// pooled connection (CONVOY #13: do not pair class 0 with a full template), +// COINBASE_TYPE_TINY only when solo, and COINBASE_TYPE_YUGE after the +// coinbaser is ready (CONVOY #10). On BLAKE2b the miner never receives the +// coinbase, so there is no per-miner selection. unsigned int datum_stratum_coinbase_index( - const T_DATUM_STRATUM_THREADPOOL_DATA *sdata, - const T_DATUM_MINER_DATA *miner, bool new_block) { + const T_DATUM_STRATUM_THREADPOOL_DATA *sdata, bool new_block) { 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 datum_protocol_is_active() ? DATUM_COINBASE_ID_EMPTY : 0; + } + return COINBASE_TYPE_YUGE; } int send_mining_notify(T_DATUM_CLIENT_DATA *c, bool clean, bool quickdiff, bool new_block) { @@ -1552,7 +1657,7 @@ int send_mining_notify(T_DATUM_CLIENT_DATA *c, bool clean, bool quickdiff, bool const int notify_out_buf_start = c->out_buf; datum_socket_send_string_to_client(c, "{\"id\":null,\"method\":\"mining.notify\",\"params\":["); - cbselect = datum_stratum_coinbase_index(sdata, m, new_block); + cbselect = datum_stratum_coinbase_index(sdata, new_block); const bool subsidy_only = cbselect == DATUM_COINBASE_ID_EMPTY; cb = subsidy_only ? &j->subsidy_only_coinbase : &j->coinbase[cbselect]; @@ -1622,64 +1727,18 @@ int send_mining_set_difficulty(T_DATUM_CLIENT_DATA *c) { void datum_stratum_fingerprint_by_UA(T_DATUM_MINER_DATA *m) { // TODO: Make this a little more efficient. perhaps move to a loadable definitions file of some kind. - if (strstr(m->useragent, "Antminer A3") == m->useragent) { - m->coinbase_selection = 0; - return; - } - - // S21 tested to handle 2.25KB coinbase work on all versions released - // UA starts with: Antminer S21/ - // S21 Pro NOT confirmed to work this way (yet)... so keep the / - if (strstr(m->useragent, "Antminer S21/") == m->useragent) { - m->coinbase_selection = 5; // ANTMAIN2 - return; - } - - // the ePIC control boards can handle almost any size coinbase - // UA starts with: PowerPlay-BM/ - if (strstr(m->useragent, "PowerPlay-BM/") == m->useragent) { - m->coinbase_selection = 4; // YUGE - return; - } - - // "vinsh" reports as xminer - // Tested to handle up to 16KB - if (strstr(m->useragent, "xminer-1.") == m->useragent) { - m->coinbase_selection = 4; // YUGE - return; - } - - // whatsminer works fine with about a 6.5 KB coinbase - // UA starts with: whatsminer/v1 - if (strstr(m->useragent, "whatsminer/v1") == m->useragent) { - m->coinbase_selection = 3; // RESPECTABLE - return; - } - - // Braiins firmware - // Appears to handle arbitrary coinbase sizes, however not extensively tested on all firmware versions - // feed the S21-like coinbase for now, which is at least moderately sized - // UA contains: bosminer-plus-tuner - if (strstr(m->useragent, "bosminer-plus-tuner") != NULL) { // match anywhere in string, not just beginning - m->coinbase_selection = 5; // ANTMAIN2 - return; - } - - // Nicehash, sadly needs a smaller coinbase than even antminer s19s - // they also need a high minimum difficulty + // For SHA256d work this function also chose the coinbase class the miner's + // firmware was known to accept: COINBASE_TYPE_TINY for the Antminer A3, the + // 2250-byte class for the Antminer S21 ("Antminer S21/") and Braiins + // ("bosminer-plus-tuner", matched anywhere in the string), 16000 for ePIC + // ("PowerPlay-BM/") and xminer ("xminer-1."), 6500 for Whatsminer + // ("whatsminer/v1") and the Bitaxe ("bitaxe"), 500 for NiceHash, and the + // 755-byte Antminer S19 class for everything else. BLAKE2b work serves + // every miner COINBASE_TYPE_YUGE (datum_stratum_coinbase_index), so only + // the NiceHash minimum difficulty remains. if (strstr(m->useragent, "NiceHash/") == m->useragent) { m->current_diff=524288; m->forced_high_min_diff=524288; - m->coinbase_selection = 1; // TINY - return; - } - - // The Bitaxe is tested to work with a large coinbase - // However, it does slow work changes slightly when they're YUGE, so we'll go with - // the whatsminer tested size as a compromise. also should save some bandwidth, which - // is probably not a bad plan, given the low odds of a bitaxe finding a block. - if (strstr(m->useragent, "bitaxe") == m->useragent) { - m->coinbase_selection = 3; // RESPECTABLE return; } } @@ -1704,9 +1763,9 @@ 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; + // The class every miner is served on BLAKE2b work once the full coinbase is + // ready (datum_stratum_coinbase_index); kept per miner only for the API. + m->coinbase_selection = COINBASE_TYPE_YUGE; m->useragent[0] = 0; if (params_obj) { @@ -2049,7 +2108,6 @@ bool datum_stratum_job_blake2b_commitment(T_DATUM_STRATUM_JOB *s, const T_DATUM_ void datum_stratum_job_refresh_blake2b(T_DATUM_STRATUM_JOB *s) { T_DATUM_TEMPLATE_DATA *block_template; - unsigned char prevblock_hidden[32]; uint32_t time_on_wire; int i; @@ -2075,10 +2133,10 @@ void datum_stratum_job_refresh_blake2b(T_DATUM_STRATUM_JOB *s) { } s->blake2b_time_on_wire = time_on_wire; - datum_blake2b_prevblock_hidden(prevblock_hidden, block_template->previousblockhash_bin); + datum_blake2b_prevblock_hidden(s->blake2b_prevblock_hidden, block_template->previousblockhash_bin); for(i=0;i<32;i++) { - uchar_to_hex(&s->prevhash[i << 1], prevblock_hidden[i]); + uchar_to_hex(&s->prevhash[i << 1], s->blake2b_prevblock_hidden[i]); } s->prevhash[64] = 0; } @@ -2336,7 +2394,6 @@ int assembleBlockAndSubmit(uint8_t *block_header, uint8_t *coinbase_txn, size_t CURL *tcurl; int ret = 0; bool free_submitblock_req = false; - char *s = NULL; unsigned char v2hdr[DATUM_BLAKE2B_BLOCK_HEADER_SIZE]; unsigned char merkle[32]; unsigned char en[12]; @@ -2459,32 +2516,8 @@ int assembleBlockAndSubmit(uint8_t *block_header, uint8_t *coinbase_txn, size_t // make the call! r = bitcoind_json_rpc_call(tcurl, &datum_config, submitblock_req); curl_easy_cleanup(tcurl); - if (!r) { - // Didn't get a usable response at all: either the request never reached the node, or it - // returned something we couldn't parse as a valid JSON-RPC reply, or a genuine top-level - // JSON-RPC error occurred. In every case we genuinely don't know if the block was - // accepted -- don't claim success. The dedicated submitblock thread triggered above is - // still independently submitting this same block. - DLOG_ERROR("Did not get a valid response submitting block %s! It may or may not have been accepted -- check your node!", block_hash_hex); - ret = 0; - } else { - json_t * const res_val = json_object_get(r, "result"); - if (json_is_null(res_val)) { - // a null result means success here - DLOG_INFO("Block %s submitted to upstream node successfully!",block_hash_hex); - ret = 1; - } else { - s = json_dumps(res_val, JSON_ENCODE_ANY); - if (!s) { - DLOG_WARN("Upstream node rejected our block! (unknown)"); - } else { - DLOG_WARN("Upstream node rejected our block! (%s)",s); - free(s); - } - ret = 0; - } - json_decref(r); - } + ret = datum_submitblock_log_reply(r, block_hash_hex) ? 1 : 0; + if (r) json_decref(r); // cleanup if (free_submitblock_req) { diff --git a/src/datum_stratum.h b/src/datum_stratum.h index 3cb1fd43..25b1d786 100644 --- a/src/datum_stratum.h +++ b/src/datum_stratum.h @@ -37,6 +37,7 @@ #define _DATUM_STRATUM_H_ #include +#include #include #ifndef T_DATUM_CLIENT_DATA @@ -57,6 +58,23 @@ #define COINBASE_TYPE_RESPECTABLE 3 // 6500 byte max (whatsminers) #define COINBASE_TYPE_YUGE 4 // 16KB max (ePIC, bitaxe) #define COINBASE_TYPE_ANTMAIN2 5 // 2.25KB max (S21, +?) +// The classes were sized to what SHA256d firmware could accept, since those +// miners receive coinb1/coinb2 and hash the coinbase. On BLAKE2b work the +// miner receives 000000 || H2 || 00000000 as coinb1 (H2 is the "Merge-mining +// hook" tagged hash, which commits to the coinbase) and an empty coinb2, and +// the work root is blake2b(0x00 || coinb1 || extranonce), so the coinbase +// itself never reaches the miner. A smaller class therefore only omits some of +// the pool's dictated outputs from the block; their value is paid to the +// pool's address as the remainder. BLAKE2b work serves every miner +// COINBASE_TYPE_YUGE once the full coinbase is ready. Until then a pooled +// job is DATUM_COINBASE_ID_EMPTY (subsidy-only, not class 0 + a full +// template); solo stays COINBASE_TYPE_TINY. Classes 1, 2, 3 and 5 are still +// built, but their indexes never appear in a job id. The 16000-byte limit of +// COINBASE_TYPE_YUGE covers the whole coinbase transaction +// (datum_stratum_coinbase_fit_to_template subtracts the fixed bytes), so it +// fits in STRATUM_COINBASE2_MAX_LEN (32768 hex characters, 16384 bytes) and +// holds a little under 512 P2WPKH outputs (31 bytes each), the most a +// coinbaser dictates, but only about 365 taproot outputs (43 bytes each). // Submitblock json rpc command max size is max block size * 2 for ascii plus some breathing room #define MAX_SUBMITBLOCK_SIZE 8500000 @@ -136,6 +154,8 @@ typedef struct T_DATUM_STRATUM_JOB { // BLAKE2b job fields uint32_t blake2b_time_on_wire; uint8_t blake2b_flags; + // Tagged hash shared by every submitted share for this job. + unsigned char blake2b_prevblock_hidden[32]; T_DATUM_TEMPLATE_DATA *block_template; @@ -273,11 +293,12 @@ bool datum_stratum_job_blake2b_commitment_from_txn(const T_DATUM_STRATUM_JOB *s, bool datum_stratum_job_blake2b_commitment(T_DATUM_STRATUM_JOB *s, const T_DATUM_STRATUM_COINBASE *cb, bool subsidy_only, unsigned char pot, unsigned char *commitment, unsigned char *coinb1); bool datum_stratum_share_is_unmasked_block( const T_DATUM_STRATUM_JOB *job, const unsigned char *share_hash); -unsigned int datum_stratum_coinbase_index(const T_DATUM_STRATUM_THREADPOOL_DATA *sdata, const T_DATUM_MINER_DATA *miner, bool new_block); +unsigned int datum_stratum_coinbase_index(const T_DATUM_STRATUM_THREADPOOL_DATA *sdata, bool new_block); void stratum_job_merkle_root_calc(T_DATUM_STRATUM_JOB *s, unsigned char *coinbase_txn_hash, unsigned char *merkle_root_output); 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); size_t datum_stratum_coinbase_for_block_hex(char *out, size_t out_size, const uint8_t *coinbase_txn, size_t coinbase_txn_size, bool add_witness); bool datum_stratum_block_needs_witness(const T_DATUM_STRATUM_JOB *job, bool subsidy_only); +void datum_stratum_describe_block_finder(char *out, size_t outsz, const T_DATUM_CLIENT_DATA *c, const char *username, bool empty_work); size_t datum_stratum_build_block_request_parts(char *out, size_t out_size, const uint8_t *block_header, const uint8_t *coinbase_txn, size_t coinbase_txn_size, bool add_witness, @@ -302,6 +323,8 @@ void datum_stratum_v1_socket_thread_client_new(T_DATUM_CLIENT_DATA *c); int datum_stratum_v1_global_subscriber_count(void); double datum_stratum_v1_est_total_th_sec(void); void datum_stratum_v1_shutdown_all(void); +void datum_stratum_set_accept_clients(bool accept); +bool datum_stratum_accept_clients(void); extern T_DATUM_SOCKET_APP *global_stratum_app; diff --git a/src/datum_stratum_tests.c b/src/datum_stratum_tests.c index 17199bba..ad28408c 100644 --- a/src/datum_stratum_tests.c +++ b/src/datum_stratum_tests.c @@ -34,11 +34,14 @@ */ #include +#include #include #include #include "datum_jsonrpc.h" #include "datum_pow.h" +#include "datum_protocol.h" +#include "datum_protocol_internal.h" #include "datum_stratum.h" #include "datum_utils.h" @@ -48,6 +51,8 @@ int client_mining_submit(T_DATUM_CLIENT_DATA *c, uint64_t id, json_t *params_obj static void datum_blake2b_refresh_time_offset_tests(void) { T_DATUM_TEMPLATE_DATA tdata; T_DATUM_STRATUM_JOB job; + unsigned char expected_hidden[32]; + char expected_hex[65]; /* A job snapshots whether miners may submit a time offset. */ memset(&tdata, 0, sizeof(tdata)); @@ -58,6 +63,13 @@ static void datum_blake2b_refresh_time_offset_tests(void) { datum_stratum_job_refresh_blake2b(&job); datum_test(job.blake2b_time_on_wire == 2000000000u); datum_test(job.blake2b_flags == DATUM_BLAKE2B_USE_TIME_OFFSET); + datum_blake2b_prevblock_hidden(expected_hidden, tdata.previousblockhash_bin); + datum_test(!memcmp(job.blake2b_prevblock_hidden, expected_hidden, sizeof(expected_hidden))); + for (size_t i = 0; i < sizeof(expected_hidden); i++) { + uchar_to_hex(expected_hex + (i << 1), expected_hidden[i]); + } + expected_hex[64] = 0; + datum_test(!strcmp(job.prevhash, expected_hex)); /* Without the flag the offset is ignored and curtime goes on the wire as is. */ tdata.curtime = 2000000000; @@ -199,22 +211,172 @@ static void datum_blake2b_h_not_zero_tests(void) { global_cur_stratum_jobs[0] = saved_job; } +static void datum_blake2b_malformed_submit_job_tests(void) { + T_DATUM_CLIENT_DATA client = {0}; + T_DATUM_MINER_DATA miner = {0}; + T_DATUM_STRATUM_JOB job = {0}; + T_DATUM_TEMPLATE_DATA tdata = {0}; + T_DATUM_STRATUM_JOB *saved_job = global_cur_stratum_jobs[0]; + static const char * const submits[] = { + "{\"id\":8,\"method\":\"mining.submit\",\"params\":[\"miner\",\"0000000000c0dg00\",\"0000000000000000\",\"00000000\",\"00000000\"]}", + "{\"id\":8,\"method\":\"mining.submit\",\"params\":[\"miner\",\"0000000000c0de00\",\"000000000000000g\",\"00000000\",\"00000000\"]}", + "{\"id\":8,\"method\":\"mining.submit\",\"params\":[\"miner\",\"0000000000c0de00\",\"0000000000000000\",\"0000000g\",\"00000000\"]}", + "{\"id\":8,\"method\":\"mining.submit\",\"params\":[\"miner\",\"0000000000c0de00\",\"0000000000000000\",\"000000000000000g\",\"00000000\"]}", + "{\"id\":8,\"method\":\"mining.submit\",\"params\":[\"miner\",\"0000000000c0de00\",\"0000000000000000\",\"00000000\",\"0000000g\"]}", + "{\"id\":8,\"method\":\"mining.submit\",\"params\":[\"miner\",\"0000000000c0de00\",\"0000000000000000\",\"00000000\",\"000000000000000g\"]}", + }; + static const char expected[] = + "{\"error\":[20,\"unknown-work\",null],\"id\":8,\"result\":null}\n"; + char submit[192]; + + client.app_client_data = &miner; + job.block_template = &tdata; + job.target_pot_index = 0; + job.coinbase[0].coinb1_len = 1; + job.coinbase[0].coinb1_bin[0] = 0xff; + strcpy(job.job_id, "0000000000c0de00"); + miner.stratum_job_diffs[0] = 1; + global_cur_stratum_jobs[0] = &job; + + for (size_t i = 0; i < sizeof(submits) / sizeof(submits[0]); i++) { + const uint64_t rejected_before = miner.share_count_rejected; + client.out_buf = 0; + strcpy(submit, submits[i]); + datum_test(datum_stratum_v1_socket_thread_client_cmd(&client, submit) == 0); + datum_test(miner.share_count_rejected == rejected_before + 1); + datum_test(client.out_buf == (int)strlen(expected)); + datum_test(!memcmp(client.w_buffer, expected, strlen(expected))); + } + + global_cur_stratum_jobs[0] = saved_job; +} + static void datum_blake2b_coinbase_selection_tests(void) { T_DATUM_STRATUM_THREADPOOL_DATA *sdata = calloc(1, sizeof(*sdata)); T_DATUM_STRATUM_JOB job = {0}; - T_DATUM_MINER_DATA miner = {.coinbase_selection = 3}; + const int saved_client_active = atomic_load(&datum_protocol_client_active); + unsigned char notice[36] = { + DATUM_ABW_DRAFT_REVISION, DATUM_ABW_ASSIGNMENT_ACTIVE, 0, + }; datum_test(sdata != NULL); if (!sdata) return; - datum_test(datum_stratum_coinbase_index(sdata, &miner, true) == DATUM_COINBASE_ID_EMPTY); - datum_test(datum_stratum_coinbase_index(sdata, &miner, false) == 0); + datum_test(datum_stratum_coinbase_index(sdata, true) == DATUM_COINBASE_ID_EMPTY); + datum_test(datum_stratum_coinbase_index(sdata, false) == 0); sdata->cur_stratum_job = &job; sdata->full_coinbase_ready = true; - datum_test(datum_stratum_coinbase_index(sdata, &miner, false) == 0); + datum_test(datum_stratum_coinbase_index(sdata, false) == 0); + job.job_state = JOB_STATE_FULL_PRIORITY_WAIT_COINBASER; + // With the job state at or above JOB_STATE_FULL_PRIORITY_WAIT_COINBASER and + // full_coinbase_ready set, every miner is served the largest class: there + // is no per-miner selection on BLAKE2b work. + datum_test(datum_stratum_coinbase_index(sdata, false) == COINBASE_TYPE_YUGE); + sdata->full_coinbase_ready = false; + /* Protocol inactive: not-ready stays class 0 (solo). */ + datum_test(datum_stratum_coinbase_index(sdata, false) == 0); + + /* Handshake (client_active != 3) is not pooled yet: still class 0. */ + atomic_store(&datum_protocol_client_active, 2); + datum_test(!datum_protocol_is_active()); + datum_test(datum_stratum_coinbase_index(sdata, false) == 0); + + /* Pooled and coinbaser late: subsidy-only, not class 0 + a full template. */ + atomic_store(&datum_protocol_client_active, 3); + if (!datum_protocol_is_active()) { + memset(notice + 3, 0x5a, 32); + notice[35] = 0xFE; + datum_test(datum_protocol_abw_assignment_notice(sizeof(notice), notice) == 1); + } + datum_test(datum_protocol_is_active()); + datum_test(datum_stratum_coinbase_index(sdata, false) == DATUM_COINBASE_ID_EMPTY); + sdata->full_coinbase_ready = true; + datum_test(datum_stratum_coinbase_index(sdata, false) == COINBASE_TYPE_YUGE); + datum_test(datum_stratum_coinbase_index(sdata, true) == DATUM_COINBASE_ID_EMPTY); + + datum_protocol_abw_reset(); + atomic_store(&datum_protocol_client_active, saved_client_active); + free(sdata); +} + +static void datum_blake2b_quickdiff_coinbase_tests(void) { + T_DATUM_THREAD_DATA *thread = calloc(1, sizeof(*thread)); + T_DATUM_STRATUM_THREADPOOL_DATA *sdata = calloc(1, sizeof(*sdata)); + T_DATUM_CLIENT_DATA client = {0}; + T_DATUM_MINER_DATA miner = {0}; + T_DATUM_STRATUM_JOB job = {0}; + T_DATUM_TEMPLATE_DATA tdata = {0}; + T_DATUM_STRATUM_JOB *saved_job = global_cur_stratum_jobs[0]; + const int saved_active = atomic_load(&datum_protocol_client_active); + static const struct { + bool ready, quickdiff; + const char *job_id; + } cases[] = { + {false, false, "N0000000000c0deff"}, + {false, true, "Q0000000000c0deff"}, + {true, true, "Q0000000000c0de04"}, + }; + static const char expected[] = + "{\"error\":[23,\"H-not-zero\",null],\"id\":42,\"result\":null}\n"; + + if (!datum_test(thread && sdata)) { + free(thread); + free(sdata); + return; + } + thread->app_thread_data = sdata; + client.datum_thread = thread; + client.app_client_data = &miner; + miner.sdata = sdata; + sdata->cur_stratum_job = &job; + job.block_template = &tdata; job.job_state = JOB_STATE_FULL_PRIORITY_WAIT_COINBASER; - datum_test(datum_stratum_coinbase_index(sdata, &miner, false) == 3); - miner.coinbase_selection = MAX_COINBASE_TYPES; - datum_test(datum_stratum_coinbase_index(sdata, &miner, false) == 0); + strcpy(job.job_id, "0000000000c0de"); + strcpy(job.version, "20000000"); + job.subsidy_only_coinbase.coinb1_len = 1; + job.subsidy_only_coinbase.coinb1_bin[0] = 0xff; + job.coinbase[COINBASE_TYPE_YUGE] = job.subsidy_only_coinbase; + tdata.curtime = 1000; + tdata.version = 0x20000000; + tdata.bits_uint = 0x1d00ffff; + datum_stratum_job_refresh_blake2b(&job); + global_cur_stratum_jobs[0] = &job; + atomic_store(&datum_protocol_client_active, 3); + if (!datum_protocol_is_active()) { + unsigned char notice[36] = {DATUM_ABW_DRAFT_REVISION, DATUM_ABW_ASSIGNMENT_ACTIVE, 0}; + memset(notice + 3, 0x5a, 32); + notice[35] = 0xfe; + datum_test(datum_protocol_abw_assignment_notice(sizeof(notice), notice) == 1); + } + datum_test(datum_protocol_is_active()); + + for (size_t i = 0; i < sizeof(cases) / sizeof(cases[0]); ++i) { + sdata->full_coinbase_ready = cases[i].ready; + miner.current_diff = miner.last_sent_diff = 2ULL << i; + miner.stratum_job_diffs[0] = 1; + client.out_buf = 0; + datum_test(send_mining_notify(&client, true, cases[i].quickdiff, false) == 0); + json_t *notify = json_loadb(client.w_buffer, client.out_buf, 0, NULL); + const char *job_id = json_string_value(json_array_get(json_object_get(notify, "params"), 0)); + if (datum_test(job_id != NULL)) { + datum_test(!strcmp(job_id, cases[i].job_id)); + datum_test(miner.quickdiff_active == cases[i].quickdiff); + datum_test(miner.stratum_job_diffs[0] == (cases[i].quickdiff ? 1 : miner.last_sent_diff)); + if (cases[i].quickdiff) datum_test(miner.quickdiff_value == miner.last_sent_diff); + json_t *params = json_pack("[sssss]", "miner", job_id, + "0000000000000000", "00000000", "00000000"); + client.out_buf = 0; + datum_test(client_mining_submit(&client, 42, params) == 0); + // The advertised job must reach proof validation, not unknown-work. + datum_test(client.out_buf == (int)strlen(expected)); + datum_test(!memcmp(client.w_buffer, expected, strlen(expected))); + json_decref(params); + } + json_decref(notify); + } + datum_protocol_abw_reset(); + atomic_store(&datum_protocol_client_active, saved_active); + global_cur_stratum_jobs[0] = saved_job; + free(thread); free(sdata); } @@ -508,7 +670,9 @@ void datum_stratum_tests(void) { datum_stratum_minimum_difficulty_configure_tests(); datum_stratum_string_request_id_tests(); datum_blake2b_coinbase_selection_tests(); + datum_blake2b_quickdiff_coinbase_tests(); datum_blake2b_h_not_zero_tests(); + datum_blake2b_malformed_submit_job_tests(); datum_blake2b_client_pot_commitment_tests(); datum_blake2b_unmasked_block_tests(); datum_stratum_abw_block_request_tests(); diff --git a/src/datum_submitblock.c b/src/datum_submitblock.c index 50853c99..f4adf376 100644 --- a/src/datum_submitblock.c +++ b/src/datum_submitblock.c @@ -44,6 +44,7 @@ #include "datum_utils.h" #include "datum_conf.h" #include "datum_jsonrpc.h" +#include "datum_submitblock.h" pthread_mutex_t submitblock_mutex = PTHREAD_MUTEX_INITIALIZER; pthread_cond_t submitblock_cond = PTHREAD_COND_INITIALIZER; @@ -64,37 +65,44 @@ void preciousblock(CURL *curl, char *blockhash) { return; } +datum_submitblock_status datum_submitblock_reply_status(const json_t *reply) { + if (!reply) return DATUM_SUBMITBLOCK_UNKNOWN; + const json_t * const result = json_object_get(reply, "result"); + if (json_is_null(result)) return DATUM_SUBMITBLOCK_ACCEPTED; + if (json_is_string(result) && !strcmp(json_string_value(result), "duplicate")) return DATUM_SUBMITBLOCK_DUPLICATE; + return DATUM_SUBMITBLOCK_REJECTED; +} + +bool datum_submitblock_log_reply(const json_t *reply, const char *block_hash_hex) { + switch (datum_submitblock_reply_status(reply)) { + case DATUM_SUBMITBLOCK_ACCEPTED: + DLOG_INFO("Block %s submitted to upstream node successfully!", block_hash_hex); + return true; + case DATUM_SUBMITBLOCK_DUPLICATE: + DLOG_INFO("Block %s was already accepted by the upstream node (duplicate submission)", block_hash_hex); + return true; + case DATUM_SUBMITBLOCK_UNKNOWN: + DLOG_ERROR("Did not get a valid response submitting block %s! It may or may not have been accepted -- check your node!", block_hash_hex); + return false; + case DATUM_SUBMITBLOCK_REJECTED: + default: { + char * const s = json_dumps(json_object_get(reply, "result"), JSON_ENCODE_ANY); + DLOG_WARN("Upstream node rejected our block! (%s)", s ? s : "unknown"); + free(s); + return false; + } + } +} + void datum_submitblock_doit(CURL *tcurl, char *url, const char *submitblock_req, const char *block_hash_hex) { json_t *r; - char *s = NULL; - // TODO: Move these types of things to the conf file if (!url) { r = bitcoind_json_rpc_call(tcurl, &datum_config, submitblock_req); } else { r = json_rpc_call(tcurl, url, NULL, submitblock_req); } - if (!r) { - // Didn't get a usable response at all: either the request never reached the node, or it - // returned something we couldn't parse as a valid JSON-RPC reply, or a genuine top-level - // JSON-RPC error occurred. In every case, we genuinely don't know whether the block was - // accepted -- don't claim success. - DLOG_ERROR("Did not get a valid response submitting block %s! It may or may not have been accepted -- check your node!", block_hash_hex); - } else { - json_t * const res_val = json_object_get(r, "result"); - if (json_is_null(res_val)) { - // a null result means success here - DLOG_INFO("Block %s submitted to upstream node successfully!",block_hash_hex); - } else { - s = json_dumps(res_val, JSON_ENCODE_ANY); - if (!s) { - DLOG_WARN("Upstream node rejected our block! (unknown)"); - } else { - DLOG_WARN("Upstream node rejected our block! (%s)",s); - free(s); - } - } - json_decref(r); - } + datum_submitblock_log_reply(r, block_hash_hex); + if (r) json_decref(r); // precious block! preciousblock(tcurl, submitblock_hash); diff --git a/src/datum_submitblock.h b/src/datum_submitblock.h index 76eeea4e..34a089ec 100644 --- a/src/datum_submitblock.h +++ b/src/datum_submitblock.h @@ -37,6 +37,19 @@ #define _DATUM_SUBMITBLOCK_H_ #include +#include + +// What a submitblock reply means. (CONVOY #3) +typedef enum { + DATUM_SUBMITBLOCK_UNKNOWN = 0, + DATUM_SUBMITBLOCK_ACCEPTED, + DATUM_SUBMITBLOCK_DUPLICATE, + DATUM_SUBMITBLOCK_REJECTED, +} datum_submitblock_status; + +datum_submitblock_status datum_submitblock_reply_status(const json_t *reply); +bool datum_submitblock_log_reply(const json_t *reply, const char *block_hash_hex); +void datum_submitblock_reply_tests(void); void datum_submitblock_init(void); void datum_submitblock_trigger(const char *ptr, const char *hash); diff --git a/src/datum_submitblock_tests.c b/src/datum_submitblock_tests.c index adc9b861..9e6542e3 100644 --- a/src/datum_submitblock_tests.c +++ b/src/datum_submitblock_tests.c @@ -34,6 +34,7 @@ */ #include +#include #include #include #include @@ -109,4 +110,27 @@ void datum_submitblock_tests(void) { datum_test(state.requests[1] == second_request); datum_test(!strcmp(state.hashes[0], first_hash)); datum_test(!strcmp(state.hashes[1], second_hash)); + datum_submitblock_reply_tests(); +} + +static datum_submitblock_status status_of(const char * const json_text) { + json_t * const reply = json_loads(json_text, 0, NULL); + datum_test(reply != NULL); + const datum_submitblock_status st = datum_submitblock_reply_status(reply); + json_decref(reply); + return st; +} + +void datum_submitblock_reply_tests(void) { + datum_test(datum_submitblock_reply_status(NULL) == DATUM_SUBMITBLOCK_UNKNOWN); + datum_test(status_of("{\"result\":null,\"error\":null,\"id\":1}") == DATUM_SUBMITBLOCK_ACCEPTED); + datum_test(status_of("{\"result\":\"duplicate\",\"error\":null,\"id\":1}") == DATUM_SUBMITBLOCK_DUPLICATE); + datum_test(status_of("{\"result\":\"duplicate-invalid\",\"error\":null,\"id\":1}") == DATUM_SUBMITBLOCK_REJECTED); + datum_test(status_of("{\"result\":\"duplicate-inconclusive\",\"error\":null,\"id\":1}") == DATUM_SUBMITBLOCK_REJECTED); + datum_test(status_of("{\"result\":\"inconclusive\",\"error\":null,\"id\":1}") == DATUM_SUBMITBLOCK_REJECTED); + datum_test(status_of("{\"result\":\"bad-txnmrklroot\",\"error\":null,\"id\":1}") == DATUM_SUBMITBLOCK_REJECTED); + datum_test(status_of("{\"result\":\"prev-blk-not-found\",\"error\":null,\"id\":1}") == DATUM_SUBMITBLOCK_REJECTED); + datum_test(status_of("{\"result\":true,\"error\":null,\"id\":1}") == DATUM_SUBMITBLOCK_REJECTED); + datum_test(status_of("{\"result\":{\"x\":1},\"error\":null,\"id\":1}") == DATUM_SUBMITBLOCK_REJECTED); + datum_test(status_of("{\"error\":null,\"id\":1}") == DATUM_SUBMITBLOCK_REJECTED); } diff --git a/src/datum_utils.c b/src/datum_utils.c index 9d296d9d..366a8bd2 100644 --- a/src/datum_utils.c +++ b/src/datum_utils.c @@ -85,6 +85,7 @@ void datum_utils_init(void) { #ifdef __GNUC__ // faster, less portable uint64_t roundDownToPowerOfTwo_64(uint64_t x) { + if (x == 0) return 0; // __builtin_clzll(0) is undefined (CONVOY #12) return 1ULL << (63 - __builtin_clzll(x)); } @@ -356,6 +357,34 @@ void hex_to_bin(const char *hex, unsigned char *bin) { } } +static int hex_value(const char c) { + if (c >= '0' && c <= '9') return c - '0'; + if (c >= 'a' && c <= 'f') return c - 'a' + 10; + if (c >= 'A' && c <= 'F') return c - 'A' + 10; + return -1; +} + +bool hex_to_bin_exact(const char *hex, unsigned char *bin, const size_t bin_len) { + int high, low; + if (!hex || !bin) return false; + for (size_t i = 0; i < bin_len; i++) { + high = hex_value(hex[i<<1]); + if (high < 0) return false; + low = hex_value(hex[(i<<1)+1]); + if (low < 0) return false; + bin[i] = (unsigned char)((high << 4) | low); + } + return hex[bin_len<<1] == 0; +} + +bool hex_to_u32(const char *hex, uint32_t *out) { + unsigned char bin[4]; + if (!out || !hex_to_bin_exact(hex, bin, sizeof(bin))) return false; + *out = ((uint32_t)bin[0] << 24) | ((uint32_t)bin[1] << 16) | + ((uint32_t)bin[2] << 8) | bin[3]; + return true; +} + void panic_from_thread(int a) { // set panic flag panic_mode = 1; @@ -580,6 +609,25 @@ bool strncpy_workerchars(char *out, const char *in, size_t maxlen) { return true; } +bool strncpy_printable(char *out, const char *in, size_t maxlen) { + // copy a string from in to out for the log, replacing anything outside + // printable ASCII with '?' so a client cannot put line breaks or terminal + // escapes into the log + // copy a max of maxlen-1 chars from in to out + size_t i = 0; + + if (in == NULL || out == NULL || maxlen == 0) { + return false; + } + + for (; in[i] != 0 && i + 1 < maxlen; i++) { + const unsigned char c = (unsigned char)in[i]; + out[i] = (c >= 0x20 && c <= 0x7e) ? (char)c : '?'; + } + out[i] = 0; + return true; +} + bool strncpy_uachars(char *out, const char *in, size_t maxlen) { // copy a string from in to out, stripping out unwanted characters // copy a max of maxlen chars from in to out diff --git a/src/datum_utils.h b/src/datum_utils.h index ff4ac339..6b909b43 100644 --- a/src/datum_utils.h +++ b/src/datum_utils.h @@ -71,6 +71,8 @@ void panic_from_thread(int a); bool double_sha256(void *out, const void *in, size_t length); void hex_to_bin_le(const char *hex, unsigned char *bin); void hex_to_bin(const char *hex, unsigned char *bin); +bool hex_to_bin_exact(const char *hex, unsigned char *bin, size_t bin_len); +bool hex_to_u32(const char *hex, uint32_t *out); void hash2hex(unsigned char *bytes, char *hexString); uint64_t roundDownToPowerOfTwo_64(uint64_t x); int addr_2_output_script(const char *addr, unsigned char *script, int max_len); @@ -80,6 +82,7 @@ void uchar_to_hex(char *s, const unsigned char b); int get_bitcoin_varint_len_bytes(uint64_t n); bool strncpy_uachars(char *out, const char *in, size_t maxlen); bool strncpy_workerchars(char *out, const char *in, size_t maxlen); +bool strncpy_printable(char *out, const char *in, size_t maxlen); long double calc_network_difficulty(const char *bits_hex); unsigned char floorPoT(uint64_t x); uint64_t datum_siphash(const void *src, uint64_t sz, const unsigned char key[16]); diff --git a/src/datum_utils_tests.c b/src/datum_utils_tests.c index 58820b07..49ef0367 100644 --- a/src/datum_utils_tests.c +++ b/src/datum_utils_tests.c @@ -36,6 +36,7 @@ #include #include #include +#include #include #include @@ -60,6 +61,8 @@ void datum_utils_tests_hex_to_bin(const uint8_t c, char * const x, const char * void datum_utils_tests_hex(void) { char x[6], x2[6]; + unsigned char exact[2] = {0x0e, 0x0e}; + uint32_t value; strcpy(&x[2], "00"); for (unsigned int c = 0; ; ++c) { datum_utils_tests_hex_to_bin(c, &x2[1], "%2.2X"); @@ -94,6 +97,38 @@ void datum_utils_tests_hex(void) { ++x[3]; } } + + datum_test(hex_to_bin_exact("00fF", exact, sizeof(exact))); + datum_test(exact[0] == 0 && exact[1] == 0xff); + datum_test(!hex_to_bin_exact("00fg", exact, sizeof(exact))); + datum_test(!hex_to_bin_exact("00f", exact, sizeof(exact))); + datum_test(!hex_to_bin_exact("00ff0", exact, sizeof(exact))); + datum_test(!hex_to_bin_exact(NULL, exact, sizeof(exact))); + datum_test(!hex_to_bin_exact("00ff", NULL, sizeof(exact))); + + datum_test(hex_to_u32("00000000", &value)); + datum_test(value == 0); + datum_test(hex_to_u32("1234aBcD", &value)); + datum_test(value == UINT32_C(0x1234abcd)); + datum_test(hex_to_u32("FFFFFFFF", &value)); + datum_test(value == UINT32_MAX); + datum_test(!hex_to_u32("1234567", &value)); + datum_test(!hex_to_u32("1234567g", &value)); + datum_test(!hex_to_u32("123456789", &value)); + datum_test(!hex_to_u32(NULL, &value)); + datum_test(!hex_to_u32("00000000", NULL)); + + /* Exact allocations expose reads past the terminator under ASan. */ + for (size_t len = 0; len < 8; ++len) { + unsigned char bin[4]; + char *truncated = malloc(len + 1); + if (!datum_test(truncated != NULL)) break; + memcpy(truncated, "1234aBcD", len); + truncated[len] = '\0'; + datum_test(!hex_to_bin_exact(truncated, bin, sizeof(bin))); + datum_test(!hex_to_u32(truncated, &value)); + free(truncated); + } } void datum_utils_tests_secure_strequals(void) { @@ -141,9 +176,25 @@ static void datum_utils_tests_pdiff_to_bdiff(void) { datum_test(datum_pdiff_to_bdiff(16) == 15.999755859375L); } +static void datum_utils_tests_strncpy_printable(void) { + char out[8]; + datum_test(!strncpy_printable(out, NULL, sizeof(out))); + datum_test(!strncpy_printable(NULL, "x", sizeof(out))); + datum_test(!strncpy_printable(out, "x", 0)); + datum_test(strncpy_printable(out, "ab\n\x1b[1m", sizeof(out))); + datum_test(strcmp(out, "ab??[1m") == 0); + datum_test(strncpy_printable(out, "0123456789", sizeof(out))); + datum_test(strcmp(out, "0123456") == 0); + datum_test(strncpy_printable(out, "\xc3\xa9", sizeof(out))); + datum_test(strcmp(out, "??") == 0); + datum_test(strncpy_printable(out, "", sizeof(out))); + datum_test(out[0] == 0); +} + void datum_utils_tests(void) { datum_utils_tests_hex(); datum_utils_tests_secure_strequals(); datum_utils_tests_scriptnum(); datum_utils_tests_pdiff_to_bdiff(); + datum_utils_tests_strncpy_printable(); }