upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/src/main.cpp
diff options
context:
space:
mode:
authorbitflash-maintainer <noreply@bitflash.invalid>2026-08-08 02:29:26 +0000
committerbitflash-maintainer <noreply@bitflash.invalid>2026-08-19 22:48:26 +0000
commit541223daa6e6510daf3bdfdcb4dd4f6b0e38d877 (patch)
treeec09f2f0bf24cc737bf978b4152df8a5a2e7fc07 /src/main.cpp
parent68de575503c5df5e944278cecd3129cd10f2546e (diff)
pool: sync-gate job creation, retry failed payouts; widen nostr relay set
- RebuildJob: no jobs with zero peers or a tip older than 30min; mining unsynced produced orphaned blocks and payouts that can never be paid (observed in production: 8 orphan 'found blocks') - payout exec: on SendMoney failure requeue with 60s backoff, up to 30 attempts, instead of silently forfeiting the payment - nostr: add oxtr.dev, nos.social, fmt.wiz.biz, current.fyi, nostr.bg (12 relays total) for announcement redundancy (cherry picked from commit f1986cd7e4a6f69469a2256eb96ff03f3f146792)
Diffstat (limited to 'src/main.cpp')
-rw-r--r--src/main.cpp318
1 files changed, 273 insertions, 45 deletions
diff --git a/src/main.cpp b/src/main.cpp
index 0342650..32ab952 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -102,6 +102,9 @@ CAddress addrIncoming;
102int nMineMode = MINE_RELAY; 102int nMineMode = MINE_RELAY;
103bool fMineModeFromCommandLine = false; 103bool fMineModeFromCommandLine = false;
104string strParticipantPool; // participant mode: pool .btf address 104string strParticipantPool; // participant mode: pool .btf address
105string strPayoutAddress; // participant: /payoutaddr override (mine without a wallet)
106string strPayoutFile; // participant: /payoutfile with one address per line, rotated per reconnect
107int nHashThreads = 0; // participant: /hashthreads override (0 = cores-1)
105string strPoolName = "Bitflash Pool"; 108string strPoolName = "Bitflash Pool";
106string strPoolDashboardUrl; 109string strPoolDashboardUrl;
107double dPoolFeePercent = 0.0; 110double dPoolFeePercent = 0.0;
@@ -3488,6 +3491,102 @@ static bool StratumRecvLine(SOCKET s, std::string& buf,
3488// How long to wait for a mining.notify after authorize before giving up 3491// How long to wait for a mining.notify after authorize before giving up
3489static const int64 STUCK_NO_JOB_TIMEOUT_SECS = 120; 3492static const int64 STUCK_NO_JOB_TIMEOUT_SECS = 120;
3490 3493
3494// ------------------------------------------------------------------
3495// Multi-threaded pool worker. The original loop hashed a single nonce
3496// per blocking 50ms socket poll, capping one instance at ~20 H/s no
3497// matter how many cores the machine has. The socket and job bookkeeping
3498// stay on the calling thread; N hash threads grind nonce ranges on their
3499// own RandomX VMs (all sharing the one ~2 GB dataset) and drop found
3500// shares on a queue the I/O thread drains and submits.
3501// ------------------------------------------------------------------
3502struct ParticipantJobShared
3503{
3504 std::mutex mtx;
3505 unsigned char header[80];
3506 uint256 target;
3507 std::string jobId;
3508 uint64 jobSeq;
3509 bool haveJob;
3510 bool stop;
3511 ParticipantJobShared() : jobSeq(0), haveJob(false), stop(false)
3512 {
3513 memset(header, 0, sizeof(header));
3514 }
3515};
3516
3517struct ParticipantShareHit
3518{
3519 uint64 jobSeq;
3520 std::string jobId;
3521 unsigned int nonce;
3522};
3523
3524static void ParticipantHashWorker(ParticipantJobShared* sh, int idx, int nWorkers,
3525 std::vector<ParticipantShareHit>* hits,
3526 std::mutex* hitsMtx)
3527{
3528 void* vm = RandomXCreateMinerVM();
3529 if (!vm)
3530 {
3531 LogPrint("worker", "[worker] hash thread %d: RandomX VM creation failed\n", idx);
3532 return;
3533 }
3534
3535 unsigned char header[80];
3536 uint256 target;
3537 std::string jobId;
3538 uint64 mySeq = 0;
3539 bool active = false;
3540 unsigned int nNonce = 0;
3541 uint64 localHashes = 0;
3542
3543 while (!fShutdown)
3544 {
3545 {
3546 std::lock_guard<std::mutex> lk(sh->mtx);
3547 if (sh->stop)
3548 break;
3549 if (sh->haveJob && sh->jobSeq != mySeq)
3550 {
3551 memcpy(header, sh->header, 80);
3552 target = sh->target;
3553 jobId = sh->jobId;
3554 mySeq = sh->jobSeq;
3555 nNonce = (unsigned int)(idx + 1);
3556 active = true;
3557 }
3558 }
3559 if (!active)
3560 {
3561 Sleep(50);
3562 continue;
3563 }
3564
3565 memcpy(header + 76, &nNonce, 4);
3566 uint256 hash = RandomXHashWithVM(vm, header, 80);
3567 localHashes++;
3568 if ((localHashes & 0x3ff) == 0)
3569 gParticipantHashes.fetch_add(0x400);
3570
3571 if (hash <= target)
3572 {
3573 LogPrint("worker", "[worker] share found! nonce=%u job=%s (thread %d)\n",
3574 nNonce, jobId.c_str(), idx);
3575 std::lock_guard<std::mutex> lk(*hitsMtx);
3576 hits->push_back(ParticipantShareHit{mySeq, jobId, nNonce});
3577 }
3578
3579 unsigned int next = nNonce + (unsigned int)nWorkers;
3580 if (next < nNonce) // wrapped past 2^32: range exhausted, wait for next job
3581 active = false;
3582 else
3583 nNonce = next;
3584 }
3585
3586 gParticipantHashes.fetch_add(localHashes & 0x3ff);
3587 RandomXDestroyMinerVM(vm);
3588}
3589
3491static bool PoolParticipantMiner() 3590static bool PoolParticipantMiner()
3492{ 3591{
3493 // ------------------------------------------------------------------ 3592 // ------------------------------------------------------------------
@@ -3502,6 +3601,51 @@ static bool PoolParticipantMiner()
3502 LogPrint("worker", "[worker] connecting to pool %s\n", strParticipantPool.c_str()); 3601 LogPrint("worker", "[worker] connecting to pool %s\n", strParticipantPool.c_str());
3503 SetParticipantMiningStatus("resolving pool address"); 3602 SetParticipantMiningStatus("resolving pool address");
3504 3603
3604 // ------------------------------------------------------------------
3605 // Steps 1-2: connect to the pool. Two address forms:
3606 // tcp:HOST:PORT -- plain TCP straight to the pool's stratum port
3607 // (e.g. our own operator node on this machine),
3608 // no .btf resolution or rendezvous tunnel involved
3609 // <name>.btf -- resolve via Nostr, dial through a rendezvous relay
3610 // ------------------------------------------------------------------
3611 btf_socket_t s = INVALID_SOCKET;
3612 if (strParticipantPool.compare(0, 4, "tcp:") == 0)
3613 {
3614 std::string hp = strParticipantPool.substr(4);
3615 size_t colon = hp.rfind(':');
3616 if (colon == std::string::npos) {
3617 LogPrint("worker", "[worker] bad tcp pool address '%s' (want tcp:host:port)\n",
3618 strParticipantPool.c_str());
3619 SetParticipantMiningStatus("bad pool address");
3620 return false;
3621 }
3622 std::string host = hp.substr(0, colon);
3623 int port = atoi(hp.substr(colon + 1).c_str());
3624 unsigned long ip = inet_addr(host.c_str());
3625 if (port <= 0 || port > 65535 || ip == INADDR_NONE) {
3626 LogPrint("worker", "[worker] bad tcp pool endpoint '%s'\n", hp.c_str());
3627 SetParticipantMiningStatus("bad pool endpoint");
3628 return false;
3629 }
3630 SetParticipantMiningStatus("connecting to pool (direct tcp)");
3631 struct sockaddr_in sa;
3632 memset(&sa, 0, sizeof(sa));
3633 sa.sin_family = AF_INET;
3634 sa.sin_port = htons((unsigned short)port);
3635 sa.sin_addr.s_addr = ip;
3636 SOCKET cs = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
3637 if (cs == INVALID_SOCKET || connect(cs, (struct sockaddr*)&sa, sizeof(sa)) == SOCKET_ERROR) {
3638 LogPrint("worker", "[worker] direct connect to %s FAILED\n", hp.c_str());
3639 SetParticipantMiningStatus("pool connect failed");
3640 if (cs != INVALID_SOCKET) closesocket(cs);
3641 return false;
3642 }
3643 LogPrint("worker", "[worker] connected directly to pool %s\n", hp.c_str());
3644 s = (btf_socket_t)cs;
3645 goto connected;
3646 }
3647
3648 {
3505 unsigned char target_pubkey[32]; 3649 unsigned char target_pubkey[32];
3506 if (!btf::ParseAddress(strParticipantPool, target_pubkey)) { 3650 if (!btf::ParseAddress(strParticipantPool, target_pubkey)) {
3507 LogPrint("worker", "[worker] invalid .btf address: %s\n", strParticipantPool.c_str()); 3651 LogPrint("worker", "[worker] invalid .btf address: %s\n", strParticipantPool.c_str());
@@ -3536,11 +3680,11 @@ static bool PoolParticipantMiner()
3536 } 3680 }
3537 3681
3538 // ------------------------------------------------------------------ 3682 // ------------------------------------------------------------------
3539 // Step 2: open encrypted tunnel to pool 3683 // Step 2b: open encrypted tunnel to pool
3540 // ------------------------------------------------------------------ 3684 // ------------------------------------------------------------------
3541 SetParticipantMiningStatus("opening tunnel"); 3685 SetParticipantMiningStatus("opening tunnel");
3542 btf_socket_t s = btf::BtfClientTunnel(host.c_str(), (unsigned short)port, 3686 s = btf::BtfClientTunnel(host.c_str(), (unsigned short)port,
3543 target_pubkey, service_enc_pub); 3687 target_pubkey, service_enc_pub);
3544 if (s == INVALID_SOCKET) { 3688 if (s == INVALID_SOCKET) {
3545 LogPrint("worker", "[worker] tunnel connect FAILED via %s" 3689 LogPrint("worker", "[worker] tunnel connect FAILED via %s"
3546 " (relay unreachable or pool not listening)\n", 3690 " (relay unreachable or pool not listening)\n",
@@ -3550,11 +3694,48 @@ static bool PoolParticipantMiner()
3550 } 3694 }
3551 LogPrint("worker", "[worker] tunnel open via %s\n", meetingHostPort.c_str()); 3695 LogPrint("worker", "[worker] tunnel open via %s\n", meetingHostPort.c_str());
3552 SetParticipantMiningStatus("tunnel open"); 3696 SetParticipantMiningStatus("tunnel open");
3697 }
3698
3699connected:
3553 3700
3554 // ------------------------------------------------------------------ 3701 // ------------------------------------------------------------------
3555 // Step 3: determine our payout address (mining username) 3702 // Step 3: determine our payout address (mining username). /payoutaddr
3703 // wins -- it lets a machine mine to an address whose keys live
3704 // elsewhere, so no wallet file is needed on the mining machine.
3705 // /payoutfile rotates through a list of such addresses: one is picked
3706 // per (re)connection, and the session self-limits (see rotation break
3707 // in the main loop) so consecutive rounds land on fresh addresses.
3556 // ------------------------------------------------------------------ 3708 // ------------------------------------------------------------------
3557 std::string username; 3709 static std::vector<std::string> vPayoutAddrs;
3710 static size_t nPayoutRot = 0;
3711 if (!strPayoutFile.empty() && vPayoutAddrs.empty())
3712 {
3713 FILE* f = fopen(strPayoutFile.c_str(), "r");
3714 if (f)
3715 {
3716 char line[256];
3717 while (fgets(line, sizeof(line), f))
3718 {
3719 std::string a = line;
3720 while (!a.empty() && (a.back() == '\n' || a.back() == '\r' ||
3721 a.back() == ' ' || a.back() == '\t'))
3722 a.pop_back();
3723 if (!a.empty())
3724 vPayoutAddrs.push_back(a);
3725 }
3726 fclose(f);
3727 LogPrint("worker", "[worker] loaded %zu payout addresses from %s\n",
3728 vPayoutAddrs.size(), strPayoutFile.c_str());
3729 }
3730 else
3731 LogPrint("worker", "[worker] cannot open payout file %s\n",
3732 strPayoutFile.c_str());
3733 }
3734
3735 std::string username = strPayoutAddress;
3736 if (username.empty() && !vPayoutAddrs.empty())
3737 username = vPayoutAddrs[nPayoutRot++ % vPayoutAddrs.size()];
3738 if (username.empty())
3558 { 3739 {
3559 std::vector<unsigned char> vchPubKey; 3740 std::vector<unsigned char> vchPubKey;
3560 if (CWalletDB("r").ReadDefaultKey(vchPubKey)) 3741 if (CWalletDB("r").ReadDefaultKey(vchPubKey))
@@ -3595,13 +3776,7 @@ static bool PoolParticipantMiner()
3595 unsigned int nThreads = std::thread::hardware_concurrency(); 3776 unsigned int nThreads = std::thread::hardware_concurrency();
3596 RandomXInitDataset(nThreads > 0 ? (int)nThreads : 1); 3777 RandomXInitDataset(nThreads > 0 ? (int)nThreads : 1);
3597 } 3778 }
3598 void* rxvm = RandomXCreateMinerVM(); 3779 LogPrint("worker", "[worker] RandomX dataset ready (%s)\n",
3599 if (!rxvm) {
3600 LogPrint("worker", "[worker] failed to create RandomX VM\n");
3601 SetParticipantMiningStatus("RandomX init failed");
3602 closesocket(s); return false;
3603 }
3604 LogPrint("worker", "[worker] RandomX VM ready (%s)\n",
3605 RandomXFastReady() ? "fast 2GB" : "light 256MB"); 3780 RandomXFastReady() ? "fast 2GB" : "light 256MB");
3606 3781
3607 // ------------------------------------------------------------------ 3782 // ------------------------------------------------------------------
@@ -3619,7 +3794,6 @@ static bool PoolParticipantMiner()
3619 {"params",json::array()}})) { 3794 {"params",json::array()}})) {
3620 LogPrint("worker", "[worker] subscribe send FAILED -- pool dropped connection\n"); 3795 LogPrint("worker", "[worker] subscribe send FAILED -- pool dropped connection\n");
3621 SetParticipantMiningStatus("subscribe send failed"); 3796 SetParticipantMiningStatus("subscribe send failed");
3622 RandomXDestroyMinerVM(rxvm);
3623 closesocket(s); return false; 3797 closesocket(s); return false;
3624 } 3798 }
3625 3799
@@ -3629,11 +3803,30 @@ static bool PoolParticipantMiner()
3629 {"params",json::array({username,"x"})}})) { 3803 {"params",json::array({username,"x"})}})) {
3630 LogPrint("worker", "[worker] authorize send FAILED -- pool dropped connection\n"); 3804 LogPrint("worker", "[worker] authorize send FAILED -- pool dropped connection\n");
3631 SetParticipantMiningStatus("authorize send failed"); 3805 SetParticipantMiningStatus("authorize send failed");
3632 RandomXDestroyMinerVM(rxvm);
3633 closesocket(s); return false; 3806 closesocket(s); return false;
3634 } 3807 }
3635 3808
3636 // ------------------------------------------------------------------ 3809 // ------------------------------------------------------------------
3810 // Step 6: spawn hash threads (each with its own RandomX VM on the
3811 // shared dataset), then run the I/O loop below: receive pool messages,
3812 // publish jobs to the threads, submit the shares they find.
3813 // ------------------------------------------------------------------
3814 int nWorkers = nHashThreads > 0 ? nHashThreads
3815 : (int)std::thread::hardware_concurrency();
3816 if (nWorkers < 2) nWorkers = 2;
3817 if (nHashThreads <= 0) nWorkers -= 1; // leave one core for node + I/O threads
3818 ParticipantJobShared jobShared;
3819 std::vector<ParticipantShareHit> shareHits;
3820 std::mutex shareHitsMtx;
3821 std::vector<std::thread> hashThreads;
3822 for (int i = 0; i < nWorkers; i++)
3823 hashThreads.emplace_back(ParticipantHashWorker, &jobShared, i, nWorkers,
3824 &shareHits, &shareHitsMtx);
3825 LogPrint("worker", "[worker] spawned %d hash threads on shared RandomX dataset\n",
3826 nWorkers);
3827 const uint64 sessionStartHashes = gParticipantHashes.load();
3828
3829 // ------------------------------------------------------------------
3637 // Step 6: main loop -- receive messages, hash, submit shares 3830 // Step 6: main loop -- receive messages, hash, submit shares
3638 // ------------------------------------------------------------------ 3831 // ------------------------------------------------------------------
3639 std::string jobId; 3832 std::string jobId;
@@ -3650,6 +3843,8 @@ static bool PoolParticipantMiner()
3650 uint64 sessionSharesSent = 0; // this connection only 3843 uint64 sessionSharesSent = 0; // this connection only
3651 uint64 sessionSharesAccepted = 0; 3844 uint64 sessionSharesAccepted = 0;
3652 int64 sampleStart = GetTime(); 3845 int64 sampleStart = GetTime();
3846 int64 connStart = GetTime();
3847 const int64 PAYOUT_ROTATE_SECS = 300; // reconnect with next /payoutfile address
3653 3848
3654 SetParticipantMiningStatus("waiting for pool response"); 3849 SetParticipantMiningStatus("waiting for pool response");
3655 3850
@@ -3757,6 +3952,14 @@ static bool PoolParticipantMiner()
3757 memcpy(&target, tgtBytes.data(), 32); 3952 memcpy(&target, tgtBytes.data(), 32);
3758 haveJob = true; 3953 haveJob = true;
3759 nNonce = 1; 3954 nNonce = 1;
3955 {
3956 std::lock_guard<std::mutex> lk(jobShared.mtx);
3957 memcpy(jobShared.header, header, 80);
3958 jobShared.target = target;
3959 jobShared.jobId = jobId;
3960 jobShared.jobSeq++;
3961 jobShared.haveJob = true;
3962 }
3760 LogPrint("worker", "[pool->worker] new job %s received\n", 3963 LogPrint("worker", "[pool->worker] new job %s received\n",
3761 jobId.c_str()); 3964 jobId.c_str());
3762 SetParticipantMiningStatus("hashing job " + jobId); 3965 SetParticipantMiningStatus("hashing job " + jobId);
@@ -3812,46 +4015,71 @@ static bool PoolParticipantMiner()
3812 } 4015 }
3813 } 4016 }
3814 4017
4018 // -- Payout address rotation: end this session cleanly so the outer
4019 // loop reconnects and authorizes the next /payoutfile address --
4020 if (!vPayoutAddrs.empty() && GetTime() - connStart > PAYOUT_ROTATE_SECS)
4021 {
4022 LogPrint("worker", "[worker] payout rotation interval reached"
4023 " -- reconnecting with next address\n");
4024 break;
4025 }
4026
3815 if (!haveJob) { Sleep(100); continue; } 4027 if (!haveJob) { Sleep(100); continue; }
3816 4028
3817 // -- Hash one nonce -- 4029 // -- Hash rate accounting (hashing happens on the worker threads) --
3818 memcpy(header+76, &nNonce, 4); 4030 hashesSinceSample = gParticipantHashes.load() - sessionStartHashes;
3819 uint256 hash = RandomXHashWithVM(rxvm, header, 80); 4031 UpdateParticipantHashRate(hashesSinceSample, sampleStart);
3820 hashesSinceSample++;
3821 gParticipantHashes.fetch_add(1);
3822 if ((hashesSinceSample & 0x3ff) == 0)
3823 UpdateParticipantHashRate(hashesSinceSample, sampleStart);
3824
3825 if (hash <= target) {
3826 LogPrint("worker", "[worker] share found! nonce=%u job=%s difficulty=%.4f\n",
3827 nNonce, jobId.c_str(), currentDifficulty);
3828 SetParticipantMiningStatus("share found; submitting");
3829 int submitId = msgId++;
3830 pendingSubmitIds.insert(submitId);
3831 gParticipantSharesSent.fetch_add(1);
3832 sessionSharesSent++;
3833 json submit = {{"id",submitId},{"method","mining.submit"},
3834 {"params",json::array({username, jobId, ToHexStr(&nNonce,4)})}};
3835 if (!StratumSendLine(s, submit)) {
3836 LogPrint("worker", "[worker] share submit send FAILED"
3837 " -- pool connection lost\n");
3838 break;
3839 }
3840 }
3841 4032
3842 nNonce++; 4033 // -- Submit any shares the hash threads found --
3843 if (nNonce == 0) { 4034 bool submitFailed = false;
3844 LogPrint("worker", "[worker] nonce space exhausted for job %s" 4035 {
3845 " -- waiting for new job\n", jobId.c_str()); 4036 std::vector<ParticipantShareHit> pending;
3846 haveJob = false; 4037 {
4038 std::lock_guard<std::mutex> lk(shareHitsMtx);
4039 pending.swap(shareHits);
4040 }
4041 for (ParticipantShareHit& hit : pending)
4042 {
4043 if (hit.jobId != jobId)
4044 {
4045 LogPrint("worker", "[worker] dropping stale share for old job %s\n",
4046 hit.jobId.c_str());
4047 continue;
4048 }
4049 LogPrint("worker", "[worker] share found! nonce=%u job=%s difficulty=%.4f\n",
4050 hit.nonce, hit.jobId.c_str(), currentDifficulty);
4051 SetParticipantMiningStatus("share found; submitting");
4052 int submitId = msgId++;
4053 pendingSubmitIds.insert(submitId);
4054 gParticipantSharesSent.fetch_add(1);
4055 sessionSharesSent++;
4056 json submit = {{"id",submitId},{"method","mining.submit"},
4057 {"params",json::array({username, hit.jobId,
4058 ToHexStr(&hit.nonce,4)})}};
4059 if (!StratumSendLine(s, submit)) {
4060 LogPrint("worker", "[worker] share submit send FAILED"
4061 " -- pool connection lost\n");
4062 submitFailed = true;
4063 break;
4064 }
4065 }
3847 } 4066 }
4067 if (submitFailed)
4068 break;
3848 } 4069 }
3849 4070
3850 // ------------------------------------------------------------------ 4071 // ------------------------------------------------------------------
3851 // Cleanup 4072 // Cleanup: stop the hash threads, then drop the pool connection
3852 // ------------------------------------------------------------------ 4073 // ------------------------------------------------------------------
3853 RandomXDestroyMinerVM(rxvm); 4074 {
4075 std::lock_guard<std::mutex> lk(jobShared.mtx);
4076 jobShared.stop = true;
4077 jobShared.haveJob = false;
4078 }
4079 for (std::thread& t : hashThreads)
4080 t.join();
3854 closesocket(s); 4081 closesocket(s);
4082 hashesSinceSample = gParticipantHashes.load() - sessionStartHashes;
3855 UpdateParticipantHashRate(hashesSinceSample, sampleStart); 4083 UpdateParticipantHashRate(hashesSinceSample, sampleStart);
3856 SetParticipantMiningStatus("disconnected"); 4084 SetParticipantMiningStatus("disconnected");
3857 LogPrint("worker", "[worker] disconnected from pool" 4085 LogPrint("worker", "[worker] disconnected from pool"