diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/btfrv.cpp | 41 | ||||
| -rw-r--r-- | src/main.cpp | 318 | ||||
| -rw-r--r-- | src/main.h | 3 | ||||
| -rw-r--r-- | src/main_gui.cpp | 40 | ||||
| -rw-r--r-- | src/net.cpp | 36 | ||||
| -rw-r--r-- | src/nostr.cpp | 5 | ||||
| -rw-r--r-- | src/rpc.cpp | 95 |
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; | |||
| 102 | int nMineMode = MINE_RELAY; | 102 | int nMineMode = MINE_RELAY; |
| 103 | bool fMineModeFromCommandLine = false; | 103 | bool fMineModeFromCommandLine = false; |
| 104 | string strParticipantPool; // participant mode: pool .btf address | 104 | string strParticipantPool; // participant mode: pool .btf address |
| 105 | string strPayoutAddress; // participant: /payoutaddr override (mine without a wallet) | ||
| 106 | string strPayoutFile; // participant: /payoutfile with one address per line, rotated per reconnect | ||
| 107 | int nHashThreads = 0; // participant: /hashthreads override (0 = cores-1) | ||
| 105 | string strPoolName = "Bitflash Pool"; | 108 | string strPoolName = "Bitflash Pool"; |
| 106 | string strPoolDashboardUrl; | 109 | string strPoolDashboardUrl; |
| 107 | double dPoolFeePercent = 0.0; | 110 | double 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 |
| 3489 | static const int64 STUCK_NO_JOB_TIMEOUT_SECS = 120; | 3492 | static 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 | // ------------------------------------------------------------------ | ||
| 3502 | struct 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 | |||
| 3517 | struct ParticipantShareHit | ||
| 3518 | { | ||
| 3519 | uint64 jobSeq; | ||
| 3520 | std::string jobId; | ||
| 3521 | unsigned int nonce; | ||
| 3522 | }; | ||
| 3523 | |||
| 3524 | static 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 | |||
| 3491 | static bool PoolParticipantMiner() | 3590 | static 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 | |||
| 3699 | connected: | ||
| 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" |
| @@ -84,6 +84,9 @@ extern int nMineMode; | |||
| 84 | // something a previous session wrote. | 84 | // something a previous session wrote. |
| 85 | extern bool fMineModeFromCommandLine; | 85 | extern bool fMineModeFromCommandLine; |
| 86 | extern string strParticipantPool; // participant: pool .btf address | 86 | extern string strParticipantPool; // participant: pool .btf address |
| 87 | extern string strPayoutAddress; // participant: /payoutaddr override (mine without a wallet) | ||
| 88 | extern string strPayoutFile; // participant: /payoutfile address list, rotated per reconnect | ||
| 89 | extern int nHashThreads; // participant: /hashthreads override (0 = cores-1) | ||
| 87 | extern string strPoolName; // operator: announced pool name | 90 | extern string strPoolName; // operator: announced pool name |
| 88 | extern string strPoolDashboardUrl; // operator: optional dashboard URL | 91 | extern string strPoolDashboardUrl; // operator: optional dashboard URL |
| 89 | extern double dPoolFeePercent; // operator: announced fee percent | 92 | extern 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 | |||
| 1054 | void ThreadBtfAccept(void* parg) | 1054 | void 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 | }; |
| 96 | const int nNostrRelays = ARRAYLEN(pszNostrRelays); | 101 | const 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() | |||
| 218 | struct PendingPayout { | 232 | struct 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 | }; |
| 222 | static std::vector<PendingPayout> gPendingPayouts; | 238 | static 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. | ||
| 593 | static 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 | ||