diff options
| author | Nsable-sh <nsable-sh@noreply.git.bitflash.network> | 2026-08-07 17:14:36 -0300 |
|---|---|---|
| committer | Bitflash <admin@bitflash.network> | 2026-08-07 17:21:08 -0300 |
| commit | b1eb8b66b8d7b47c7fb2e85d5d4db86a11823366 (patch) | |
| tree | 8f0cb17723ce1dc434ae7be7a148f7fec3f977a0 /src | |
| parent | 060a37de4a1f08ffb5939c8fc47a805c5d0d0024 (diff) | |
pool: publish round proof ledger
Diffstat (limited to 'src')
| -rw-r--r-- | src/main.cpp | 1 | ||||
| -rw-r--r-- | src/main.h | 1 | ||||
| -rw-r--r-- | src/main_gui.cpp | 5 | ||||
| -rw-r--r-- | src/readme.txt | 4 | ||||
| -rw-r--r-- | src/rpc.cpp | 316 |
5 files changed, 323 insertions, 4 deletions
diff --git a/src/main.cpp b/src/main.cpp index 2be9c54..2eb0e3a 100644 --- a/src/main.cpp +++ b/src/main.cpp | |||
| @@ -105,6 +105,7 @@ string strParticipantPool; // participant mode: pool .btf address | |||
| 105 | string strPoolName = "Bitflash Pool"; | 105 | string strPoolName = "Bitflash Pool"; |
| 106 | string strPoolDashboardUrl; | 106 | string strPoolDashboardUrl; |
| 107 | string strPoolStatusFile; | 107 | string strPoolStatusFile; |
| 108 | string strPoolRoundsFile; | ||
| 108 | double dPoolFeePercent = 0.0; | 109 | double dPoolFeePercent = 0.0; |
| 109 | bool fStratumBridge = false; | 110 | bool fStratumBridge = false; |
| 110 | int nStratumBridgePort = 3333; | 111 | int nStratumBridgePort = 3333; |
| @@ -87,6 +87,7 @@ extern string strParticipantPool; // participant: pool .btf address | |||
| 87 | extern string strPoolName; // operator: announced pool name | 87 | extern string strPoolName; // operator: announced pool name |
| 88 | extern string strPoolDashboardUrl; // operator: optional dashboard URL | 88 | extern string strPoolDashboardUrl; // operator: optional dashboard URL |
| 89 | extern string strPoolStatusFile; // operator: optional JSON status output path | 89 | extern string strPoolStatusFile; // operator: optional JSON status output path |
| 90 | extern string strPoolRoundsFile; // operator: optional public round proof path | ||
| 90 | extern double dPoolFeePercent; // operator: announced fee percent | 91 | extern double dPoolFeePercent; // operator: announced fee percent |
| 91 | extern bool fStratumBridge; // participant helper: local Stratum bridge | 92 | extern bool fStratumBridge; // participant helper: local Stratum bridge |
| 92 | extern int nStratumBridgePort; // local bridge listen port, default 3333 | 93 | extern int nStratumBridgePort; // local bridge listen port, default 3333 |
diff --git a/src/main_gui.cpp b/src/main_gui.cpp index ce6d2b5..7ad2075 100644 --- a/src/main_gui.cpp +++ b/src/main_gui.cpp | |||
| @@ -126,6 +126,7 @@ static void PrintUsage() | |||
| 126 | printf(" /poolfee=PCT\n"); | 126 | printf(" /poolfee=PCT\n"); |
| 127 | printf(" /pooldashboard=URL (alias: /pooldash=URL)\n"); | 127 | printf(" /pooldashboard=URL (alias: /pooldash=URL)\n"); |
| 128 | printf(" /poolstatusfile=PATH (write pool status JSON for dashboards)\n"); | 128 | printf(" /poolstatusfile=PATH (write pool status JSON for dashboards)\n"); |
| 129 | printf(" /poolroundsfile=PATH (write public pool round proofs JSON)\n"); | ||
| 129 | printf("\n"); | 130 | printf("\n"); |
| 130 | printf(".btf and rendezvous:\n"); | 131 | printf(".btf and rendezvous:\n"); |
| 131 | printf(" /connectbtf=PEER_BTF_ADDRESS\n"); | 132 | printf(" /connectbtf=PEER_BTF_ADDRESS\n"); |
| @@ -262,6 +263,10 @@ static void ParseStartupArguments(int argc, char* argv[]) | |||
| 262 | if (!poolStatusFile.empty()) | 263 | if (!poolStatusFile.empty()) |
| 263 | strPoolStatusFile = poolStatusFile; | 264 | strPoolStatusFile = poolStatusFile; |
| 264 | 265 | ||
| 266 | string poolRoundsFile = argval2(argc, argv, "/poolroundsfile", "-poolroundsfile"); | ||
| 267 | if (!poolRoundsFile.empty()) | ||
| 268 | strPoolRoundsFile = poolRoundsFile; | ||
| 269 | |||
| 265 | string btfConnect = argval2(argc, argv, "/connectbtf", "-connectbtf"); | 270 | string btfConnect = argval2(argc, argv, "/connectbtf", "-connectbtf"); |
| 266 | if (!btfConnect.empty()) | 271 | if (!btfConnect.empty()) |
| 267 | strBtfConnect = btfConnect; | 272 | strBtfConnect = btfConnect; |
diff --git a/src/readme.txt b/src/readme.txt index fa58b6e..b8b89c1 100644 --- a/src/readme.txt +++ b/src/readme.txt | |||
| @@ -55,6 +55,10 @@ Command-line options (in addition to the original ones) | |||
| 55 | /poolfee=PCT operator announcement fee percent (example: 0.75) | 55 | /poolfee=PCT operator announcement fee percent (example: 0.75) |
| 56 | /pooldashboard=URL | 56 | /pooldashboard=URL |
| 57 | operator announcement dashboard URL | 57 | operator announcement dashboard URL |
| 58 | /poolstatusfile=PATH | ||
| 59 | write pool status JSON for dashboards | ||
| 60 | /poolroundsfile=PATH | ||
| 61 | write public pool round proofs JSON | ||
| 58 | /connectbtf=ADDR | 62 | /connectbtf=ADDR |
| 59 | keep an outbound .btf connection to ADDR | 63 | keep an outbound .btf connection to ADDR |
| 60 | /rvrelay=HOST:PORT | 64 | /rvrelay=HOST:PORT |
diff --git a/src/rpc.cpp b/src/rpc.cpp index 1aa4f9b..813f1d1 100644 --- a/src/rpc.cpp +++ b/src/rpc.cpp | |||
| @@ -263,11 +263,47 @@ struct PendingPayout { | |||
| 263 | static std::vector<PendingPayout> gPendingPayouts; | 263 | static std::vector<PendingPayout> gPendingPayouts; |
| 264 | static std::mutex gPayoutLedgerMutex; | 264 | static std::mutex gPayoutLedgerMutex; |
| 265 | 265 | ||
| 266 | struct PoolRoundProof { | ||
| 267 | std::string id; | ||
| 268 | int blockHeight; | ||
| 269 | int matureAtHeight; | ||
| 270 | int64 createdAt; | ||
| 271 | std::string blockHash; | ||
| 272 | std::string coinbaseTxid; | ||
| 273 | std::string foundBy; | ||
| 274 | double feePercent; | ||
| 275 | uint64 totalShares; | ||
| 276 | int64 coinbaseValue; | ||
| 277 | int64 subsidy; | ||
| 278 | int64 minerReward; | ||
| 279 | int64 operatorReward; | ||
| 280 | int skippedDust; | ||
| 281 | int64 forfeitedBadAddress; | ||
| 282 | std::map<std::string, uint64> shares; | ||
| 283 | std::map<std::string, int64> payouts; | ||
| 284 | std::map<std::string, std::string> paidTxids; | ||
| 285 | |||
| 286 | PoolRoundProof() | ||
| 287 | : blockHeight(0), matureAtHeight(0), createdAt(0), feePercent(0.0), | ||
| 288 | totalShares(0), coinbaseValue(0), subsidy(0), minerReward(0), operatorReward(0), | ||
| 289 | skippedDust(0), forfeitedBadAddress(0) {} | ||
| 290 | }; | ||
| 291 | static std::vector<PoolRoundProof> gPoolRounds; | ||
| 292 | static std::mutex gPoolRoundsMutex; | ||
| 293 | static const size_t MAX_POOL_ROUNDS = 200; | ||
| 294 | |||
| 266 | static std::string PayoutFilePath() | 295 | static std::string PayoutFilePath() |
| 267 | { | 296 | { |
| 268 | return GetAppDir() + "/pending_payouts.json"; | 297 | return GetAppDir() + "/pending_payouts.json"; |
| 269 | } | 298 | } |
| 270 | 299 | ||
| 300 | static std::string PoolRoundsFilePath() | ||
| 301 | { | ||
| 302 | if (!strPoolRoundsFile.empty()) | ||
| 303 | return strPoolRoundsFile; | ||
| 304 | return GetAppDir() + "/pool_rounds.json"; | ||
| 305 | } | ||
| 306 | |||
| 271 | static std::string PayoutIdForBlock(int blockHeight) | 307 | static std::string PayoutIdForBlock(int blockHeight) |
| 272 | { | 308 | { |
| 273 | return strprintf("block-%d", blockHeight); | 309 | return strprintf("block-%d", blockHeight); |
| @@ -362,6 +398,167 @@ static void LoadPendingPayouts() | |||
| 362 | } | 398 | } |
| 363 | } | 399 | } |
| 364 | 400 | ||
| 401 | static nlohmann::json PoolRoundToJson(const PoolRoundProof& r) | ||
| 402 | { | ||
| 403 | nlohmann::json j; | ||
| 404 | j["id"] = r.id; | ||
| 405 | j["blockHeight"] = r.blockHeight; | ||
| 406 | j["matureAtHeight"] = r.matureAtHeight; | ||
| 407 | j["createdAt"] = r.createdAt; | ||
| 408 | j["blockHash"] = r.blockHash; | ||
| 409 | j["coinbaseTxid"] = r.coinbaseTxid; | ||
| 410 | j["foundBy"] = r.foundBy; | ||
| 411 | j["feePercent"] = r.feePercent; | ||
| 412 | j["totalShares"] = r.totalShares; | ||
| 413 | j["coinbaseValueSatoshis"] = r.coinbaseValue; | ||
| 414 | j["coinbaseValueBtf"] = FormatMoney(r.coinbaseValue); | ||
| 415 | j["subsidySatoshis"] = r.subsidy; | ||
| 416 | j["subsidyBtf"] = FormatMoney(r.subsidy); | ||
| 417 | j["minerRewardSatoshis"] = r.minerReward; | ||
| 418 | j["minerRewardBtf"] = FormatMoney(r.minerReward); | ||
| 419 | j["operatorRewardSatoshis"] = r.operatorReward; | ||
| 420 | j["operatorRewardBtf"] = FormatMoney(r.operatorReward); | ||
| 421 | j["skippedDust"] = r.skippedDust; | ||
| 422 | j["forfeitedBadAddressSatoshis"] = r.forfeitedBadAddress; | ||
| 423 | j["forfeitedBadAddressBtf"] = FormatMoney(r.forfeitedBadAddress); | ||
| 424 | |||
| 425 | j["shares"] = nlohmann::json::object(); | ||
| 426 | for (const auto& kv : r.shares) | ||
| 427 | j["shares"][kv.first] = kv.second; | ||
| 428 | |||
| 429 | j["payouts"] = nlohmann::json::object(); | ||
| 430 | for (const auto& kv : r.payouts) { | ||
| 431 | nlohmann::json p; | ||
| 432 | p["amountSatoshis"] = kv.second; | ||
| 433 | p["amountBtf"] = FormatMoney(kv.second); | ||
| 434 | std::map<std::string, std::string>::const_iterator tx = r.paidTxids.find(kv.first); | ||
| 435 | if (tx != r.paidTxids.end()) | ||
| 436 | p["txid"] = tx->second; | ||
| 437 | else | ||
| 438 | p["txid"] = nullptr; | ||
| 439 | j["payouts"][kv.first] = p; | ||
| 440 | } | ||
| 441 | return j; | ||
| 442 | } | ||
| 443 | |||
| 444 | static void SavePoolRoundsLocked() | ||
| 445 | { | ||
| 446 | nlohmann::json root; | ||
| 447 | root["schema"] = "bitflash-pool-rounds-1"; | ||
| 448 | root["updatedAt"] = GetTime(); | ||
| 449 | root["rounds"] = nlohmann::json::array(); | ||
| 450 | for (const PoolRoundProof& r : gPoolRounds) | ||
| 451 | root["rounds"].push_back(PoolRoundToJson(r)); | ||
| 452 | |||
| 453 | std::string path = PoolRoundsFilePath(); | ||
| 454 | std::string tmp = path + ".tmp"; | ||
| 455 | FILE* f = fopen(tmp.c_str(), "w"); | ||
| 456 | if (!f) { | ||
| 457 | LogPrint("payout", "[payout] WARNING: could not write %s\n", tmp.c_str()); | ||
| 458 | return; | ||
| 459 | } | ||
| 460 | std::string s = root.dump(2); | ||
| 461 | fwrite(s.c_str(), 1, s.size(), f); | ||
| 462 | if (fclose(f) != 0) { | ||
| 463 | LogPrint("payout", "[payout] WARNING: could not close %s\n", tmp.c_str()); | ||
| 464 | return; | ||
| 465 | } | ||
| 466 | #ifdef _WIN32 | ||
| 467 | remove(path.c_str()); | ||
| 468 | #endif | ||
| 469 | if (rename(tmp.c_str(), path.c_str()) != 0) | ||
| 470 | LogPrint("payout", "[payout] WARNING: could not replace %s\n", path.c_str()); | ||
| 471 | } | ||
| 472 | |||
| 473 | static void LoadPoolRounds() | ||
| 474 | { | ||
| 475 | std::string path = PoolRoundsFilePath(); | ||
| 476 | FILE* f = fopen(path.c_str(), "r"); | ||
| 477 | if (!f) return; | ||
| 478 | fseek(f, 0, SEEK_END); | ||
| 479 | long sz = ftell(f); | ||
| 480 | fseek(f, 0, SEEK_SET); | ||
| 481 | std::string s(sz, '\0'); | ||
| 482 | fread(&s[0], 1, sz, f); | ||
| 483 | fclose(f); | ||
| 484 | |||
| 485 | try { | ||
| 486 | nlohmann::json root = nlohmann::json::parse(s); | ||
| 487 | nlohmann::json arr = root.is_array() ? root : root["rounds"]; | ||
| 488 | std::lock_guard<std::mutex> lk(gPoolRoundsMutex); | ||
| 489 | for (const auto& obj : arr) { | ||
| 490 | PoolRoundProof r; | ||
| 491 | if (obj.contains("id") && obj["id"].is_string()) r.id = obj["id"].get<std::string>(); | ||
| 492 | if (obj.contains("blockHeight")) r.blockHeight = obj["blockHeight"].get<int>(); | ||
| 493 | if (obj.contains("matureAtHeight")) r.matureAtHeight = obj["matureAtHeight"].get<int>(); | ||
| 494 | if (obj.contains("createdAt")) r.createdAt = obj["createdAt"].get<int64>(); | ||
| 495 | if (obj.contains("blockHash") && obj["blockHash"].is_string()) r.blockHash = obj["blockHash"].get<std::string>(); | ||
| 496 | if (obj.contains("coinbaseTxid") && obj["coinbaseTxid"].is_string()) r.coinbaseTxid = obj["coinbaseTxid"].get<std::string>(); | ||
| 497 | if (obj.contains("foundBy") && obj["foundBy"].is_string()) r.foundBy = obj["foundBy"].get<std::string>(); | ||
| 498 | if (obj.contains("feePercent")) r.feePercent = obj["feePercent"].get<double>(); | ||
| 499 | if (obj.contains("totalShares")) r.totalShares = obj["totalShares"].get<uint64>(); | ||
| 500 | if (obj.contains("coinbaseValueSatoshis")) r.coinbaseValue = obj["coinbaseValueSatoshis"].get<int64>(); | ||
| 501 | if (obj.contains("subsidySatoshis")) r.subsidy = obj["subsidySatoshis"].get<int64>(); | ||
| 502 | if (obj.contains("minerRewardSatoshis")) r.minerReward = obj["minerRewardSatoshis"].get<int64>(); | ||
| 503 | if (obj.contains("operatorRewardSatoshis")) r.operatorReward = obj["operatorRewardSatoshis"].get<int64>(); | ||
| 504 | if (obj.contains("skippedDust")) r.skippedDust = obj["skippedDust"].get<int>(); | ||
| 505 | if (obj.contains("forfeitedBadAddressSatoshis")) r.forfeitedBadAddress = obj["forfeitedBadAddressSatoshis"].get<int64>(); | ||
| 506 | if (obj.contains("shares") && obj["shares"].is_object()) | ||
| 507 | for (auto it = obj["shares"].begin(); it != obj["shares"].end(); ++it) | ||
| 508 | r.shares[it.key()] = it.value().get<uint64>(); | ||
| 509 | if (obj.contains("payouts") && obj["payouts"].is_object()) | ||
| 510 | for (auto it = obj["payouts"].begin(); it != obj["payouts"].end(); ++it) { | ||
| 511 | if (it.value().is_object()) { | ||
| 512 | r.payouts[it.key()] = it.value().value("amountSatoshis", (int64)0); | ||
| 513 | if (it.value().contains("txid") && it.value()["txid"].is_string()) | ||
| 514 | r.paidTxids[it.key()] = it.value()["txid"].get<std::string>(); | ||
| 515 | } else if (it.value().is_number_integer()) { | ||
| 516 | r.payouts[it.key()] = it.value().get<int64>(); | ||
| 517 | } | ||
| 518 | } | ||
| 519 | if (!r.id.empty()) | ||
| 520 | gPoolRounds.push_back(r); | ||
| 521 | } | ||
| 522 | while (gPoolRounds.size() > MAX_POOL_ROUNDS) | ||
| 523 | gPoolRounds.erase(gPoolRounds.begin()); | ||
| 524 | LogPrint("payout", "[payout] loaded %zu pool round proof(s) from disk\n", | ||
| 525 | gPoolRounds.size()); | ||
| 526 | } catch (const std::exception& e) { | ||
| 527 | LogPrint("payout", "[payout] WARNING: failed to parse %s: %s\n", | ||
| 528 | path.c_str(), e.what()); | ||
| 529 | } | ||
| 530 | } | ||
| 531 | |||
| 532 | static void RecordPoolRound(const PoolRoundProof& round) | ||
| 533 | { | ||
| 534 | std::lock_guard<std::mutex> lk(gPoolRoundsMutex); | ||
| 535 | for (PoolRoundProof& existing : gPoolRounds) { | ||
| 536 | if (existing.id == round.id) { | ||
| 537 | existing = round; | ||
| 538 | SavePoolRoundsLocked(); | ||
| 539 | return; | ||
| 540 | } | ||
| 541 | } | ||
| 542 | gPoolRounds.push_back(round); | ||
| 543 | while (gPoolRounds.size() > MAX_POOL_ROUNDS) | ||
| 544 | gPoolRounds.erase(gPoolRounds.begin()); | ||
| 545 | SavePoolRoundsLocked(); | ||
| 546 | } | ||
| 547 | |||
| 548 | static void MarkPoolRoundPaid(const std::string& id, | ||
| 549 | const std::string& address, | ||
| 550 | const std::string& txid) | ||
| 551 | { | ||
| 552 | std::lock_guard<std::mutex> lk(gPoolRoundsMutex); | ||
| 553 | for (PoolRoundProof& r : gPoolRounds) { | ||
| 554 | if (r.id != id) | ||
| 555 | continue; | ||
| 556 | r.paidTxids[address] = txid; | ||
| 557 | SavePoolRoundsLocked(); | ||
| 558 | return; | ||
| 559 | } | ||
| 560 | } | ||
| 561 | |||
| 365 | // Payouts ready to execute are moved here under gPayoutExecMutex. | 562 | // Payouts ready to execute are moved here under gPayoutExecMutex. |
| 366 | // A dedicated thread drains this list so SendMoney (which takes cs_main) | 563 | // A dedicated thread drains this list so SendMoney (which takes cs_main) |
| 367 | // never runs on the event loop thread and can't stall miner I/O. | 564 | // never runs on the event loop thread and can't stall miner I/O. |
| @@ -399,10 +596,28 @@ static bool FindRecordedPoolPayout(const std::string& id, | |||
| 399 | } | 596 | } |
| 400 | 597 | ||
| 401 | static void QueuePayouts(int blockHeight, | 598 | static void QueuePayouts(int blockHeight, |
| 599 | const std::string& blockHash, | ||
| 600 | const std::string& coinbaseTxid, | ||
| 601 | int64 coinbaseValue, | ||
| 602 | const std::string& foundBy, | ||
| 402 | const std::map<std::string, uint64>& shareCount, | 603 | const std::map<std::string, uint64>& shareCount, |
| 403 | uint64 total) | 604 | uint64 total) |
| 404 | { | 605 | { |
| 606 | PoolRoundProof round; | ||
| 607 | round.id = PayoutIdForBlock(blockHeight); | ||
| 608 | round.blockHeight = blockHeight; | ||
| 609 | round.matureAtHeight = blockHeight + COINBASE_MATURITY; | ||
| 610 | round.createdAt = GetTime(); | ||
| 611 | round.blockHash = blockHash; | ||
| 612 | round.coinbaseTxid = coinbaseTxid; | ||
| 613 | round.coinbaseValue = coinbaseValue; | ||
| 614 | round.foundBy = foundBy; | ||
| 615 | round.feePercent = dPoolFeePercent; | ||
| 616 | round.totalShares = total; | ||
| 617 | round.shares = shareCount; | ||
| 618 | |||
| 405 | if (total == 0) { | 619 | if (total == 0) { |
| 620 | RecordPoolRound(round); | ||
| 406 | LogPrint("payout", "[payout] block %d: zero shares recorded -- nothing to pay out\n", | 621 | LogPrint("payout", "[payout] block %d: zero shares recorded -- nothing to pay out\n", |
| 407 | blockHeight); | 622 | blockHeight); |
| 408 | return; | 623 | return; |
| @@ -418,9 +633,13 @@ static void QueuePayouts(int blockHeight, | |||
| 418 | int64 nSubsidy = 50 * COIN; | 633 | int64 nSubsidy = 50 * COIN; |
| 419 | nSubsidy >>= (blockHeight / 210000); // halving schedule | 634 | nSubsidy >>= (blockHeight / 210000); // halving schedule |
| 420 | int64 minerReward = (int64)((double)nSubsidy * (1.0 - feeFrac)); | 635 | int64 minerReward = (int64)((double)nSubsidy * (1.0 - feeFrac)); |
| 636 | int64 operatorCut = nSubsidy - minerReward; | ||
| 637 | round.subsidy = nSubsidy; | ||
| 638 | round.minerReward = minerReward; | ||
| 639 | round.operatorReward = operatorCut; | ||
| 421 | 640 | ||
| 422 | PendingPayout pp; | 641 | PendingPayout pp; |
| 423 | pp.id = PayoutIdForBlock(blockHeight); | 642 | pp.id = round.id; |
| 424 | pp.blockHeight = blockHeight; | 643 | pp.blockHeight = blockHeight; |
| 425 | pp.matureAtHeight = blockHeight + COINBASE_MATURITY; | 644 | pp.matureAtHeight = blockHeight + COINBASE_MATURITY; |
| 426 | 645 | ||
| @@ -443,15 +662,18 @@ static void QueuePayouts(int blockHeight, | |||
| 443 | continue; | 662 | continue; |
| 444 | } | 663 | } |
| 445 | pp.amounts[kv.first] = amount; | 664 | pp.amounts[kv.first] = amount; |
| 665 | round.payouts[kv.first] = amount; | ||
| 446 | } | 666 | } |
| 667 | round.skippedDust = skipped; | ||
| 668 | round.forfeitedBadAddress = burned; | ||
| 447 | 669 | ||
| 448 | { | 670 | { |
| 449 | std::lock_guard<std::mutex> lk(gPayoutLedgerMutex); | 671 | std::lock_guard<std::mutex> lk(gPayoutLedgerMutex); |
| 450 | gPendingPayouts.push_back(pp); | 672 | gPendingPayouts.push_back(pp); |
| 451 | SavePendingPayoutsLocked(); | 673 | SavePendingPayoutsLocked(); |
| 452 | } | 674 | } |
| 675 | RecordPoolRound(round); | ||
| 453 | 676 | ||
| 454 | int64 operatorCut = nSubsidy - minerReward; | ||
| 455 | LogPrint("payout", | 677 | LogPrint("payout", |
| 456 | "[payout] block %d: subsidy %s BTF, miners get %s" | 678 | "[payout] block %d: subsidy %s BTF, miners get %s" |
| 457 | " (fee %.2f%% = %s to operator)," | 679 | " (fee %.2f%% = %s to operator)," |
| @@ -548,12 +770,14 @@ static void PayoutThreadFn(void*) | |||
| 548 | LogPrint("payout", "[payout] recovered recorded payout %s to %s" | 770 | LogPrint("payout", "[payout] recovered recorded payout %s to %s" |
| 549 | " from wallet tx %s\n", | 771 | " from wallet tx %s\n", |
| 550 | FormatMoney(kv.second).c_str(), kv.first.c_str(), txid.c_str()); | 772 | FormatMoney(kv.second).c_str(), kv.first.c_str(), txid.c_str()); |
| 773 | MarkPoolRoundPaid(ppQueued.id, kv.first, txid); | ||
| 551 | } | 774 | } |
| 552 | else if (SendMoney(sc, kv.second, wtx)) | 775 | else if (SendMoney(sc, kv.second, wtx)) |
| 553 | { | 776 | { |
| 554 | txid = wtx.GetHash().GetHex(); | 777 | txid = wtx.GetHash().GetHex(); |
| 555 | LogPrint("payout", "[payout] paid %s to %s\n", | 778 | LogPrint("payout", "[payout] paid %s to %s\n", |
| 556 | FormatMoney(kv.second).c_str(), kv.first.c_str()); | 779 | FormatMoney(kv.second).c_str(), kv.first.c_str()); |
| 780 | MarkPoolRoundPaid(ppQueued.id, kv.first, txid); | ||
| 557 | } | 781 | } |
| 558 | else | 782 | else |
| 559 | { | 783 | { |
| @@ -664,7 +888,8 @@ static void WritePoolStatusJsonLocked() | |||
| 664 | {"name", strPoolName}, | 888 | {"name", strPoolName}, |
| 665 | {"btfAddress", BtfLocalAddress()}, | 889 | {"btfAddress", BtfLocalAddress()}, |
| 666 | {"feePercent", dPoolFeePercent}, | 890 | {"feePercent", dPoolFeePercent}, |
| 667 | {"dashboardUrl", strPoolDashboardUrl} | 891 | {"dashboardUrl", strPoolDashboardUrl}, |
| 892 | {"roundsFile", "pool_rounds.json"} | ||
| 668 | }; | 893 | }; |
| 669 | 894 | ||
| 670 | j["node"] = { | 895 | j["node"] = { |
| @@ -702,6 +927,30 @@ static void WritePoolStatusJsonLocked() | |||
| 702 | }); | 927 | }); |
| 703 | } | 928 | } |
| 704 | 929 | ||
| 930 | j["recentRounds"] = nlohmann::json::array(); | ||
| 931 | { | ||
| 932 | std::lock_guard<std::mutex> lk(gPoolRoundsMutex); | ||
| 933 | size_t start = gPoolRounds.size() > 10 ? gPoolRounds.size() - 10 : 0; | ||
| 934 | for (size_t i = start; i < gPoolRounds.size(); i++) { | ||
| 935 | const PoolRoundProof& r = gPoolRounds[i]; | ||
| 936 | int unpaid = 0; | ||
| 937 | for (const auto& kv : r.payouts) | ||
| 938 | if (r.paidTxids.count(kv.first) == 0) | ||
| 939 | unpaid++; | ||
| 940 | j["recentRounds"].push_back({ | ||
| 941 | {"id", r.id}, | ||
| 942 | {"blockHeight", r.blockHeight}, | ||
| 943 | {"blockHash", r.blockHash}, | ||
| 944 | {"coinbaseTxid", r.coinbaseTxid}, | ||
| 945 | {"totalShares", r.totalShares}, | ||
| 946 | {"payouts", (int)r.payouts.size()}, | ||
| 947 | {"unpaidPayouts", unpaid}, | ||
| 948 | {"coinbaseValueBtf", FormatMoney(r.coinbaseValue)}, | ||
| 949 | {"minerRewardBtf", FormatMoney(r.minerReward)} | ||
| 950 | }); | ||
| 951 | } | ||
| 952 | } | ||
| 953 | |||
| 705 | std::string path = PoolStatusFilePath(); | 954 | std::string path = PoolStatusFilePath(); |
| 706 | std::string tmp = path + ".tmp"; | 955 | std::string tmp = path + ".tmp"; |
| 707 | FILE* f = fopen(tmp.c_str(), "w"); | 956 | FILE* f = fopen(tmp.c_str(), "w"); |
| @@ -1231,7 +1480,9 @@ static bool HandleLine(Miner* m, const std::string& rawLine, | |||
| 1231 | if (accepted) { | 1480 | if (accepted) { |
| 1232 | // Snapshot height before RebuildJob() (which increments it) | 1481 | // Snapshot height before RebuildJob() (which increments it) |
| 1233 | int foundHeight = gCurrentJob.height; | 1482 | int foundHeight = gCurrentJob.height; |
| 1234 | QueuePayouts(foundHeight, roundShareCount, roundShareTotal); | 1483 | QueuePayouts(foundHeight, b.GetHash().GetHex(), b.vtx[0].GetHash().GetHex(), |
| 1484 | b.vtx[0].vout[0].nValue, m->address, | ||
| 1485 | roundShareCount, roundShareTotal); | ||
| 1235 | // Reset round shares | 1486 | // Reset round shares |
| 1236 | roundShareCount.clear(); | 1487 | roundShareCount.clear(); |
| 1237 | roundShareTotal = 0; | 1488 | roundShareTotal = 0; |
| @@ -1333,6 +1584,62 @@ int RunPoolStratumSelfTest() | |||
| 1333 | HandleLine(&miner, stale.dump(), roundShareCount, roundShareTotal, blocksFound); | 1584 | HandleLine(&miner, stale.dump(), roundShareCount, roundShareTotal, blocksFound); |
| 1334 | check(roundShareTotal == 1, "stale submit does not increment shares"); | 1585 | check(roundShareTotal == 1, "stale submit does not increment shares"); |
| 1335 | 1586 | ||
| 1587 | std::vector<PoolRoundProof> savedRounds = gPoolRounds; | ||
| 1588 | std::string savedRoundsFile = strPoolRoundsFile; | ||
| 1589 | std::string proofPath = "pool_rounds_selftest.json"; | ||
| 1590 | remove(proofPath.c_str()); | ||
| 1591 | remove((proofPath + ".tmp").c_str()); | ||
| 1592 | gPoolRounds.clear(); | ||
| 1593 | strPoolRoundsFile = proofPath; | ||
| 1594 | |||
| 1595 | PoolRoundProof proof; | ||
| 1596 | proof.id = "selftest-round"; | ||
| 1597 | proof.blockHeight = 42; | ||
| 1598 | proof.matureAtHeight = 42 + COINBASE_MATURITY; | ||
| 1599 | proof.createdAt = GetTime(); | ||
| 1600 | proof.blockHash = block.GetHash().GetHex(); | ||
| 1601 | proof.coinbaseTxid = coinbase.GetHash().GetHex(); | ||
| 1602 | proof.foundBy = miner.address; | ||
| 1603 | proof.feePercent = 1.25; | ||
| 1604 | proof.totalShares = 3; | ||
| 1605 | proof.coinbaseValue = 50 * COIN; | ||
| 1606 | proof.subsidy = 50 * COIN; | ||
| 1607 | proof.minerReward = 49 * COIN; | ||
| 1608 | proof.operatorReward = 1 * COIN; | ||
| 1609 | proof.shares[miner.address] = 3; | ||
| 1610 | proof.payouts[miner.address] = 49 * COIN; | ||
| 1611 | RecordPoolRound(proof); | ||
| 1612 | MarkPoolRoundPaid(proof.id, miner.address, "selftest-txid"); | ||
| 1613 | |||
| 1614 | bool proofOk = false; | ||
| 1615 | FILE* pf = fopen(proofPath.c_str(), "r"); | ||
| 1616 | if (pf) { | ||
| 1617 | fseek(pf, 0, SEEK_END); | ||
| 1618 | long sz = ftell(pf); | ||
| 1619 | fseek(pf, 0, SEEK_SET); | ||
| 1620 | std::string s(sz, '\0'); | ||
| 1621 | fread(&s[0], 1, sz, pf); | ||
| 1622 | fclose(pf); | ||
| 1623 | try { | ||
| 1624 | json root = json::parse(s); | ||
| 1625 | proofOk = | ||
| 1626 | root.value("schema", "") == "bitflash-pool-rounds-1" && | ||
| 1627 | root["rounds"].is_array() && | ||
| 1628 | root["rounds"].size() == 1 && | ||
| 1629 | root["rounds"][0].value("id", "") == proof.id && | ||
| 1630 | root["rounds"][0]["shares"].value(miner.address, (uint64)0) == 3 && | ||
| 1631 | root["rounds"][0]["payouts"][miner.address].value("txid", "") == "selftest-txid"; | ||
| 1632 | } catch (...) { | ||
| 1633 | proofOk = false; | ||
| 1634 | } | ||
| 1635 | } | ||
| 1636 | check(proofOk, "pool round proof ledger is written and paid txid is recorded"); | ||
| 1637 | |||
| 1638 | remove(proofPath.c_str()); | ||
| 1639 | remove((proofPath + ".tmp").c_str()); | ||
| 1640 | strPoolRoundsFile = savedRoundsFile; | ||
| 1641 | gPoolRounds = savedRounds; | ||
| 1642 | |||
| 1336 | gCurrentJob = savedJob; | 1643 | gCurrentJob = savedJob; |
| 1337 | gHaveJob = savedHaveJob; | 1644 | gHaveJob = savedHaveJob; |
| 1338 | 1645 | ||
| @@ -1754,6 +2061,7 @@ void ThreadRPCServer(void*) | |||
| 1754 | } | 2061 | } |
| 1755 | 2062 | ||
| 1756 | LoadPendingPayouts(); | 2063 | LoadPendingPayouts(); |
| 2064 | LoadPoolRounds(); | ||
| 1757 | 2065 | ||
| 1758 | _beginthread(AcceptLoopFn, 0, NULL); | 2066 | _beginthread(AcceptLoopFn, 0, NULL); |
| 1759 | _beginthread(ThreadBtfPoolAnnouncer, 0, NULL); | 2067 | _beginthread(ThreadBtfPoolAnnouncer, 0, NULL); |