diff --git a/src/ckpool.c b/src/ckpool.c index c9df915..7bf1fda 100644 --- a/src/ckpool.c +++ b/src/ckpool.c @@ -1768,6 +1768,9 @@ static void parse_config(void) LOGWARNING("Signature %s too long, truncating to 38 bytes", ckpool.btcsig); ckpool.btcsig[38] = '\0'; } + yyjson_obj_get_string(&ckpool.payoutplan, json_conf, "payoutplan"); + if (ckpool.payoutplan && !strlen(ckpool.payoutplan)) + dealloc(ckpool.payoutplan); yyjson_obj_get_int(&ckpool.blockpoll, json_conf, "blockpoll"); yyjson_obj_get_int(&ckpool.nonce1length, json_conf, "nonce1length"); yyjson_obj_get_int(&ckpool.nonce2length, json_conf, "nonce2length"); @@ -2068,6 +2071,7 @@ static void report_config(void) printf("cashaddr_prefix = %s\n", ckpool.cashaddr_prefix ? ckpool.cashaddr_prefix : "(autodetect)"); printf("mindiff_overrides = %d\n", ckpool.mindiff_overrides_count); printf("btcsig = %s\n", ckpool.btcsig ? ckpool.btcsig : "(none)"); + printf("payoutplan = %s\n", ckpool.payoutplan ? ckpool.payoutplan : "(none)"); printf("mindiff = %"PRId64"\n", ckpool.mindiff); printf("startdiff = %"PRId64"\n", ckpool.startdiff); diff --git a/src/ckpool.h b/src/ckpool.h index 5cb5bc3..3b283f5 100644 --- a/src/ckpool.h +++ b/src/ckpool.h @@ -292,6 +292,7 @@ struct ckpool_instance { bool poolvalid; // Pool address is valid double poolfee; // Pool operator fee percentage char *btcsig; // Optional signature to add to coinbase + char *payoutplan; // Pool mode only: file listing the miner coinbase outputs (PPLNS) bool coinbase_valid; // Coinbase transaction confirmed valid bool regtest; diff --git a/src/stratifier.c b/src/stratifier.c index 455bf61..755fee3 100644 --- a/src/stratifier.c +++ b/src/stratifier.c @@ -598,6 +598,327 @@ static int ser_bip34_height(uint8_t *buf, uint32_t height) return nlen + 1; } +/* --------------------------------------------------------------------- + * DirectPool PPLNS payout plan (pool mode only, "payoutplan" config key). + * + * An external planner keeps the PPLNS share window and the deferred-payout + * ledger, and writes the miner outputs it wants in the next coinbase to a + * small text file, one output per line: + * + * + * + * Only standard P2PKH (76a914<20>88ac) and P2SH (a914<20>87) scripts are + * accepted, weights are positive integers. The miner part of the coinbase + * (coinbasevalue minus the pool fee) is split in proportion to the weights; + * rounding dust goes to the largest weight. The pool fee output is kept as + * the last output, exactly as in the single-output layout. + * + * A file that fails to parse is ignored and the last good plan stays in + * use. Every new plan is checked once by building a sample coinbase and + * asking the node to decode it; a plan the node rejects is never used. + * Without any good plan the pool falls back to upstream behaviour (all of + * the miner reward to btcaddress) and says so loudly in the log. + * ------------------------------------------------------------------- */ + +#define PLAN_MAX_OUTPUTS 200 +#define PLAN_MAX_SCRIPT 25 +#define PLAN_MAX_FILE (64 * 1024) +#define PLAN_STALE_SECS 600 +#define PLAN_MAX_WEIGHT 2100000000000000ULL + +typedef struct plan_out { + uchar script[PLAN_MAX_SCRIPT]; + int scriptlen; + uint64_t weight; +} plan_out_t; + +typedef struct payout_plan { + plan_out_t outs[PLAN_MAX_OUTPUTS]; + int count; + uint64_t generation; +} payout_plan_t; + +static pthread_mutex_t plan_lock = PTHREAD_MUTEX_INITIALIZER; +static payout_plan_t plan_new; /* latest parsed file, maybe unchecked */ +static payout_plan_t plan_ok; /* latest plan the node accepted */ +static uint64_t plan_generation; +static uint64_t plan_bad_generation; +static time_t plan_file_mtime; +static long plan_file_mtime_ns; +static off_t plan_file_size; +static time_t plan_stale_warned; +static time_t plan_fallback_warned; + +static bool plan_script_ok(const uchar *s, int len) +{ + if (len == 25) + return s[0] == 0x76 && s[1] == 0xa9 && s[2] == 0x14 && s[23] == 0x88 && s[24] == 0xac; + if (len == 23) + return s[0] == 0xa9 && s[1] == 0x14 && s[22] == 0x87; + return false; +} + +/* Parse the plan file into *out. Returns false (and leaves *out untouched + * on the caller's side) on any malformed line. */ +static bool plan_parse(const char *text, payout_plan_t *out) +{ + const char *line = text; + int count = 0, lineno = 0; + + while (*line) { + const char *end = strchr(line, '\n'); + char buf[128], hex[64]; + unsigned long long weight; + size_t len = end ? (size_t)(end - line) : strlen(line); + int hexlen, n; + + lineno++; + if (len >= sizeof(buf)) { + LOGWARNING("Payout plan line %d too long", lineno); + return false; + } + memcpy(buf, line, len); + buf[len] = '\0'; + line += len + (end ? 1 : 0); + if (len && buf[len - 1] == '\r') + buf[--len] = '\0'; + if (!len || buf[0] == '#') + continue; + if (sscanf(buf, "%63s %llu%n", hex, &weight, &n) != 2 || buf[n] != '\0') { + LOGWARNING("Payout plan line %d malformed", lineno); + return false; + } + hexlen = strlen(hex); + if (hexlen % 2 || hexlen / 2 > PLAN_MAX_SCRIPT || !weight || weight > PLAN_MAX_WEIGHT) { + LOGWARNING("Payout plan line %d has a bad script length or weight", lineno); + return false; + } + if (count >= PLAN_MAX_OUTPUTS) { + LOGWARNING("Payout plan has more than %d outputs", PLAN_MAX_OUTPUTS); + return false; + } + for (n = 0; n < hexlen; n++) { + if (!isxdigit((unsigned char)hex[n])) { + LOGWARNING("Payout plan line %d is not hex", lineno); + return false; + } + } + if (!hex2bin(out->outs[count].script, hex, hexlen / 2) || + !plan_script_ok(out->outs[count].script, hexlen / 2)) { + LOGWARNING("Payout plan line %d is not a P2PKH or P2SH script", lineno); + return false; + } + out->outs[count].scriptlen = hexlen / 2; + out->outs[count].weight = weight; + count++; + } + out->count = count; + return true; +} + +/* Re-read the plan file if it changed. Called with plan_lock held. */ +static void plan_reload(void) +{ + struct stat st; + char *text; + FILE *fp; + size_t got; + time_t now = time(NULL); + + if (stat(ckpool.payoutplan, &st)) { + if (now - plan_stale_warned >= 60) { + LOGWARNING("Payout plan %s missing: %s", ckpool.payoutplan, strerror(errno)); + plan_stale_warned = now; + } + return; + } + if (now - st.st_mtime > PLAN_STALE_SECS && now - plan_stale_warned >= 60) { + LOGWARNING("Payout plan is %ld seconds old, is the PPLNS planner running?", + (long)(now - st.st_mtime)); + plan_stale_warned = now; + } + if (st.st_mtime == plan_file_mtime && st.st_mtim.tv_nsec == plan_file_mtime_ns && + st.st_size == plan_file_size) + return; + if (st.st_size > PLAN_MAX_FILE) { + LOGWARNING("Payout plan %s too large (%ld bytes)", ckpool.payoutplan, (long)st.st_size); + return; + } + fp = fopen(ckpool.payoutplan, "re"); + if (!fp) { + LOGWARNING("Cannot open payout plan %s: %s", ckpool.payoutplan, strerror(errno)); + return; + } + text = ckzalloc(PLAN_MAX_FILE + 1); + got = fread(text, 1, PLAN_MAX_FILE, fp); + fclose(fp); + text[got] = '\0'; + plan_file_mtime = st.st_mtime; + plan_file_mtime_ns = st.st_mtim.tv_nsec; + plan_file_size = st.st_size; + { + payout_plan_t *parsed = ckzalloc(sizeof(payout_plan_t)); + + if (plan_parse(text, parsed)) { + parsed->generation = ++plan_generation; + memcpy(&plan_new, parsed, sizeof(payout_plan_t)); + LOGINFO("Payout plan reloaded: %d outputs", plan_new.count); + } else + LOGWARNING("Ignoring invalid payout plan, keeping the previous one"); + free(parsed); + } + free(text); +} + +/* Build coinb2 (everything after the extranonces) for a plan. Returns the + * new buffer and its length, or NULL if the plan pays nothing. */ +static uchar *plan_build_coinb2(const workbase_t *wb, const payout_plan_t *plan, int *len_out) +{ + uint64_t g64 = wb->coinbasevalue, d64 = 0, given = 0, u64; + uint64_t amounts[PLAN_MAX_OUTPUTS]; + unsigned __int128 total_w = 0; + sdata_t *sdata = ckpool.sdata; + int i, nout = 0, ofs, biggest = 0; + uchar *buf; + + if (plan->count < 1) + return NULL; + if (ckpool.poolvalid && ckpool.poolfee > 0) { + double dbl64 = (double)g64 / 100 * ckpool.poolfee; + + d64 = dbl64; + if (d64 < 546) + d64 = 0; /* same dust rule as the single-output layout */ + } + g64 -= d64; + for (i = 0; i < plan->count; i++) { + total_w += plan->outs[i].weight; + if (plan->outs[i].weight > plan->outs[biggest].weight) + biggest = i; + } + for (i = 0; i < plan->count; i++) { + amounts[i] = (uint64_t)((unsigned __int128)g64 * plan->outs[i].weight / total_w); + given += amounts[i]; + } + amounts[biggest] += g64 - given; + for (i = 0; i < plan->count; i++) + if (amounts[i]) + nout++; + nout += d64 ? 1 : 0; + if (nout < 1 || nout >= 0xfd) + return NULL; + + buf = ckzalloc(wb->coinb2len + 1 + plan->count * (8 + 1 + PLAN_MAX_SCRIPT) + 8 + 1 + + sizeof(sdata->pooltxnbin) + 4); + memcpy(buf, wb->coinb2bin, wb->coinb2len); + ofs = wb->coinb2len; + buf[ofs++] = nout; + for (i = 0; i < plan->count; i++) { + if (!amounts[i]) + continue; + u64 = htole64(amounts[i]); + memcpy(buf + ofs, &u64, 8); + ofs += 8; + buf[ofs++] = plan->outs[i].scriptlen; + memcpy(buf + ofs, plan->outs[i].script, plan->outs[i].scriptlen); + ofs += plan->outs[i].scriptlen; + } + if (d64) { + u64 = htole64(d64); + memcpy(buf + ofs, &u64, 8); + ofs += 8; + buf[ofs++] = sdata->pooltxnlen; + memcpy(buf + ofs, sdata->pooltxnbin, sdata->pooltxnlen); + ofs += sdata->pooltxnlen; + } + ofs += 4; /* zero nLockTime, buffer is zeroed */ + *len_out = ofs; + return buf; +} + +/* Ask the node to decode a sample coinbase built from this coinb2. */ +static bool plan_coinbase_valid(const workbase_t *wb, const uchar *coinb2, int coinb2len) +{ + int cblen = wb->coinb1len + ckpool.nonce1length + ckpool.nonce2length + coinb2len; + uchar *cb = ckzalloc(cblen); + char *hex, *decoded; + + memcpy(cb, wb->coinb1bin, wb->coinb1len); + memcpy(cb + wb->coinb1len + ckpool.nonce1length + ckpool.nonce2length, coinb2, coinb2len); + hex = bin2hex(cb, cblen); + free(cb); + decoded = generator_checktxn(hex); + free(hex); + if (!decoded) + return false; + free(decoded); + return true; +} + +/* Replace the coinbase outputs of wb with the PPLNS plan. Called by + * generate_coinbase() in pool mode, after coinb2 holds the scriptSig tail + * and nSequence. Returns false to fall back to the single-output layout. */ +static bool plan_coinbase(workbase_t *wb) +{ + payout_plan_t *use = NULL, *cand = NULL; + uchar *buf = NULL; + int len = 0; + time_t now = time(NULL); + + if (unlikely(wb->insert_witness)) { + LOGWARNING("Witness commitment requested, payout plan not applied"); + return false; + } + cand = ckzalloc(sizeof(payout_plan_t)); + pthread_mutex_lock(&plan_lock); + plan_reload(); + if (plan_new.generation && plan_new.generation != plan_ok.generation && + plan_new.generation != plan_bad_generation) + memcpy(cand, &plan_new, sizeof(payout_plan_t)); + pthread_mutex_unlock(&plan_lock); + + if (cand->generation) { + /* An empty plan is valid: nobody has shares in the window yet. */ + buf = cand->count ? plan_build_coinb2(wb, cand, &len) : NULL; + if (!cand->count || (buf && plan_coinbase_valid(wb, buf, len))) { + pthread_mutex_lock(&plan_lock); + memcpy(&plan_ok, cand, sizeof(payout_plan_t)); + pthread_mutex_unlock(&plan_lock); + if (cand->count) + LOGINFO("Payout plan with %d outputs checked by the node", cand->count); + } else { + LOGEMERG("Payout plan produced a coinbase the node rejects, not using it"); + pthread_mutex_lock(&plan_lock); + plan_bad_generation = cand->generation; + pthread_mutex_unlock(&plan_lock); + dealloc(buf); + } + } + if (!buf) { + pthread_mutex_lock(&plan_lock); + if (plan_ok.count) + memcpy(cand, &plan_ok, sizeof(payout_plan_t)); + else + cand->count = 0; + pthread_mutex_unlock(&plan_lock); + use = cand; + buf = use->count ? plan_build_coinb2(wb, use, &len) : NULL; + } + free(cand); + if (!buf) { + if (now - plan_fallback_warned >= 300) { + LOGWARNING("No usable payout plan with shares yet, coinbase pays %s", ckpool.btcaddress); + plan_fallback_warned = now; + } + return false; + } + free(wb->coinb2bin); + wb->coinb2bin = buf; + wb->coinb2len = len; + wb->coinb3len = 0; + return true; +} + static void generate_coinbase(workbase_t *wb) { uint64_t u64, g64, d64 = 0; @@ -688,6 +1009,10 @@ static void generate_coinbase(workbase_t *wb) memcpy(wb->coinb2bin + wb->coinb2len, "\xff\xff\xff\xff", 4); wb->coinb2len += 4; + /* DirectPool PPLNS: miner outputs from the payout plan */ + if (!ckpool.btcsolo && ckpool.payoutplan && plan_coinbase(wb)) + goto coinb2_done; + // Generation value g64 = wb->coinbasevalue; /* Pool operator fee: a second coinbase output paying ckpool.poolfee @@ -843,6 +1168,7 @@ static void generate_coinbase(workbase_t *wb) LOGWARNING("Mining solo to any incoming valid BCH address username"); } +coinb2_done: /* Set this just for node compatibility, though it's unused */ wb->coinb2 = bin2hex(wb->coinb2bin, wb->coinb2len); LOGDEBUG("Coinb2: %s", wb->coinb2); @@ -6912,9 +7238,13 @@ static bool parse_authorise(stratum_instance_t *client, yyjson_mut_val *params_v * username looked like an attempted address but failed validation; * reject it with an explicit error instead of the generic auth * failure message so a typo is obvious to the miner. */ - if (!ckpool.btcsolo || user->btcaddress || user->pool_fallback) + if ((!ckpool.btcsolo && !ckpool.payoutplan) || user->btcaddress || user->pool_fallback) ret = true; - else if (looks_like_address(user->username)) { + else if (ckpool.payoutplan && !looks_like_address(user->username)) { + /* PPLNS pays every miner in the coinbase, so the username + * must be the address to pay. */ + *err_doc = yyjson_string("Use your Bitcoin Cash address as the username (address.worker)"); + } else if (looks_like_address(user->username)) { *err_doc = yyjson_string("Invalid BCH address (bad checksum or wrong network) - " "check your username for typos"); } @@ -11485,6 +11815,11 @@ void *stratifier(void *arg) ckpool.poolfee = 0; } } + if (ckpool.payoutplan && (ckpool.btcsolo || ckpool.proxy)) { + LOGWARNING("payoutplan only works in pool mode, ignoring it"); + dealloc(ckpool.payoutplan); + } else if (ckpool.payoutplan) + LOGWARNING("PPLNS mode: miner coinbase outputs from payout plan %s", ckpool.payoutplan); } randomiser = time(NULL);