-
Notifications
You must be signed in to change notification settings - Fork 14
Expand file tree
/
Copy pathnode.cpp
More file actions
2282 lines (2035 loc) · 94.2 KB
/
Copy pathnode.cpp
File metadata and controls
2282 lines (2035 loc) · 94.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// SPDX-License-Identifier: AGPL-3.0-or-later
#include "node.hpp"
#include <core/common.hpp>
#include <core/hash.hpp>
#include <core/random.hpp>
#include <core/target_utils.hpp>
#include <sharechain/prepared_list.hpp>
#include <algorithm>
#include <filesystem>
#include <fstream>
#include <iomanip>
#include <random>
#ifndef _WIN32
#include <execinfo.h> // backtrace() for think() watchdog stack dump (glibc-only)
#endif
// Static members for DensePPLNSWindow precomputed decay table
std::vector<uint64_t> ltc::DensePPLNSWindow::s_decay_table;
uint64_t ltc::DensePPLNSWindow::s_decay_per = 0;
bool ltc::DensePPLNSWindow::s_table_initialized = false;
// Helper: read current RSS from /proc/self/status (Linux only)
static long get_rss_mb() {
std::ifstream f("/proc/self/status");
std::string line;
while (std::getline(f, line)) {
if (line.rfind("VmRSS:", 0) == 0) {
long kb = 0;
sscanf(line.c_str(), "VmRSS: %ld", &kb);
return kb / 1024;
}
}
return 0;
}
static long g_rss_limit_mb = 4000; // abort if RSS exceeds this (configurable)
// p2pool-style hashrate formatting: auto-scale to H/s, kH/s, MH/s, GH/s, TH/s
static std::string format_hashrate(double hs) {
std::ostringstream os;
os << std::fixed;
if (hs >= 1e12) os << std::setprecision(2) << hs / 1e12 << "TH/s";
else if (hs >= 1e9) os << std::setprecision(2) << hs / 1e9 << "GH/s";
else if (hs >= 1e6) os << std::setprecision(2) << hs / 1e6 << "MH/s";
else if (hs >= 1e3) os << std::setprecision(1) << hs / 1e3 << "kH/s";
else os << std::setprecision(0) << hs << "H/s";
return os.str();
}
// p2pool-style duration formatting: auto-scale to seconds, hours, days, years
static std::string format_duration(double secs) {
if (secs <= 0 || !std::isfinite(secs)) return "???";
std::ostringstream os;
os << std::fixed;
if (secs >= 86400.0 * 365.25)
os << std::setprecision(1) << secs / (86400.0 * 365.25) << " years";
else if (secs >= 86400.0)
os << std::setprecision(1) << secs / 86400.0 << " days";
else if (secs >= 3600.0)
os << std::setprecision(1) << secs / 3600.0 << " hours";
else if (secs >= 60.0)
os << std::setprecision(1) << secs / 60.0 << " minutes";
else
os << std::setprecision(1) << secs << " seconds";
return os.str();
}
// p2pool-style Wilson score confidence interval (util/math.py:133-152)
// Returns "~X.Y% (lo-hi%)" string for binomial proportion x/n at 95% confidence.
static std::string format_binomial_conf(int x, int n, double conf = 0.95) {
if (n == 0) return "???";
// z for 95% ≈ 1.96 (inverse error function approximation)
double z = 1.96;
double p = static_cast<double>(x) / n;
double topa = p + z * z / (2.0 * n);
double topb = z * std::sqrt(p * (1.0 - p) / n + z * z / (4.0 * n * n));
double bottom = 1.0 + z * z / n;
double lo = std::max(0.0, (topa - topb) / bottom);
double hi = std::min(1.0, (topa + topb) / bottom);
std::ostringstream os;
os << "~" << std::fixed << std::setprecision(1) << (100.0 * p) << "% ("
<< static_cast<int>(std::floor(100.0 * lo)) << "-"
<< static_cast<int>(std::ceil(100.0 * hi)) << "%)";
return os.str();
}
// Wilson score confidence interval for efficiency: 1 - stale_rate, scaled
static std::string format_binomial_conf_efficiency(int stale, int n, double stale_prop) {
if (n == 0) return "???";
double z = 1.96;
double p = static_cast<double>(stale) / n;
double topa = p + z * z / (2.0 * n);
double topb = z * std::sqrt(p * (1.0 - p) / n + z * z / (4.0 * n * n));
double bottom = 1.0 + z * z / n;
double lo_stale = std::max(0.0, (topa - topb) / bottom);
double hi_stale = std::min(1.0, (topa + topb) / bottom);
// Efficiency = (1 - stale_rate) / (1 - stale_prop)
double denom = (stale_prop < 0.999) ? (1.0 - stale_prop) : 1.0;
double eff = (1.0 - p) / denom;
double eff_lo = (1.0 - hi_stale) / denom;
double eff_hi = (1.0 - lo_stale) / denom;
eff_lo = std::max(0.0, eff_lo);
eff_hi = std::min(1.0, eff_hi);
std::ostringstream os;
os << "~" << std::fixed << std::setprecision(1) << (100.0 * eff) << "% ("
<< static_cast<int>(std::floor(100.0 * eff_lo)) << "-"
<< static_cast<int>(std::ceil(100.0 * eff_hi)) << "%)";
return os.str();
}
namespace ltc
{
static uint64_t make_random_nonce()
{
std::mt19937_64 rng(std::random_device{}());
return rng();
}
void NodeImpl::send_ping(peer_ptr peer)
{
auto rmsg = ltc::message_ping::make_raw();
peer->write(std::move(rmsg));
};
void NodeImpl::connected(std::shared_ptr<core::Socket> socket)
{
auto addr = socket->get_addr();
bool is_outbound = m_pending_outbound.erase(addr) > 0;
// Reject banned peers
if (is_banned(addr))
{
LOG_INFO << "[Pool] Rejecting connection from banned peer " << addr.to_string();
socket->close();
return;
}
// Let BaseNode create the peer and set up the timeout timer
base_t::connected(socket);
if (is_outbound)
m_outbound_addrs.insert(addr);
auto peer = m_connections[addr];
send_version(peer);
}
void NodeImpl::error(const message_error_type& err, const NetService& service, const std::source_location where)
{
// If peer disconnected within 10s of us sending shares, those shares
// were likely rejected (e.g. PoW-invalid). Mark them so we don't
// keep re-broadcasting the same bad share on every reconnection.
{
auto it = m_last_broadcast_to.find(service);
if (it != m_last_broadcast_to.end()) {
auto elapsed = std::chrono::steady_clock::now() - it->second.when;
if (elapsed < std::chrono::seconds(10)) {
for (const auto& h : it->second.hashes) {
if (m_rejected_share_hashes.insert(h).second) {
LOG_WARNING << "[Pool] Marking share " << h.GetHex().substr(0, 16)
<< " as rejected (peer " << service.to_string()
<< " disconnected " << std::chrono::duration_cast<std::chrono::milliseconds>(elapsed).count()
<< "ms after broadcast)";
}
}
}
m_last_broadcast_to.erase(it);
}
}
// Drop stale nonce->peer entries for this endpoint before base cleanup.
// Without this, reconnects can be rejected as false duplicates because
// m_peers still contains the old nonce mapping.
for (auto it = m_peers.begin(); it != m_peers.end(); )
{
if (it->second && it->second->addr() == service)
it = m_peers.erase(it);
else
++it;
}
// Clean outbound tracking before base removes the peer
m_pending_outbound.erase(service);
m_outbound_addrs.erase(service);
// p2pool p2p.py:595: self.get_shares.respond_all(reason)
// Cancel pending share requests for this peer — invokes callbacks with
// empty response so m_downloading_shares entries are cleaned up immediately
// instead of waiting for the 5s ReplyMatcher timeout.
{
std::vector<uint256> to_cancel;
for (auto& [req_id, peer_addr] : m_pending_share_reqs) {
if (peer_addr == service)
to_cancel.push_back(req_id);
}
for (auto& req_id : to_cancel) {
m_pending_share_reqs.erase(req_id);
m_share_getter.cancel(req_id);
}
}
base_t::error(err, service, where);
}
void NodeImpl::close_connection(const NetService& service)
{
// Same rejection tracking as error() — close_connection is another
// path for peer disconnection.
{
auto it = m_last_broadcast_to.find(service);
if (it != m_last_broadcast_to.end()) {
auto elapsed = std::chrono::steady_clock::now() - it->second.when;
if (elapsed < std::chrono::seconds(10)) {
for (const auto& h : it->second.hashes)
m_rejected_share_hashes.insert(h);
}
m_last_broadcast_to.erase(it);
}
}
m_pending_outbound.erase(service);
m_outbound_addrs.erase(service);
// Cancel pending share requests for this peer (same as in error())
{
std::vector<uint256> to_cancel;
for (auto& [req_id, peer_addr] : m_pending_share_reqs) {
if (peer_addr == service)
to_cancel.push_back(req_id);
}
for (auto& req_id : to_cancel) {
m_pending_share_reqs.erase(req_id);
m_share_getter.cancel(req_id);
}
}
base_t::close_connection(service);
}
NodeImpl::TrackerSnapshot NodeImpl::get_tracker_snapshot() const {
std::lock_guard<std::mutex> lock(m_snapshot_mutex);
return m_snapshot;
}
int NodeImpl::get_chain_count() const { return get_tracker_snapshot().chain_count; }
int NodeImpl::get_verified_count() const { return get_tracker_snapshot().verified_count; }
void NodeImpl::send_version(peer_ptr peer)
{
auto rmsg = ltc::message_version::make_raw(
m_tracker.m_params->advertised_protocol_version, // advertise our V36 capability, NOT the accept-floor (handle_version:328 keeps floor)
1, // services
addr_t{1, peer->addr()}, // addr_to (the remote)
addr_t{1, NetService{"0.0.0.0", m_tracker.m_params->p2p_port}}, // addr_from (us)
m_nonce,
m_software_version,
1, // mode (always 1 for legacy compat)
advertised_best_share() // verified head, or raw head pre-sync (ROOT-2)
);
peer->write(std::move(rmsg));
}
std::optional<pool::PeerConnectionType> NodeImpl::handle_version(std::unique_ptr<RawMessage> rmsg, peer_ptr peer)
{
LOG_DEBUG_POOL << "handle message_version";
std::unique_ptr<ltc::message_version> msg;
msg = ltc::message_version::make(rmsg->m_data);
LOG_INFO << "[Pool] Peer "
<< msg->m_addr_from.m_endpoint.to_string()
<< " says protocol version is "
<< msg->m_version
<< ", client version "
<< msg->m_subversion;
if (peer->m_other_version.has_value())
{
LOG_DEBUG_POOL << "more than one version message";
throw std::runtime_error("more than one version message");
}
peer->m_other_version = msg->m_version;
peer->m_other_subversion = msg->m_subversion;
peer->m_other_services = msg->m_services;
if (m_nonce == msg->m_nonce)
{
LOG_WARNING << "[Pool] was connected to self";
return std::nullopt;
}
if (m_peers.contains(msg->m_nonce))
{
LOG_DEBUG_POOL << "Detected duplicate connection, disconnecting from " << peer->addr().to_string();
return std::nullopt;
}
peer->m_nonce = msg->m_nonce;
m_peers[peer->m_nonce] = peer;
// Request peers from the newly established connection
{
auto getaddrs_msg = ltc::message_getaddrs::make_raw(8);
peer->write(std::move(getaddrs_msg));
}
// Reject peers running too-old protocol
if (msg->m_version < m_tracker.m_params->minimum_protocol_version)
{
LOG_WARNING << "Peer " << msg->m_addr_from.m_endpoint.to_string()
<< " protocol " << msg->m_version
<< " < minimum " << m_tracker.m_params->minimum_protocol_version
<< ", disconnecting";
throw std::runtime_error("peer protocol too old");
}
if (!msg->m_best_share.IsNull())
{
LOG_INFO << "Best share hash for " << msg->m_addr_from.m_endpoint.to_string()
<< " = " << msg->m_best_share.ToString();
if (!m_chain->contains(msg->m_best_share)) {
// Start downloading shares we don't have
download_shares(peer, msg->m_best_share);
} else {
// p2pool: handle_share_hashes → handle_shares → set_best_share()
// Even when the share is known, re-run think() to re-evaluate
// best chain with the peer's perspective. Critical after restart:
// shares loaded from LevelDB may have stale best_share selection.
run_think();
}
}
// Advertise ourselves to the peer (matching Python p2pool sendAdvertisement)
{
auto port = core::Server::listen_port();
auto addrme_msg = ltc::message_addrme::make_raw(port);
peer->write(std::move(addrme_msg));
}
return pool::PeerConnectionType::legacy;
}
void NodeImpl::processing_shares(HandleSharesData& data_ref, NetService addr)
{
// Take ownership immediately so the caller can return/free its local.
auto data = std::make_shared<HandleSharesData>(std::move(data_ref));
size_t n = data->m_items.size();
if (n == 0) return;
// Phase 1 (thread pool, parallel): run share_init_verify() for each share.
// share_init_verify() does scrypt-1024 (~20ms each) — must NOT block io_context.
// Each share's hash computation is independent, so we can fully parallelize.
auto remaining = std::make_shared<std::atomic<int>>(static_cast<int>(n));
for (size_t i = 0; i < n; i++)
{
boost::asio::post(m_verify_pool,
[i, data, remaining, this, addr]()
{
auto& share = data->m_items[i];
if (share.hash().IsNull())
{
try
{
share.ACTION({
obj->m_hash = share_init_verify(*obj, *m_tracker.m_params, true);
});
}
catch (const std::exception&)
{
// leave hash null — phase 2 will skip this share
}
}
// When all verifications are done, schedule phase 2 on io_context
if (--(*remaining) == 0)
{
boost::asio::post(*m_context,
[data, this, addr]()
{
processing_shares_phase2(*data, addr);
});
}
});
}
}
void NodeImpl::processing_shares_phase2(HandleSharesData& data, NetService addr)
{
// Phase 2 (io_context thread): topological sort + chain insertion + LevelDB store.
// All shared state (m_tracker, m_chain, m_raw_share_cache, m_storage) touched here.
//
// Non-blocking mutex: if think() holds the exclusive lock on the compute
// thread, queue this batch for processing after think() releases. The IO
// thread never blocks — keepalive timers and network I/O continue.
//
// HOLD this lock across the entire mutation body below (mirrors LTC f445db8e).
// try_to_lock keeps the IO thread non-blocking — busy => queue + return. Once
// acquired we must NOT release until all m_tracker.chain mutations are done:
// the prior code released here and ran the body lock-free, letting the
// compute-thread clean_tracker() exclusive prune (drop_tails) free chain nodes
// mid-mutation -> SIGSEGV (kr1z1s LTC/DGB). Released just before run_think().
std::unique_lock lock(m_tracker_mutex, std::try_to_lock);
if (!lock.owns_lock()) {
// ── Backpressure (V36 livelock defense-in-depth) ──────────────
// If think() is wedged/slow the deferred queue could grow without
// bound and blow memory. Cap it: over MAX_PENDING_ADDS we DROP the
// new batch (peers re-advertise their best share, so dropped shares
// are re-requested later) and warn instead of growing unbounded.
if (m_pending_adds.size() >= MAX_PENDING_ADDS) {
LOG_WARNING << "[ASYNC-DEFER] BACKPRESSURE: pending_adds at cap ("
<< m_pending_adds.size() << "/" << MAX_PENDING_ADDS
<< "), dropping batch of " << data.m_items.size()
<< " shares from " << addr.to_string()
<< " — think() may be wedged";
return;
}
LOG_INFO << "[ASYNC-DEFER] processing_shares_phase2: mutex busy, queuing "
<< data.m_items.size() << " shares from " << addr.to_string()
<< " (pending=" << m_pending_adds.size() + 1 << ")";
m_pending_adds.push_back(PendingShareBatch{
std::make_unique<HandleSharesData>(std::move(data)), addr});
return;
}
// Lock acquired and HELD across the mutation body below; released just before
// the async run_think() trigger so the compute thread can take the exclusive
// lock. ASIO single-thread still guarantees no overlapping IO handler.
// Step 1: collect verified shares (skip any that failed verification, hash still null)
std::vector<ShareType> valid_shares;
valid_shares.reserve(data.m_items.size());
for (size_t idx = 0; idx < data.m_items.size(); ++idx)
{
auto& share = data.m_items[idx];
if (share.hash().IsNull())
continue; // verification failed in phase 1
// Cache original raw bytes for relay (keyed by computed hash)
if (idx < data.m_raw_items.size() && !data.m_raw_items[idx].contents.m_data.empty())
m_raw_share_cache[share.hash()] = std::move(data.m_raw_items[idx]);
valid_shares.push_back(share);
}
// Step 2: Topologically sort valid shares by hash/prev_hash linkage
chain::PreparedList<uint256, ShareType> prepare_shares(valid_shares);
std::vector<ShareType> shares = prepare_shares.build_list();
// Step 3: Process sorted shares
int32_t new_count = 0;
int32_t dup_count = 0;
std::map<uint256, coin::MutableTransaction> all_new_txs;
std::vector<c2pool::storage::SharechainStorage::ShareBatchEntry> db_batch;
for (int i = 0; i < (int)shares.size(); ++i)
{
auto& share = shares[i];
// Safety: abort if RSS exceeds limit
if (i % 100 == 0) {
long rss_now = get_rss_mb();
if (rss_now > g_rss_limit_mb) {
LOG_ERROR << "RSS LIMIT EXCEEDED (" << rss_now << "MB > " << g_rss_limit_mb << "MB) — aborting!";
std::abort();
}
}
auto& new_txs = data.m_txs[share.hash()];
if (!new_txs.empty())
{
for (auto& new_tx : new_txs)
{
PackStream packed_tx = pack(coin::TX_WITH_WITNESS(new_tx));
all_new_txs[Hash(packed_tx.get_span())] = new_tx;
}
}
if (m_chain->contains(share.hash()))
{
++dup_count;
continue;
}
++new_count;
// Log received share — p2pool format: "Received share diff=X hash=Y miner=Z"
share.invoke([](auto* obj) {
auto target = chain::bits_to_target(obj->m_bits);
double diff = chain::target_to_difficulty(target);
// Extract miner identity (pubkey_hash for v17/v33/v36, address script for v34/v35)
std::string miner_hex;
if constexpr (requires { obj->m_pubkey_hash; })
miner_hex = obj->m_pubkey_hash.GetHex().substr(0, 16);
else if constexpr (requires { obj->m_address; })
miner_hex = "script";
LOG_INFO << "Received share: diff=" << std::scientific << std::setprecision(2) << diff
<< " hash=" << obj->m_hash.GetHex().substr(0, 16)
<< " miner=" << miner_hex;
});
m_tracker.add(share);
// Log fork detection: if this share's prev_hash has other children, it forks
{
uint256 prev;
bool is_local = false;
share.invoke([&](auto* obj) {
prev = obj->m_prev_hash;
is_local = (obj->peer_addr == NetService{"0.0.0.0", 0} ||
obj->peer_addr == NetService{});
});
if (is_local && !prev.IsNull()) {
auto& rev = m_tracker.chain.get_reverse();
auto it = rev.find(prev);
if (it != rev.end() && it->second.size() > 1) {
static int fork_log = 0;
if (fork_log++ < 50)
LOG_WARNING << "[FORK] Local share forks! prev=" << prev.GetHex().substr(0,16)
<< " siblings=" << it->second.size()
<< " verified_best=" << m_best_share_hash.GetHex().substr(0,16);
}
}
}
// Verification is deferred to think() Phase 1 (called after this batch).
// p2pool: handle_shares() only adds, set_best_share()→think() verifies.
// Inline verification was redundant and caused double-verify CPU waste.
// NOTE: Do NOT trim inside the processing loop. The trim in run_think()
// handles pruning between batches. Trimming here is unsafe because
// shares added at the tail can be freed while the loop still holds
// dangling raw pointers to them (use-after-free).
// Collect for batch LevelDB persist (committed atomically after loop)
if (m_storage && m_storage->is_available())
{
std::vector<uint8_t> bytes;
auto raw_it = m_raw_share_cache.find(share.hash());
if (raw_it != m_raw_share_cache.end() &&
raw_it->second.type == share.version() &&
!raw_it->second.contents.m_data.empty())
{
bytes.assign(raw_it->second.contents.m_data.begin(),
raw_it->second.contents.m_data.end());
}
else
{
PackStream ps = pack(share);
auto span = ps.get_span();
bytes.assign(reinterpret_cast<const uint8_t*>(span.data()),
reinterpret_cast<const uint8_t*>(span.data()) + span.size());
}
uint64_t ver = share.version();
std::vector<uint8_t> versioned;
versioned.resize(8 + bytes.size());
std::memcpy(versioned.data(), &ver, 8);
std::memcpy(versioned.data() + 8, bytes.data(), bytes.size());
share.ACTION({
uint256 target = chain::bits_to_target(obj->m_bits);
uint256 abswork_256;
std::copy(obj->m_abswork.begin(), obj->m_abswork.end(), abswork_256.begin());
c2pool::storage::SharechainStorage::ShareBatchEntry entry;
entry.hash = obj->m_hash;
entry.serialized_data = std::move(versioned);
entry.prev_hash = obj->m_prev_hash;
entry.height = obj->m_absheight;
entry.timestamp = obj->m_timestamp;
entry.work = abswork_256;
entry.target = target;
db_batch.push_back(std::move(entry));
});
}
}
// Commit all shares to LevelDB atomically (one WriteBatch for entire batch).
// Crash-safe: either ALL shares persisted or NONE.
if (!db_batch.empty() && m_storage && m_storage->is_available()) {
m_storage->store_shares_batch(db_batch);
}
if (new_count > 0) {
auto as2 = addr.to_string();
std::string source = (addr.port() == 0) ? as2.substr(0, as2.rfind(':')) : as2;
LOG_INFO << "Processing " << new_count << " shares from "
<< source << "... (dup=" << dup_count
<< " chain=" << m_tracker.chain.size() << ")";
}
// Release the tracker lock before triggering think(): run_think() posts the
// think+prune job to the compute thread, which needs the exclusive lock. All
// chain mutations above are complete at this point.
lock.unlock();
// Trigger think() after every share batch (p2pool: set_best_share after handle_shares).
// p2pool calls set_best_share() after EVERY batch with new_count > 0 — no size gate.
// think() scores heads and updates best_share + desired set for download_shares.
if (new_count > 0) {
run_think();
}
}
std::vector<ltc::ShareType> NodeImpl::handle_get_share(std::vector<uint256> hashes, uint64_t parents, std::vector<uint256> stops, NetService peer_addr)
{
// try_to_lock per the architectural rule (node.hpp:67) — IO thread MUST
// never block on m_tracker_mutex. A blocking shared_lock here was the
// root cause of the periodic event-loop freeze + SIGABRT cycle on
// contabo (2026-04-12, -16, -19, -21, -25): when the compute thread
// held the exclusive lock for a long think+clean cycle on a wedged
// chain (~30+s), an incoming SHAREREQ on the IO thread would block
// here, the watchdog would fire after 30s of io_context unresponsive,
// and systemd would restart.
//
// Empty reply does NOT cause peer disconnect — p2pool's downloader
// (node.py:120) picks a random peer per request and retries; an empty
// hit just shifts to a different peer next iteration.
std::shared_lock<std::shared_mutex> lock(m_tracker_mutex, std::try_to_lock);
if (!lock.owns_lock())
{
static int defer_log = 0;
if (defer_log++ % 50 == 0)
LOG_INFO << "[handle_get_share] tracker busy — returning empty to "
<< peer_addr.to_string()
<< " (peer will retry against another peer)";
return {};
}
parents = std::min(parents, (uint64_t)1000/hashes.size());
std::vector<ltc::ShareType> shares;
for (const auto& handle_hash : hashes)
{
if (!m_chain->contains(handle_hash))
{
static int miss_log = 0;
if (miss_log++ < 5)
LOG_WARNING << "[handle_get_share] hash NOT in chain: "
<< handle_hash.ToString().substr(0, 16)
<< " chain_size=" << m_chain->size()
<< " tracker_chain_size=" << m_tracker.chain.size();
continue;
}
uint64_t n = std::min(parents+1, (uint64_t) m_chain->get_height(handle_hash));
for (auto [hash, data] : m_chain->get_chain(handle_hash, n))
{
if (std::find(stops.begin(), stops.end(), hash) != stops.end())
break;
if (m_rejected_share_hashes.count(hash))
continue;
shares.push_back(data.share);
}
}
if (!shares.empty())
{
LOG_INFO << "[Pool] Sending " << shares.size() << " shares to " << peer_addr.to_string();
}
return shares;
}
void NodeImpl::send_shares(peer_ptr peer, const std::vector<uint256>& share_hashes)
{
// try_to_lock per the architectural rule (node.hpp:67) — see freeze
// analysis in handle_get_share above. If we can't acquire NOW, skip
// this batch. The shares are still in our chain; the next broadcast
// cycle (or the next think() result) picks them up.
std::shared_lock<std::shared_mutex> lock(m_tracker_mutex, std::try_to_lock);
if (!lock.owns_lock())
{
static int defer_log = 0;
if (defer_log++ % 50 == 0)
LOG_INFO << "[send_shares] tracker busy — skipping send to "
<< peer->addr().to_string() << " (will retry next cycle)";
return;
}
// Collect shares that exist in our chain (skip rejected)
std::vector<ShareType> shares;
for (const auto& hash : share_hashes)
{
if (!m_chain->contains(hash))
continue;
if (m_rejected_share_hashes.count(hash))
continue;
// Retrieve the share via get_chain(hash, 1) — first element is the share itself
for (auto [h, data] : m_chain->get_chain(hash, 1))
{
shares.push_back(data.share);
break;
}
}
if (shares.empty())
return;
// Collect transactions that the peer doesn't know about
std::set<uint256> needed_txs;
for (auto& share : shares)
{
share.invoke([&](auto* obj) {
if constexpr (requires { obj->m_new_transaction_hashes; })
{
for (const auto& th : obj->m_new_transaction_hashes)
{
if (!peer->m_remote_txs.count(th) &&
!peer->m_remembered_txs.count(th))
needed_txs.insert(th);
}
}
});
}
// Send remember_tx for txs the peer needs
if (!needed_txs.empty())
{
std::vector<uint256> known_hashes; // hashes in peer's remote set
std::vector<coin::MutableTransaction> full_txs; // full txs otherwise
for (const auto& th : needed_txs)
{
if (peer->m_remote_txs.count(th))
{
known_hashes.push_back(th);
}
else
{
auto it = m_known_txs.find(th);
if (it != m_known_txs.end())
full_txs.emplace_back(it->second);
}
}
if (!known_hashes.empty() || !full_txs.empty())
{
auto rtx_msg = message_remember_tx::make_raw(known_hashes, full_txs);
peer->write(std::move(rtx_msg));
}
}
// Pack and send shares — use cached original bytes when available
std::vector<chain::RawShare> rshares;
rshares.reserve(shares.size());
for (size_t i = 0; i < shares.size(); ++i)
{
auto it = m_raw_share_cache.find(shares[i].hash());
if (it != m_raw_share_cache.end())
{
rshares.push_back(it->second);
}
else
{
rshares.emplace_back(shares[i].version(), pack(shares[i]));
}
}
auto shares_msg = message_shares::make_raw(rshares);
peer->write(std::move(shares_msg));
// Send forget_tx so peer can free the remembered txs
if (!needed_txs.empty())
{
std::vector<uint256> forget_vec(needed_txs.begin(), needed_txs.end());
auto ftx_msg = message_forget_tx::make_raw(forget_vec);
peer->write(std::move(ftx_msg));
}
LOG_INFO << "[Pool] Sent " << shares.size() << " shares (+" << needed_txs.size()
<< " txs) to " << peer->addr().to_string();
}
void NodeImpl::broadcast_share(const uint256& share_hash)
{
// try_to_lock per the architectural rule (node.hpp:67) — see freeze
// analysis in handle_get_share above. If think+clean holds the
// exclusive lock right now, defer this broadcast: the share is still
// in our chain; the next local share creation, the next think cycle,
// or the next peer-driven SHAREREQ will pick it up. Blocking here
// was a contributing freeze trigger (called from local-share creation
// and from the share-add hot path).
std::shared_lock<std::shared_mutex> lock(m_tracker_mutex, std::try_to_lock);
if (!lock.owns_lock())
{
static int defer_log = 0;
if (defer_log++ % 50 == 0)
LOG_INFO << "[broadcast_share] tracker busy — deferring broadcast of "
<< share_hash.GetHex().substr(0, 16)
<< " (next cycle will pick it up)";
return;
}
// Walk the chain back from share_hash, collecting un-broadcast shares
std::vector<uint256> to_send;
int32_t height = m_chain->get_height(share_hash);
int32_t walk = std::min(height, 5);
for (auto [hash, data] : m_chain->get_chain(share_hash, walk))
{
if (m_shared_share_hashes.count(hash))
break;
if (m_rejected_share_hashes.count(hash))
continue; // skip shares previously rejected by peers
m_shared_share_hashes.insert(hash);
to_send.push_back(hash);
}
if (to_send.empty())
return;
auto now = std::chrono::steady_clock::now();
for (auto& [nonce, peer] : m_peers) {
send_shares(peer, to_send);
m_last_broadcast_to[peer->addr()] = {to_send, now};
}
}
void NodeImpl::notify_local_share(const uint256& share_hash)
{
// p2pool: set_best_share() → think() synchronously on the reactor thread.
// Use think() for ALL best_share decisions, as p2pool does.
if (share_hash.IsNull())
return;
// Both the chain.contains() read AND attempt_verify() run UNDER the tracker
// lock (mirrors LTC f445db8e). A bare m_tracker.chain.contains() here (the
// prior code) raced the compute-thread clean_tracker() exclusive prune freeing
// chain nodes -> SIGSEGV (kr1z1s LTC/DGB). try_to_lock keeps the IO thread
// non-blocking; if think()/clean holds the lock we skip the inline verify —
// the share is already in-chain and run_think() below will score it next cycle.
{
std::unique_lock lock(m_tracker_mutex, std::try_to_lock);
if (lock.owns_lock() && m_tracker.chain.contains(share_hash))
m_tracker.attempt_verify(share_hash);
}
// Trigger async think() — will pick up the local share through scoring.
run_think();
}
uint256 NodeImpl::best_share_hash()
{
// p2pool's think() returns the best VERIFIED head — not the raw chain tip.
// This ensures shares are built on a chain that all peers agree on.
// Use think().best ONLY if it's on the VERIFIED chain.
// p2pool's think() only considers verified shares for the best head.
// If think().best is on an unverified fork (e.g., our own shares that
// p2pool hasn't verified yet), fall back to the best verified head.
// This prevents building a self-reinforcing fork that never joins
// the main chain.
if (!m_best_share_hash.IsNull() && m_tracker.verified.contains(m_best_share_hash)) {
static int log_count = 0;
if (log_count++ % 60 == 0) {
auto h = m_tracker.verified.get_height(m_best_share_hash);
LOG_INFO << "[best_share] using think() result height=" << h
<< " verified=" << m_tracker.verified.size()
<< " raw=" << (m_chain ? m_chain->size() : 0);
}
return m_best_share_hash;
}
// Fallback: if think() hasn't run yet, pick best verified head by work
auto& verified = m_tracker.verified;
if (verified.size() > 0) {
uint256 best;
uint288 best_work;
bool first = true;
for (const auto& [head_hash, tail_hash] : verified.get_heads()) {
auto* idx = verified.get_index(head_hash);
if (idx && (first || idx->work > best_work)) {
best = head_hash;
best_work = idx->work;
first = false;
}
}
if (!best.IsNull()) {
static int log_count2 = 0;
auto best_height = verified.get_height(best);
if (log_count2++ % 60 == 0)
LOG_INFO << "[best_share] fallback VERIFIED head height=" << best_height
<< " work=" << best_work.GetHex().substr(0, 16)
<< " verified=" << verified.size()
<< " raw=" << (m_chain ? m_chain->size() : 0);
return best;
}
}
// No verified chain yet — return null to prevent creating shares with
// MAX_TARGET max_bits. p2pool does the same: best_share_var.value = None
// until the verified chain exists, and generate_transaction refuses to
// create work when best_share is None (with peers connected).
//
// On a genesis node (no peers, fresh chain), this returns ZERO which
// triggers genesis share creation with 100% donation payout.
if (!m_peers.empty()) {
static int wait_log = 0;
if (wait_log++ % 12 == 0)
LOG_INFO << "[best_share] waiting for verified chain (peers="
<< m_peers.size() << " raw="
<< (m_chain ? m_chain->size() : 0) << ")";
return uint256::ZERO;
}
// True genesis: no peers at all — use raw chain to bootstrap
if (!m_chain || m_chain->size() == 0)
return uint256::ZERO;
uint256 best;
int32_t best_height = -1;
for (const auto& [head_hash, tail_hash] : m_chain->get_heads()) {
auto h = m_chain->get_height(head_hash);
if (h > best_height) {
best = head_hash;
best_height = h;
}
}
static int raw_log = 0;
if (raw_log++ % 60 == 0)
LOG_INFO << "[best_share] using RAW head (genesis, no peers) height=" << best_height;
return best;
}
uint256 NodeImpl::advertised_best_share()
{
// Peer-facing advertisement ONLY (version handshake + timer re-announce).
// NEVER used for work/share creation — best_share_hash() owns that and
// deliberately returns a VERIFIED head (or NULL) so we never build local
// work on an unagreed chain. For ADVERTISEMENT the opposite is right: a
// peer must learn our tallest head to call download_shares() and pull our
// chain. ROOT-2 (Option-A net-id soak): a --genesis node whose verified
// chain is still empty at handshake advertises NULL via best_share_hash();
// the peer never downloads and broadcast can't backfill (head shares are
// already de-dup-marked). Advertising the raw head breaks that deadlock
// without touching work creation.
uint256 v = best_share_hash();
if (!v.IsNull())
return v;
// Verified chain still empty — advertise our tallest RAW head instead.
if (m_chain && m_chain->size() > 0) {
uint256 best;
int32_t best_height = -1;
for (const auto& [head_hash, tail_hash] : m_chain->get_heads()) {
auto h = m_chain->get_height(head_hash);
if (h > best_height) {
best = head_hash;
best_height = h;
}
}
return best;
}
return uint256::ZERO;
}
void NodeImpl::readvertise_best_share()
{
// ROOT-2 re-advertisement. A peer that finished its version handshake
// while our verified chain was empty got a NULL best_share and never
// called download_shares(); broadcast_share() can't wake it either because
// our head shares are already in m_shared_share_hashes (de-dup-marked at
// creation) so its walk breaks immediately. Here we re-push the tip walk
// to every peer WITHOUT consulting the de-dup set. Advertise-only: never
// affects local work creation.
uint256 head = advertised_best_share();
if (head.IsNull())
return;
// Same try_to_lock discipline as broadcast_share (node.hpp:67): never block
// the IO thread on the tracker mutex. If think() holds it now, the next
// trigger (best-change or the timer) retries.
std::shared_lock<std::shared_mutex> lock(m_tracker_mutex, std::try_to_lock);
if (!lock.owns_lock())
return;
if (!m_chain->contains(head))
return;
std::vector<uint256> to_send;
int32_t height = m_chain->get_height(head);
int32_t walk = std::min(height, 5);
for (auto [hash, data] : m_chain->get_chain(head, walk)) {
if (m_rejected_share_hashes.count(hash))
continue; // never re-broadcast peer-rejected shares
to_send.push_back(hash);
}
if (to_send.empty())
return;
auto now = std::chrono::steady_clock::now();
for (auto& [nonce, peer] : m_peers) {
send_shares(peer, to_send);
m_last_broadcast_to[peer->addr()] = {to_send, now};
}
LOG_INFO << "[readvertise] re-pushed " << to_send.size()
<< " head share(s) to " << m_peers.size() << " peer(s) (ROOT-2)";
}
void NodeImpl::download_shares(peer_ptr /*unused_peer*/, const uint256& target_hash)
{
// download_shares(): C++ implementation of the p2pool share-download loop.
//
// Key differences from old c2pool implementation:
// 1. RANDOM peer selection (not the reporting peer)
// 2. RANDOM parent count 0-499 (not fixed 500)