upleb.uk

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

summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/btfrv.cpp41
-rw-r--r--src/main.cpp318
-rw-r--r--src/main.h3
-rw-r--r--src/main_gui.cpp40
-rw-r--r--src/net.cpp36
-rw-r--r--src/nostr.cpp5
-rw-r--r--src/rpc.cpp95
7 files changed, 471 insertions, 67 deletions
diff --git a/src/btfrv.cpp b/src/btfrv.cpp
index 6ac3280..905717f 100644
--- a/src/btfrv.cpp
+++ b/src/btfrv.cpp
@@ -94,31 +94,42 @@ static SOCKET ConnectTo(const char* host, unsigned short port)
94{ 94{
95 struct addrinfo hints, *res = NULL; 95 struct addrinfo hints, *res = NULL;
96 memset(&hints, 0, sizeof(hints)); 96 memset(&hints, 0, sizeof(hints));
97 hints.ai_family = AF_INET; 97 hints.ai_family = AF_UNSPEC; // IPv4 or IPv6 (some relays are v6-only)
98 hints.ai_socktype = SOCK_STREAM; 98 hints.ai_socktype = SOCK_STREAM;
99 char portstr[16]; sprintf(portstr, "%u", (unsigned)port); 99 char portstr[16]; sprintf(portstr, "%u", (unsigned)port);
100 if (getaddrinfo(host, portstr, &hints, &res) != 0 || !res) return INVALID_SOCKET; 100 if (getaddrinfo(host, portstr, &hints, &res) != 0 || !res) return INVALID_SOCKET;
101 SOCKET s = BtfSocketTag(socket(res->ai_family, res->ai_socktype, res->ai_protocol), SOCK_RV_DIAL);
102 if (s == INVALID_SOCKET) { freeaddrinfo(res); return INVALID_SOCKET; }
103 101
104 // Apply a short timeout only for the connect() and initial handshake so an 102 // Try each resolved address until one connects.
105 // unresponsive relay doesn't block the thread. Once the tunnel is live the 103 SOCKET s = INVALID_SOCKET;
106 // timeout is cleared -- a forwarding pipe must block indefinitely on recv(). 104 for (struct addrinfo* rp = res; rp != NULL; rp = rp->ai_next)
105 {
106 s = BtfSocketTag(socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol), SOCK_RV_DIAL);
107 if (s == INVALID_SOCKET)
108 continue;
109
110 // Apply a short timeout only for the connect() and initial handshake so an
111 // unresponsive relay doesn't block the thread. Once the tunnel is live the
112 // timeout is cleared -- a forwarding pipe must block indefinitely on recv().
107#ifdef _WIN32 113#ifdef _WIN32
108 DWORD tvShort = 10000; 114 DWORD tvShort = 10000;
109 setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, (const char*)&tvShort, sizeof(tvShort)); 115 setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, (const char*)&tvShort, sizeof(tvShort));
110 setsockopt(s, SOL_SOCKET, SO_SNDTIMEO, (const char*)&tvShort, sizeof(tvShort)); 116 setsockopt(s, SOL_SOCKET, SO_SNDTIMEO, (const char*)&tvShort, sizeof(tvShort));
111#else 117#else
112 struct timeval tvShort; tvShort.tv_sec = 10; tvShort.tv_usec = 0; 118 struct timeval tvShort; tvShort.tv_sec = 10; tvShort.tv_usec = 0;
113 setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, &tvShort, sizeof(tvShort)); 119 setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, (char*)&tvShort, sizeof(tvShort));
114 setsockopt(s, SOL_SOCKET, SO_SNDTIMEO, &tvShort, sizeof(tvShort)); 120 setsockopt(s, SOL_SOCKET, SO_SNDTIMEO, (char*)&tvShort, sizeof(tvShort));
115#endif 121#endif
116 122
117 if (connect(s, res->ai_addr, (int)res->ai_addrlen) != 0) 123 if (connect(s, rp->ai_addr, (int)rp->ai_addrlen) != 0)
118 { 124 {
119 CLOSESOCK(s); freeaddrinfo(res); return INVALID_SOCKET; 125 CLOSESOCK(s);
126 s = INVALID_SOCKET;
127 continue;
128 }
129 break;
120 } 130 }
121 freeaddrinfo(res); 131 freeaddrinfo(res);
132 if (s == INVALID_SOCKET) return INVALID_SOCKET;
122 133
123 // Clear the timeout -- the socket is now a live tunnel and must not 134 // Clear the timeout -- the socket is now a live tunnel and must not
124 // time out when idle. recv() will block until data arrives or peer closes. 135 // time out when idle. recv() will block until data arrives or peer closes.
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"
diff --git a/src/main.h b/src/main.h
index c458412..307cee5 100644
--- a/src/main.h
+++ b/src/main.h
@@ -84,6 +84,9 @@ extern int nMineMode;
84// something a previous session wrote. 84// something a previous session wrote.
85extern bool fMineModeFromCommandLine; 85extern bool fMineModeFromCommandLine;
86extern string strParticipantPool; // participant: pool .btf address 86extern string strParticipantPool; // participant: pool .btf address
87extern string strPayoutAddress; // participant: /payoutaddr override (mine without a wallet)
88extern string strPayoutFile; // participant: /payoutfile address list, rotated per reconnect
89extern int nHashThreads; // participant: /hashthreads override (0 = cores-1)
87extern string strPoolName; // operator: announced pool name 90extern string strPoolName; // operator: announced pool name
88extern string strPoolDashboardUrl; // operator: optional dashboard URL 91extern string strPoolDashboardUrl; // operator: optional dashboard URL
89extern double dPoolFeePercent; // operator: announced fee percent 92extern double dPoolFeePercent; // operator: announced fee percent
diff --git a/src/main_gui.cpp b/src/main_gui.cpp
index 875988f..0b4e091 100644
--- a/src/main_gui.cpp
+++ b/src/main_gui.cpp
@@ -115,6 +115,9 @@ static void PrintUsage()
115 printf("Mining mode:\n"); 115 printf("Mining mode:\n");
116 printf(" /operator\n"); 116 printf(" /operator\n");
117 printf(" /participant=POOL_BTF_ADDRESS\n"); 117 printf(" /participant=POOL_BTF_ADDRESS\n");
118 printf(" /payoutaddr=BTF_ADDRESS (participant: pay shares to this address, no wallet needed)\n");
119 printf(" /payoutfile=FILE (participant: rotate payout addresses from file)\n");
120 printf(" /hashthreads=N (participant: mining threads, default cores-1)\n");
118 printf(" /solomine\n"); 121 printf(" /solomine\n");
119 printf(" /genproclimit=N (mining threads; 0 or absent = every core but one)\n"); 122 printf(" /genproclimit=N (mining threads; 0 or absent = every core but one)\n");
120 printf(" /checkblocks=N (blocks re-verified at startup, default 288, 0 = all)\n"); 123 printf(" /checkblocks=N (blocks re-verified at startup, default 288, 0 = all)\n");
@@ -225,6 +228,18 @@ static void ParseStartupArguments(int argc, char* argv[])
225 fMineModeFromCommandLine = true; 228 fMineModeFromCommandLine = true;
226 } 229 }
227 230
231 string payoutAddr = argval2(argc, argv, "/payoutaddr", "-payoutaddr");
232 if (!payoutAddr.empty())
233 strPayoutAddress = payoutAddr;
234
235 string payoutFile = argval2(argc, argv, "/payoutfile", "-payoutfile");
236 if (!payoutFile.empty())
237 strPayoutFile = payoutFile;
238
239 string hashThreads = argval2(argc, argv, "/hashthreads", "-hashthreads");
240 if (!hashThreads.empty())
241 nHashThreads = atoi(hashThreads.c_str());
242
228 string poolName = argval2(argc, argv, "/poolname", "-poolname"); 243 string poolName = argval2(argc, argv, "/poolname", "-poolname");
229 if (!poolName.empty()) 244 if (!poolName.empty())
230 strPoolName = poolName; 245 strPoolName = poolName;
@@ -610,6 +625,31 @@ int main(int argc, char* argv[])
610 625
611 ReacceptWalletTransactions(); 626 ReacceptWalletTransactions();
612 627
628 // /dumpaddresses=N:FILE -- generate N fresh wallet keys and write their
629 // addresses (one per line) to FILE, then exit. Keys stay in this wallet;
630 // the file alone is enough for a miner elsewhere to rotate payout
631 // addresses via /payoutfile. (No HD/xpub exists in this wallet format.)
632 string dumpSpec = argval2(argc, argv, "/dumpaddresses", "-dumpaddresses");
633 if (!dumpSpec.empty())
634 {
635 size_t colon = dumpSpec.rfind(':');
636 if (colon == string::npos) { fprintf(stderr, "usage: /dumpaddresses=N:FILE\n"); return 1; }
637 int n = atoi(dumpSpec.substr(0, colon).c_str());
638 string file = dumpSpec.substr(colon + 1);
639 FILE* f = fopen(file.c_str(), "w");
640 if (!f) { fprintf(stderr, "cannot open %s\n", file.c_str()); return 1; }
641 for (int i = 0; i < n; i++)
642 {
643 CKey key;
644 key.MakeNewKey();
645 if (!AddKey(key)) { fclose(f); fprintf(stderr, "AddKey failed at %d\n", i); return 1; }
646 fprintf(f, "%s\n", PubKeyToAddress(key.GetPubKey()).c_str());
647 }
648 fclose(f);
649 printf("wrote %d fresh wallet addresses to %s\n", n, file.c_str());
650 return 0;
651 }
652
613 if (!StartNode(strErrors)) { fprintf(stderr,"StartNode: %s\n",strErrors.c_str()); return 1; } 653 if (!StartNode(strErrors)) { fprintf(stderr,"StartNode: %s\n",strErrors.c_str()); return 1; }
614 654
615 if (nMineMode == MINE_OPERATOR) { 655 if (nMineMode == MINE_OPERATOR) {
diff --git a/src/net.cpp b/src/net.cpp
index 449d91c..5cf2f8c 100644
--- a/src/net.cpp
+++ b/src/net.cpp
@@ -1054,6 +1054,18 @@ CNode* ConnectNodeBtfResolved(const string& strBtfAddr, const string& strMeeting
1054void ThreadBtfAccept(void* parg) 1054void ThreadBtfAccept(void* parg)
1055{ 1055{
1056 printf("ThreadBtfAccept started\n"); 1056 printf("ThreadBtfAccept started\n");
1057 // In operator mode the pool server registers this same node identity as a
1058 // stratum service at the rendezvous relays, and a relay only holds ONE
1059 // service registration per pubkey -- peer-accept and pool-accept would
1060 // kick each other off forever, breaking both. The pool's registration is
1061 // the one that must be stable (that's what miners resolve and dial), so
1062 // the node skips inbound rendezvous here and relies on outbound .btf
1063 // connections for block sync.
1064 if (nMineMode == MINE_OPERATOR)
1065 {
1066 printf("ThreadBtfAccept: operator mode -- skipping peer rendezvous registration\n");
1067 return;
1068 }
1057 unsigned char pk[32]; 1069 unsigned char pk[32];
1058 unsigned char sk[32]; 1070 unsigned char sk[32];
1059 while (!BtfGetIdentity(pk, sk)) 1071 while (!BtfGetIdentity(pk, sk))
@@ -1928,11 +1940,29 @@ bool StartNode(string& strError)
1928 { 1940 {
1929 int nErr = WSAGetLastError(); 1941 int nErr = WSAGetLastError();
1930 if (nErr == WSAEADDRINUSE) 1942 if (nErr == WSAEADDRINUSE)
1931 strError = strprintf("Error: Unable to bind to port %s on this computer. The program is probably already running.", addrLocalHost.ToString().c_str()); 1943 {
1944 // Another local instance holds the default port. Fall back to an
1945 // ephemeral port: inbound direct-IP peering is optional, and .btf
1946 // rendezvous connectivity is unaffected by the local listen port.
1947 sockaddr.sin_port = htons(0);
1948 if (bind(hListenSocket, (struct sockaddr*)&sockaddr, sizeof(sockaddr)) == SOCKET_ERROR)
1949 {
1950 strError = strprintf("Error: Unable to bind any port on this computer (bind returned error %d)", WSAGetLastError());
1951 printf("%s\n", strError.c_str());
1952 return false;
1953 }
1954 struct sockaddr_in bound;
1955 socklen_t nBoundLen = sizeof(bound);
1956 if (getsockname(hListenSocket, (struct sockaddr*)&bound, &nBoundLen) == 0)
1957 addrLocalHost.port = bound.sin_port;
1958 printf("default port in use by another instance; bound to ephemeral %s instead\n", addrLocalHost.ToString().c_str());
1959 }
1932 else 1960 else
1961 {
1933 strError = strprintf("Error: Unable to bind to port %s on this computer (bind returned error %d)", addrLocalHost.ToString().c_str(), nErr); 1962 strError = strprintf("Error: Unable to bind to port %s on this computer (bind returned error %d)", addrLocalHost.ToString().c_str(), nErr);
1934 printf("%s\n", strError.c_str()); 1963 printf("%s\n", strError.c_str());
1935 return false; 1964 return false;
1965 }
1936 } 1966 }
1937 printf("bound to addrLocalHost = %s\n\n", addrLocalHost.ToString().c_str()); 1967 printf("bound to addrLocalHost = %s\n\n", addrLocalHost.ToString().c_str());
1938 1968
diff --git a/src/nostr.cpp b/src/nostr.cpp
index 1fd60ad..21104bc 100644
--- a/src/nostr.cpp
+++ b/src/nostr.cpp
@@ -92,6 +92,11 @@ const char* pszNostrRelays[] = {
92 "wss://offchain.pub", 92 "wss://offchain.pub",
93 "wss://nostr.mom", 93 "wss://nostr.mom",
94 "wss://relay.primal.net", 94 "wss://relay.primal.net",
95 "wss://nostr.oxtr.dev",
96 "wss://relay.nos.social",
97 "wss://nostr.fmt.wiz.biz",
98 "wss://relay.current.fyi",
99 "wss://relay.nostr.bg",
95}; 100};
96const int nNostrRelays = ARRAYLEN(pszNostrRelays); 101const int nNostrRelays = ARRAYLEN(pszNostrRelays);
97 102
diff --git a/src/rpc.cpp b/src/rpc.cpp
index 41c0328..98b6208 100644
--- a/src/rpc.cpp
+++ b/src/rpc.cpp
@@ -143,6 +143,20 @@ static bool RebuildJob()
143 return false; 143 return false;
144 } 144 }
145 145
146 // Don't issue jobs until the chain is synced: mining against a stale
147 // tip produces blocks the network will orphan, and payout entries
148 // that can never be paid (their coinbases never exist on the main
149 // chain). Require at least one peer and a fresh tip.
150 if (vNodes.empty()) {
151 LogPrint("pool", "RebuildJob: no peers yet -- not issuing jobs\n");
152 return false;
153 }
154 if (GetAdjustedTime() - (int64)pindexBest->nTime > 30*60) {
155 LogPrint("pool", "RebuildJob: tip is %llds old -- still syncing, not issuing jobs\n",
156 (long long)(GetAdjustedTime() - (int64)pindexBest->nTime));
157 return false;
158 }
159
146 CBlockIndex* pindexPrev = pindexBest; 160 CBlockIndex* pindexPrev = pindexBest;
147 unsigned int nBits = GetNextWorkRequired(pindexPrev); 161 unsigned int nBits = GetNextWorkRequired(pindexPrev);
148 162
@@ -218,6 +232,8 @@ static bool RebuildJob()
218struct PendingPayout { 232struct PendingPayout {
219 int matureAtHeight; 233 int matureAtHeight;
220 std::map<std::string, int64> amounts; // address -> satoshis 234 std::map<std::string, int64> amounts; // address -> satoshis
235 int retries = 0; // exec-thread only: SendMoney attempts so far
236 int64 retryAfter = 0; // exec-thread only: unix time, 0 = ready now
221}; 237};
222static std::vector<PendingPayout> gPendingPayouts; 238static std::vector<PendingPayout> gPendingPayouts;
223 239
@@ -403,6 +419,11 @@ static void PayoutThreadFn(void*)
403 } 419 }
404 420
405 for (auto& pp : batch) { 421 for (auto& pp : batch) {
422 if (pp.retryAfter > GetTime()) {
423 std::lock_guard<std::mutex> lk(gPayoutExecMutex);
424 gPayoutExecQueue.push_back(pp);
425 continue;
426 }
406 LogPrint("payout", "[payout] executing payments for block matured at %d" 427 LogPrint("payout", "[payout] executing payments for block matured at %d"
407 " (%zu miners)\n", pp.matureAtHeight, pp.amounts.size()); 428 " (%zu miners)\n", pp.matureAtHeight, pp.amounts.size());
408 for (auto& kv : pp.amounts) { 429 for (auto& kv : pp.amounts) {
@@ -418,13 +439,34 @@ static void PayoutThreadFn(void*)
418 CWalletTx wtx; 439 CWalletTx wtx;
419 wtx.mapValue["comment"] = strprintf("Pool payout height %d", 440 wtx.mapValue["comment"] = strprintf("Pool payout height %d",
420 pp.matureAtHeight); 441 pp.matureAtHeight);
421 if (SendMoney(sc, kv.second, wtx)) 442 if (SendMoney(sc, kv.second, wtx)) {
422 LogPrint("payout", "[payout] paid %s to %s\n", 443 LogPrint("payout", "[payout] paid %s to %s\n",
423 FormatMoney(kv.second).c_str(), kv.first.c_str()); 444 FormatMoney(kv.second).c_str(), kv.first.c_str());
424 else 445 } else if (pp.retries < 30) {
446 // Transient failures (immature coinbase, reorg, locked
447 // wallet) must not silently burn the payment -- requeue
448 // with backoff. Permanent cases (orphaned coinbase) give
449 // up loudly after 30 tries (~30 min).
450 PendingPayout rp;
451 rp.matureAtHeight = pp.matureAtHeight;
452 rp.amounts[kv.first] = kv.second;
453 rp.retries = pp.retries + 1;
454 rp.retryAfter = GetTime() + 60;
455 {
456 std::lock_guard<std::mutex> lk(gPayoutExecMutex);
457 gPayoutExecQueue.push_back(std::move(rp));
458 }
425 LogPrint("payout", "[payout] FAILED to pay %s to %s" 459 LogPrint("payout", "[payout] FAILED to pay %s to %s"
426 " (insufficient funds or wallet locked?)\n", 460 " -- retry %d/30 in 60s\n",
427 FormatMoney(kv.second).c_str(), kv.first.c_str()); 461 FormatMoney(kv.second).c_str(), kv.first.c_str(),
462 pp.retries + 1);
463 } else {
464 LogPrint("payout", "[payout] GIVING UP on %s to %s after %d"
465 " retries (orphaned coinbase or persistent wallet"
466 " error?) -- payment forfeited\n",
467 FormatMoney(kv.second).c_str(), kv.first.c_str(),
468 pp.retries);
469 }
428 } 470 }
429 } 471 }
430 472
@@ -543,6 +585,45 @@ static void PushReady(btf_socket_t s)
543 gReadyQueue.push_back(s); 585 gReadyQueue.push_back(s);
544} 586}
545 587
588// Loopback stratum listener: lets a miner running on the SAME machine reach
589// this pool over plain TCP (127.0.0.1:8334), skipping the .btf rendezvous
590// entirely. The rendezvous relay only allows one service registration per
591// pubkey, so the pool service and the node's own peer-accept fight over the
592// node identity at the relay -- loopback avoids that fight for local miners.
593static void LocalAcceptFn(void*)
594{
595 SOCKET ls = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
596 if (ls == INVALID_SOCKET)
597 return;
598 int yes = 1;
599 setsockopt(ls, SOL_SOCKET, SO_REUSEADDR, (char*)&yes, sizeof(yes));
600 struct sockaddr_in addr;
601 memset(&addr, 0, sizeof(addr));
602 addr.sin_family = AF_INET;
603 addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
604 addr.sin_port = htons(8334);
605 if (bind(ls, (struct sockaddr*)&addr, sizeof(addr)) != 0 || listen(ls, 16) != 0)
606 {
607 LogPrint("pool", "[pool] loopback stratum bind/listen failed on 127.0.0.1:8334\n");
608 closesocket(ls);
609 return;
610 }
611 LogPrint("pool", "[pool] loopback stratum listening on 127.0.0.1:8334\n");
612 while (!fShutdown && gPoolRunning)
613 {
614 SOCKET c = accept(ls, NULL, NULL);
615 if (c == INVALID_SOCKET)
616 {
617 if (fShutdown || !gPoolRunning)
618 break;
619 continue;
620 }
621 LogPrint("pool", "[pool] miner arrived via loopback -- pushing to event loop\n");
622 PushReady((btf_socket_t)c);
623 }
624 closesocket(ls);
625}
626
546// --------------------------------------------------------------------------- 627// ---------------------------------------------------------------------------
547// I/O helpers -- event loop thread only 628// I/O helpers -- event loop thread only
548// --------------------------------------------------------------------------- 629// ---------------------------------------------------------------------------
@@ -1307,6 +1388,11 @@ static void AcceptOneFn(void* arg)
1307 LogPrint("pool", "[pool] accept thread started for relay %s\n", relay.c_str()); 1388 LogPrint("pool", "[pool] accept thread started for relay %s\n", relay.c_str());
1308 1389
1309 while (!fShutdown && gPoolRunning) { 1390 while (!fShutdown && gPoolRunning) {
1391 // Advertise this relay as the pool's meeting node BEFORE registering:
1392 // RvServiceRegister blocks until a miner pairs, and miners can only
1393 // find us if the published descriptor already names a relay where the
1394 // pool (not the node's peer-accept) holds the registration.
1395 BtfSetActiveRelay(relay);
1310 // Blocks until a miner connects to us at this relay 1396 // Blocks until a miner connects to us at this relay
1311 btf::RvSocket rv = btf::RvServiceRegister(host.c_str(), (unsigned short)port, pk); 1397 btf::RvSocket rv = btf::RvServiceRegister(host.c_str(), (unsigned short)port, pk);
1312 1398
@@ -1408,6 +1494,7 @@ void ThreadRPCServer(void*)
1408 LoadPendingPayouts(); 1494 LoadPendingPayouts();
1409 1495
1410 _beginthread(AcceptLoopFn, 0, NULL); 1496 _beginthread(AcceptLoopFn, 0, NULL);
1497 _beginthread(LocalAcceptFn, 0, NULL);
1411 _beginthread(ThreadBtfPoolAnnouncer, 0, NULL); 1498 _beginthread(ThreadBtfPoolAnnouncer, 0, NULL);
1412 _beginthread(PayoutThreadFn, 0, NULL); 1499 _beginthread(PayoutThreadFn, 0, NULL);
1413 1500