-
Notifications
You must be signed in to change notification settings - Fork 15
Expand file tree
/
Copy pathnode.cpp
More file actions
1545 lines (1413 loc) · 72.7 KB
/
Copy pathnode.cpp
File metadata and controls
1545 lines (1413 loc) · 72.7 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 "share.hpp"
#include "mint_runloop.hpp" // dash::mint::elect_best_share (election policy SSOT)
#include "known_txs_retention.hpp" // dash::retain_template_txs / all_txs_backable (F1/F3)
#include "think_gate.hpp" // dash::think:: think-slot protocol + clean-cycle tip decision (#854)
#include <cassert>
#include <core/uint256.hpp>
#include <core/common.hpp>
#include <core/random.hpp>
#include <core/version_gate.hpp> // core::version_gate::V36_ACTIVATION_VERSION (ratchet v36 guard)
#include <boost/asio/post.hpp>
#include <atomic>
#include <chrono>
#include <cstring>
#include <memory>
#include <vector>
// Dash p2pool sharechain pool-node — S8 pool-node reception, slice .4 (bodies).
//
// SLICE SCOPE (integrator-confirmed 2026-07-09, #656 a1444b4 branch-tip (awaiting merge tap)): the two
// link-deferred declarations from node.hpp — processing_shares() and
// handle_get_share() — get their REDUCED dash-native bodies here. "Reduced"
// means: the parallel X11 share_init_verify -> try_lock + m_tracker.add core,
// with think()/download/best-share/persist DEFERRED to their own later slices.
//
// This is a deliberate, non-1:1 port of src/impl/ltc/node.cpp. The DASH node is
// header-only and does NOT carry the ~8 LTC-only symbols the full ltc bodies
// touch (run_think/think, chain::PreparedList, m_raw_share_cache,
// m_best_share_hash, chain::get_reverse fork-logging, m_rejected_share_hashes,
// processing_shares_phase2 as a distinct TU method). Those are intentionally
// absent — force-fitting them would be scope creep. What remains is the minimal
// admit path that makes the Legacy/Actual dispatch (protocol_{legacy,actual}.cpp)
// actually reach the tracker: verify in parallel off the io_context, then insert
// under the non-blocking tracker lock on the io_context thread.
namespace dash
{
void NodeImpl::processing_shares(HandleSharesData& data_ref, NetService addr)
{
// Take ownership immediately so the caller (the dispatch handler) can return
// and free its local HandleSharesData.
auto data = std::make_shared<HandleSharesData>(std::move(data_ref));
size_t n = data->m_items.size();
if (n == 0)
return;
// ── Phase 1 (m_verify_pool, parallel) ────────────────────────────────
// Run share_init_verify() for each received share off the io_context thread.
// DASH is X11 (~ms of CPU per share); the structural + PoW verify must NOT
// block network I/O. Each share's hash computation is independent, so the
// batch fully parallelises across the pool. A share that throws is left with
// a null hash and skipped in phase 2.
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_coin_params, true);
});
}
catch (const std::exception&)
{
// leave hash null — phase 2 will skip this share
}
}
// Last verify done → hop back to the io_context thread for the
// tracker insertion (writes stay single-threaded on io_context).
if (--(*remaining) == 0)
{
boost::asio::post(*m_context,
[data, this, addr]()
{
add_verified_shares(*data, addr);
});
}
});
}
}
void NodeImpl::add_verified_shares(HandleSharesData& data, NetService addr)
{
// io_context thread. Non-blocking tracker lock (architectural rule,
// node.hpp): the compute thread (a later slice's think()) takes the
// exclusive lock; the IO thread must NEVER block on it. If think() holds the
// lock, defer this batch onto m_pending_adds — the compute thread drains it
// after releasing. Until the think() slice lands nothing takes the exclusive
// lock, so try_to_lock always succeeds here.
{
std::unique_lock lock(m_tracker_mutex, std::try_to_lock);
if (!lock.owns_lock())
{
if (m_pending_adds.size() < MAX_PENDING_ADDS)
{
LOG_INFO << "[ASYNC-DEFER] add_verified_shares: tracker 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});
}
else
{
LOG_WARNING << "[ASYNC-DEFER] add_verified_shares: pending queue full ("
<< MAX_PENDING_ADDS << ") — dropping batch from "
<< addr.to_string();
}
return;
}
// Lock held for the whole insertion below.
int32_t new_count = 0;
int32_t dup_count = 0;
std::vector<c2pool::storage::SharechainStorage::ShareBatchEntry> db_batch;
for (auto& share : data.m_items)
{
// Skip shares that failed phase-1 verification (hash still null).
if (share.hash().IsNull())
continue;
if (m_chain->contains(share.hash()))
{
++dup_count;
continue;
}
m_tracker.add(share);
++new_count;
// Collect for the atomic LevelDB batch persist (btc parity:
// 8-byte version prefix + packed contents; committed after loop).
if (m_storage && m_storage->is_available())
collect_share_batch_entry(share, db_batch);
}
// Commit all shares atomically (one WriteBatch — crash-safe).
if (!db_batch.empty() && m_storage && m_storage->is_available())
m_storage->store_shares_batch(db_batch);
if (new_count > 0)
{
auto as = addr.to_string();
std::string source = (addr.port() == 0) ? as.substr(0, as.rfind(':')) : as;
LOG_INFO << "Processing " << new_count << " shares from " << source
<< "... (dup=" << dup_count
<< " chain=" << m_tracker.chain.size() << ")";
}
// Lock released at scope exit; think() trigger below runs lock-free.
if (new_count == 0)
return;
}
// p2pool: set_best_share() after EVERY batch with new shares — think()
// verifies, scores heads, and elects m_best_share_hash so minted shares
// build on the live tip.
run_think();
}
// Serialize one share into a LevelDB batch entry (btc node.cpp parity:
// [8B version LE][packed contents]; height slot carries absheight so the
// load path's height-ordered scan replays parents before children).
void NodeImpl::collect_share_batch_entry(
ShareType& share,
std::vector<c2pool::storage::SharechainStorage::ShareBatchEntry>& out)
{
PackStream ps = pack(share);
auto span = ps.get_span();
uint64_t ver = share.version();
std::vector<uint8_t> versioned;
versioned.resize(8 + span.size());
std::memcpy(versioned.data(), &ver, 8);
std::memcpy(versioned.data() + 8,
reinterpret_cast<const uint8_t*>(span.data()), span.size());
share.invoke([&](auto* obj) {
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;
out.push_back(std::move(entry));
});
}
std::vector<dash::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): the IO thread MUST
// never block on m_tracker_mutex. An empty reply does not disconnect the
// requesting peer — p2pool's downloader retries against another random peer,
// so a busy-skip just shifts the request one 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 {};
}
if (hashes.empty())
return {};
parents = std::min(parents, (uint64_t)1000 / hashes.size());
std::vector<dash::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();
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;
shares.push_back(data.share);
}
}
if (!shares.empty())
LOG_INFO << "[Pool] Sending " << shares.size() << " shares to " << peer_addr.to_string();
return shares;
}
// ════════════════════════════════════════════════════════════════════════════
// Run-loop mint slice (3/3) bodies — ports of the btc::NodeImpl prod
// reference (src/impl/btc/node.cpp), reconciled to the DASH pool-node.
// ════════════════════════════════════════════════════════════════════════════
void NodeImpl::init_storage(const std::string& net_name)
{
if (m_storage)
return; // idempotent — run-loop calls once
m_storage = std::make_unique<c2pool::storage::SharechainStorage>(net_name);
// p2pool known_verified pattern: newly verified hashes buffer, flushed in
// batches (and at shutdown).
m_tracker.m_on_share_verified = [this](const uint256& hash) {
m_verified_flush_buf.push_back(hash);
if (m_verified_flush_buf.size() >= 50)
flush_verified_to_leveldb();
};
// Pruned shares → batched LevelDB deletion (flushed at shutdown; unflushed
// leftovers get pruned by the next startup's load_persisted_shares).
m_tracker.chain.on_removed([this](const uint256& hash) {
m_removal_flush_buf.push_back(hash);
if (m_removal_flush_buf.size() >= 200 && m_storage && m_storage->is_available()) {
m_storage->remove_shares_batch(m_removal_flush_buf);
m_removal_flush_buf.clear();
}
});
load_persisted_shares();
}
void NodeImpl::load_persisted_shares()
{
if (!m_storage || !m_storage->is_available())
return;
auto all_hashes = m_storage->get_shares_by_height_range(0, UINT64_MAX);
if (all_hashes.empty()) {
LOG_INFO << "[Pool] No persisted DASH shares found in LevelDB";
return;
}
// Only the newest window is loaded (LevelDB accumulates forever).
const size_t keep = static_cast<size_t>(SharechainConfig::chain_length()) * 2 + 10;
const size_t total_in_db = all_hashes.size();
size_t skip = (total_in_db > keep) ? (total_in_db - keep) : 0;
int loaded = 0, skipped = 0;
std::vector<uint256> verified_hashes;
for (size_t i = skip; i < total_in_db; ++i)
{
const auto& hash = all_hashes[i];
std::vector<uint8_t> data;
core::ShareMetadata meta;
if (!m_storage->load_share(hash, data, meta) || data.size() < 8) {
++skipped;
continue;
}
try {
uint64_t ver;
std::memcpy(&ver, data.data(), 8);
chain::RawShare rshare(ver, PackStream(
std::vector<unsigned char>(data.begin() + 8, data.end())));
auto share = dash::load_share(rshare, NetService{"database", 0});
// m_hash is not serialized — restore from the LevelDB key.
share.ACTION({ obj->m_hash = hash; });
if (m_chain->contains(share.hash())) {
++skipped;
continue;
}
m_tracker.add(share);
++loaded;
// Restore the cached X11 pow_hash (attempt_verify recomputes and
// re-caches when missing).
if (auto* idx = m_tracker.chain.get_index(hash); idx && !meta.pow_hash.IsNull())
idx->pow_hash = meta.pow_hash;
if (meta.is_verified)
verified_hashes.push_back(hash);
} catch (const std::exception& e) {
LOG_WARNING << "[Pool] failed to load persisted share "
<< hash.GetHex().substr(0, 16) << ": " << e.what();
}
}
// Pre-populate the verified subset (p2pool node.py known_verified); the
// height-ordered scan guarantees parent-before-child.
int pre_verified = 0;
for (const auto& vh : verified_hashes) {
if (m_tracker.chain.contains(vh) && !m_tracker.verified.contains(vh)) {
try {
m_tracker.verified.add(m_tracker.chain.get_share(vh));
++pre_verified;
} catch (...) {}
}
}
LOG_INFO << "[Pool] Loaded " << loaded << " persisted DASH shares from LevelDB"
<< " (db_total=" << total_in_db << " window=" << keep
<< " pre_verified=" << pre_verified << " skipped=" << skipped << ")";
// Prune everything older than the loaded window.
if (skip > 0) {
std::vector<uint256> prune(all_hashes.begin(),
all_hashes.begin() + static_cast<long>(skip));
m_storage->remove_shares_batch(prune);
LOG_INFO << "[Pool] Pruned " << prune.size() << " old shares from LevelDB";
}
// Seed the best-share election + snapshot BEFORE the compute thread starts
// (still single-threaded here — called once from the run loop pre-serving).
// Without this, m_best_share_hash and TrackerSnapshot.best_share are null
// until the FIRST think() finishes, and the first think()'s bootstrap
// mass-verify holds the exclusive lock for seconds; over that window
// best_share_hash()'s busy-fallback serves null → miners reconnecting at
// restart get dead time (fail-closed — no bad payout — but avoidable). One
// synchronous election over the just-loaded verified chain gives the tip
// immediately. has_peers=false: no peers exist yet at load time, so the
// election bootstraps off the verified/raw chain rather than refusing.
m_best_share_hash =
dash::mint::elect_best_share(m_tracker, m_best_share_hash, /*has_peers=*/false);
publish_snapshot();
if (!m_best_share_hash.IsNull())
LOG_INFO << "[Pool] Seeded best share from persisted chain: "
<< m_best_share_hash.GetHex().substr(0, 16);
}
void NodeImpl::flush_verified_to_leveldb()
{
if (m_verified_flush_buf.empty() || !m_storage || !m_storage->is_available())
return;
std::vector<std::pair<uint256, uint256>> hash_pow_pairs;
hash_pow_pairs.reserve(m_verified_flush_buf.size());
for (const auto& hash : m_verified_flush_buf) {
uint256 pow;
if (auto* idx = m_tracker.chain.get_index(hash))
pow = idx->pow_hash;
hash_pow_pairs.emplace_back(hash, pow);
}
m_storage->mark_shares_verified_with_pow(hash_pow_pairs);
m_verified_flush_buf.clear();
}
void NodeImpl::shutdown_persistence()
{
// Teardown runs on the MAIN thread after ioc.run() returns, but a think()
// cycle may still be in flight on the compute thread, mutating
// m_verified_flush_buf / the chain under the exclusive lock (TSAN-captured
// during the join-burst repro: flush_verified_to_leveldb() here racing
// attempt_verify's m_on_share_verified push_back). Blocking here is fine —
// the IO loop is already stopped, so the IO-never-blocks rule does not
// apply; we just wait out the tail end of the last think() cycle.
std::unique_lock lock(m_tracker_mutex);
flush_verified_to_leveldb();
if (!m_removal_flush_buf.empty() && m_storage && m_storage->is_available()) {
m_storage->remove_shares_batch(m_removal_flush_buf);
m_removal_flush_buf.clear();
}
}
void NodeImpl::apply_min_protocol_ratchet()
{
// Runtime wiring of the v36 accept-floor ratchet (auto_ratchet.hpp pure fns).
// Structural mirror of dgb::NodeImpl::apply_min_protocol_ratchet (src/impl/dgb/
// node.cpp:815), called at the same best-share-advance sites. Lifts the inbound
// P2P accept-floor 1700 -> 3600 once the best share's DESIRED version holds
// >= 95% of the work-weighted desired-version tally over the [9/10..10/10] window
// behind the best share's parent — the SAME window the 60% successor gate reads
// (version_negotiation.hpp). MUST run under the exclusive m_tracker_mutex.
const uint32_t target = SharechainConfig::NEW_MINIMUM_PROTOCOL_VERSION; // 3600
const uint32_t current =
m_runtime_min_protocol_version.load(std::memory_order_relaxed);
if (current >= target) // already ratcheted -> latched, no-op
return;
if (m_best_share_hash.IsNull() || !m_tracker.chain.contains(m_best_share_hash))
return;
// DASH DIVERGENCE FROM dgb (flagged for integrator): dgb keys the ratchet on the
// best share's static TYPE version (35 -> 36 after the format switch). DASH has
// NO v36 share TYPE — DashShare is permanently wire-type 16 — so "the best share
// is v36" is expressed by its m_desired_version VOTE reaching 36, and that vote is
// what the work-weighted tally is keyed by. Using the static type here would check
// weights[16] (always ~100% pre-crossing) and FALSELY lift the floor; using the
// vote checks weights[36], which is ~0 until the crossing actually happens.
int64_t best_desired = 0;
uint256 prev_hash;
m_tracker.chain.get_share(m_best_share_hash).invoke([&](auto* obj) {
best_desired = static_cast<int64_t>(obj->m_desired_version);
prev_hash = obj->m_prev_hash;
});
// SAFETY GUARD (DASH-specific): only a best share that itself desires v36 can lift
// the floor. Without this, a fully-agreed pre-crossing chain (everyone desires 16)
// would satisfy weights[best_desired=16] >= 95% and spuriously ratchet to 3600,
// partitioning the pool from every legacy 1700 peer. dgb gets this for free because
// its target floor is reachable only once the best share TYPE is already 36.
if (best_desired < static_cast<int64_t>(
core::version_gate::V36_ACTIVATION_VERSION)) // < 36
return;
if (prev_hash.IsNull() || !m_tracker.chain.contains(prev_hash))
return;
const int32_t CL = static_cast<int32_t>(SharechainConfig::chain_length());
const int32_t parent_height = m_tracker.chain.get_height(prev_hash);
// Sample the SAME window the 60% switch gate reads (version_negotiation.hpp
// negotiation_window): anchor = nth_parent(parent, CHAIN_LENGTH*9/10),
// size = CHAIN_LENGTH/10.
const uint32_t window_start = (static_cast<uint32_t>(CL) * 9) / 10;
const uint32_t window_size = static_cast<uint32_t>(CL) / 10;
const uint256 anchor = m_tracker.chain.get_nth_parent_key(
prev_hash, static_cast<int32_t>(window_start));
auto weights = dash::version_negotiation::get_desired_version_weights(
m_tracker.chain, anchor, window_size);
// apply_min_protocol_ratchet_decision applies the full-window guard (parent_height
// >= CHAIN_LENGTH) the pure ratchet omits, then delegates to the floor-divided 95%
// work-weighted gate.
const uint32_t lifted = dash::apply_min_protocol_ratchet_decision(
parent_height, CL, weights, best_desired, current, target);
if (lifted != current) {
// Publish via the ATOMIC only. The IO-thread handshake (handle_version)
// reads m_runtime_min_protocol_version.load() and composes it with the
// operator knob (m_min_protocol_gate) via max(), so the ratcheted floor is
// enforced immediately with NO data race on the plain-uint32 gate member
// (which stays IO-thread-owned / operator-set).
m_runtime_min_protocol_version.store(lifted, std::memory_order_relaxed);
LOG_INFO << "[min-proto-ratchet] MINIMUM_PROTOCOL_VERSION " << current
<< " -> " << lifted << " (>=95% window work desires v"
<< best_desired << ", best="
<< m_best_share_hash.GetHex().substr(0, 16) << ")";
}
}
void NodeImpl::run_think()
{
// THREAD-CONFINEMENT (enforced, not assumed — register_template_txs
// precedent): the think-slot flags (m_think_running / m_rethink_pending)
// are IO-thread-only. Every caller is on the single ioc.run() thread: the
// mint hook (add_local_share), the reception path (add_verified_shares,
// which posts itself back to m_context after the verify pool), the 2 s
// advert drain, and both cycles' IO-phase epilogues. The compute thread
// NEVER touches them — it only runs the tracker under the exclusive lock.
// A compute-thread caller would break the "release then re-post" handshake
// below (it would re-enter the slot it still owns), so trip loudly in
// debug builds rather than let it be discovered in production.
assert(!is_compute_thread() &&
"run_think must not be called from the compute thread — the think "
"slot (m_think_running/m_rethink_pending) is IO-thread-confined");
// Serialize: only one think() in flight (compute pool has 1 thread).
// #854: a skipped request is no longer DROPPED — acquire_think_slot()
// records it in m_rethink_pending and the cycle that owns the slot
// re-posts run_think() when it releases. Without that, a local mint or a
// peer best-advert landing during a think/clean IO-phase (tracker lock
// already released, flag still true) silently loses its re-election, so
// the tip change never reaches the rigs until the 25 s keepalive.
if (!dash::think::acquire_think_slot(m_think_running, m_rethink_pending)) {
static int skip_log = 0;
if (skip_log++ % 20 == 0)
LOG_INFO << "[ASYNC-THINK] skipped — compute thread busy (re-think queued)";
return;
}
if (!m_context) {
// Rig-free (KAT/standalone) node: no IO context to hop back to, so
// there is nowhere to post a queued re-think either — drop it.
(void)dash::think::release_think_slot(m_think_running, m_rethink_pending);
return;
}
auto block_rel_height = m_block_rel_height_fn
? m_block_rel_height_fn
: std::function<int32_t(uint256)>([](uint256) -> int32_t { return 0; });
boost::asio::post(m_think_pool, [this, block_rel_height]() {
m_compute_thread_id.store(std::this_thread::get_id(), std::memory_order_relaxed);
TrackerThinkResult result;
bool best_changed = false;
bool needs_continue = false;
int64_t think_ms = 0;
try {
std::unique_lock lock(m_tracker_mutex); // exclusive — IO defers
// Bootstrap: no verified chain yet → verify everything in one
// pass (p2pool think() runs synchronously during initial sync).
const bool bootstrap = m_tracker.verified.size() == 0;
auto t0 = std::chrono::steady_clock::now();
result = m_tracker.think(block_rel_height,
/*previous_block=*/uint256(),
/*bits=*/0, bootstrap);
think_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - t0).count();
m_last_top5_heads = std::move(result.top5_heads);
if (!result.best.IsNull()) {
best_changed = (m_best_share_hash != result.best);
m_best_share_hash = result.best;
apply_min_protocol_ratchet(); // v36 accept-floor ratchet (dgb node.cpp:1526 parallel)
}
publish_snapshot();
needs_continue = m_tracker.m_think_needs_continue
|| m_tracker.m_think_walk_needs_continue;
flush_verified_to_leveldb();
} catch (const std::exception& e) {
LOG_ERROR << "run_think() failed on compute thread: " << e.what();
} catch (...) {
LOG_ERROR << "run_think() failed on compute thread: unknown error";
}
// ── exclusive lock released ──
LOG_INFO << "[ASYNC-THINK] compute done in " << think_ms << "ms"
<< " best_changed=" << best_changed
<< " needs_continue=" << needs_continue
<< " bads=" << result.bad_peer_addresses.size()
<< " desired=" << result.desired.size();
boost::asio::post(*m_context, [this, result = std::move(result),
best_changed, needs_continue]() {
// #755 guard (ltc IO-phase parity, node.cpp:1580/1670): an exception
// escaping this handler would propagate out of ioc.run() and
// TERMINATE the node (Exit 134) — e.g. a chain-walk throw over a
// fragmented/unrooted restart chain. Catch, log, and keep serving.
try {
// Ban peers that provided invalid/unverifiable shares (btc
// node.cpp:1585-1603 port). Whitelisted bootstrap seeds are
// immune (is_banned bypass); port-0 entries are database-loaded
// addresses, not connectable peers.
{
const auto now = std::chrono::steady_clock::now();
for (const auto& bad_addr : result.bad_peer_addresses) {
if (bad_addr.port() == 0)
continue;
LOG_WARNING << "run_think: banning peer " << bad_addr.to_string()
<< " for unverifiable shares";
m_ban_list[bad_addr] = now + m_ban_duration;
}
// Expire old bans.
for (auto it = m_ban_list.begin(); it != m_ban_list.end(); ) {
if (it->second <= now) it = m_ban_list.erase(it);
else ++it;
}
for (auto it = m_ip_ban_list.begin(); it != m_ip_ban_list.end(); ) {
if (it->second <= now) it = m_ip_ban_list.erase(it);
else ++it;
}
}
// #754 share-download leg (p2pool node.py download loop; ltc
// node.cpp:1600-1618 port): reset the per-cycle gate — p2pool
// re-adds desired hashes from scratch every cycle with sleep(1)
// backoff; permanent blacklisting stalls bootstrap — then request
// each desired missing parent from a RANDOM peer.
m_download_gate.new_cycle();
drain_peer_best_adverts();
if (!result.desired.empty() && !m_peers.empty()) {
for (const auto& [peer_addr, hash] : result.desired) {
(void)peer_addr; // oracle: random peer, not the reporter
auto peer_it = m_peers.begin();
if (m_peers.size() > 1)
std::advance(peer_it,
core::random::random_uint256().GetLow64() % m_peers.size());
download_shares(peer_it->second, hash);
}
}
if (best_changed) {
LOG_INFO << "[ASYNC-THINK] best=" << m_best_share_hash.GetHex().substr(0, 16)
<< " — refreshing work + re-advertising";
if (m_on_best_share_changed)
m_on_best_share_changed();
// Re-announce our head so peers pull it (btc ROOT-2 parity).
broadcast_share(m_best_share_hash);
}
drain_pending_adds();
// #854: release_think_slot() reports whether a run_think() request
// was dropped while this cycle held the slot — e.g. the mint that
// drain_pending_adds()/add_local_share() just landed. Serve it, or
// that share's election (and the miner notify it drives) is lost
// until the next timer tick.
const bool rethink_owed =
dash::think::release_think_slot(m_think_running, m_rethink_pending);
if (needs_continue || rethink_owed)
boost::asio::post(*m_context, [this]() { run_think(); });
} catch (const std::exception& e) {
LOG_ERROR << "run_think() IO phase failed: " << e.what();
if (dash::think::release_think_slot(m_think_running, m_rethink_pending))
boost::asio::post(*m_context, [this]() { run_think(); });
} catch (...) {
LOG_ERROR << "run_think() IO phase failed: unknown error";
if (dash::think::release_think_slot(m_think_running, m_rethink_pending))
boost::asio::post(*m_context, [this]() { run_think(); });
}
});
});
}
void NodeImpl::drain_pending_adds()
{
if (m_pending_adds.empty())
return;
auto pending = std::move(m_pending_adds);
m_pending_adds.clear();
LOG_INFO << "[ASYNC-THINK] draining " << pending.size() << " deferred share batch(es)";
for (auto& batch : pending)
add_verified_shares(*batch.data, batch.addr);
}
// ════════════════════════════════════════════════════════════════════════════
// #754 share-download leg — faithful port of the PROVEN ltc downloader
// (src/impl/ltc/node.cpp:968-1105 download_shares, :1289-1327
// start_outbound_connections), coin-agnostic planning/gating shared via
// pool/share_download.hpp. This is what lets an EMPTY c2pool-dash node JOIN an
// established p2pool-dash sharechain: request the missing ancestors, insert
// them through the SAME processing_shares pipeline reception uses, and let
// think() root + verify the previously-orphaned live-pushed shares.
// ════════════════════════════════════════════════════════════════════════════
void NodeImpl::download_shares(peer_ptr peer, const uint256& target_hash)
{
// C++ implementation of the p2pool share-download loop (ltc parity):
// 1. de-dup + per-cycle fail gate (no re-request while in flight);
// 2. RANDOM parent count 0..499 (oracle: random.randrange(500));
// 3. STOPS list: known heads + their 10th parents (bounds the reply);
// 4. on reply: feed processing_shares, then continue from the oldest
// received share's parent until the chain roots.
if (target_hash.IsNull())
return;
if (!m_download_gate.try_begin(target_hash))
return; // already in flight, or failed out this think() cycle
// p2pool: if len(self.peers) == 0: sleep(1); continue
if (m_peers.empty()) {
m_download_gate.abort(target_hash);
return;
}
// Ask the supplied peer when it is still connected (the handshake-advert
// path asks the ADVERTISING peer, oracle p2p.py handle_version →
// handle_share_hashes); fall back to a random peer (oracle download loop:
// peer = random.choice(self.peers.values()); ltc:1002-1007).
if (!peer || !m_connections.contains(peer->addr())) {
auto peer_it = m_peers.begin();
if (m_peers.size() > 1)
std::advance(peer_it,
core::random::random_uint256().GetLow64() % m_peers.size());
peer = peer_it->second;
}
// p2pool: parents=random.randrange(500)
const uint64_t parents =
core::random::random_uint256().GetLow64() % pool::download::PARENTS_RANGE;
// stops need a tracker read — try_to_lock per the architectural rule (the
// IO thread NEVER blocks on m_tracker_mutex). If think() holds it, skip;
// the next think() cycle re-derives desired and retries.
std::vector<uint256> stops;
{
std::shared_lock<std::shared_mutex> lock(m_tracker_mutex, std::try_to_lock);
if (!lock.owns_lock()) {
m_download_gate.abort(target_hash);
return;
}
stops = pool::download::build_stops(m_tracker.chain);
}
const uint256 req_id = core::random::random_uint256();
std::vector<uint256> hashes = { target_hash };
// Track req_id → peer for selective cancellation on disconnect.
m_pending_share_reqs[req_id] = peer->addr();
LOG_INFO << "[Pool] Requesting parent share "
<< target_hash.ToString().substr(0, 16)
<< " from " << peer->addr().to_string()
<< " (parents=" << parents << " stops=" << stops.size() << ")";
// weak_ptr prevents use-after-free if the peer disconnects before reply.
std::weak_ptr<peer_t> weak_peer = peer;
const auto peer_addr_for_log = peer->addr();
request_shares(req_id, peer, hashes, parents, stops,
[this, weak_peer, target_hash, peer_addr_for_log, req_id]
(dash::ShareReplyData reply)
{
m_pending_share_reqs.erase(req_id);
if (reply.m_items.empty())
{
// Empty reply = timeout, cancel, or peer had no match.
const int fails = m_download_gate.on_empty(target_hash);
LOG_INFO << "[Pool] Share request empty for "
<< target_hash.ToString().substr(0, 16)
<< " from " << peer_addr_for_log.to_string()
<< " (fail " << fails << "/"
<< pool::download::MAX_EMPTY_RETRIES << ")";
return;
}
m_download_gate.on_success(target_hash);
LOG_INFO << "[Pool] Received " << reply.m_items.size()
<< " share(s) for download request "
<< target_hash.ToString().substr(0, 16);
// Feed the SAME reception pipeline live pushes ride: parallel
// X11 verify → tracker insert → run_think (verify/root + next
// desired set).
HandleSharesData data;
for (size_t idx = 0; idx < reply.m_items.size(); ++idx)
{
if (idx < reply.m_raw_items.size())
data.add(reply.m_items[idx], {}, reply.m_raw_items[idx]);
else
data.add(reply.m_items[idx], {});
}
processing_shares(data, peer_addr_for_log);
// Backfill continuation: oldest received share's parent still
// unknown → keep pulling (ltc:1093-1102). This drives the full
// history download without waiting one think() cycle per chunk.
//
// contains() reads the chain map — reader discipline: shared
// lock (try_to_lock, IO thread never blocks). During the join
// burst think() holds the exclusive lock most of the time, and
// this callback fires on EVERY sharereply — the hottest of the
// formerly-unlocked walks. Busy ⇒ queue the continuation on the
// advert queue; the 2s advert timer / think()-IO-phase drain
// re-checks under its own lock and resumes the backfill.
const uint256 next = pool::download::oldest_parent(reply.m_items);
if (!next.IsNull())
{
bool known = false;
{
std::shared_lock<std::shared_mutex> lk(m_tracker_mutex,
std::try_to_lock);
if (!lk.owns_lock()) {
m_peer_best_adverts.emplace_back(weak_peer, next);
return;
}
known = m_chain->contains(next);
}
if (!known)
{
if (auto locked = weak_peer.lock())
download_shares(locked, next);
}
}
});
}
void NodeImpl::drain_peer_best_adverts()
{
if (m_peer_best_adverts.empty())
return;
// contains() below reads the chain map — reader discipline: shared lock
// (try_to_lock, IO thread never blocks). Busy ⇒ leave the queue intact;
// the next 2s advert tick / think()-IO-phase drain retries. The lock is
// scoped to the contains() partition only — download_shares()/run_think()
// take their own locks.
std::vector<std::pair<std::weak_ptr<peer_t>, uint256>> to_download;
bool rethink = false;
{
std::shared_lock<std::shared_mutex> lk(m_tracker_mutex, std::try_to_lock);
if (!lk.owns_lock())
return;
auto adverts = std::move(m_peer_best_adverts);
m_peer_best_adverts.clear();
for (auto& [weak_peer, best] : adverts)
{
if (weak_peer.expired())
continue;
if (m_chain->contains(best))
{
// Known share: re-run think() to re-evaluate the best chain
// with the peer's perspective (ltc node.cpp:328-334 —
// critical after restart, when LevelDB-loaded shares carry a
// stale election).
rethink = true;
continue;
}
to_download.emplace_back(weak_peer, best);
}
}
// #854 (was a TODO here): when drain_peer_best_adverts runs FROM the
// think() IO-phase, m_think_running is still true, so this run_think()
// cannot start a cycle. It is no longer a no-op — acquire_think_slot()
// records the request in m_rethink_pending and the owning cycle re-posts
// run_think() when it releases, so the re-election is guaranteed rather
// than left to the next 2 s advert-timer tick.
if (rethink)
run_think();
for (auto& [weak_peer, best] : to_download)
{
if (auto peer = weak_peer.lock())
download_shares(peer, best);
}
}
void NodeImpl::start_outbound_connections()
{
if (!m_context)
return; // rig-free (KAT/standalone) node
// ── have_tx / losing_tx delta sweep ──────────────────────────────────
// Started before the outbound-dialing early-return below: a node with
// outbound dialing disabled still accepts inbound peers and must still
// advertise its tx pool to them. Advert-only, no consensus state.
m_tx_advert_timer = std::make_unique<core::Timer>(m_context, true);
m_tx_advert_timer->start(core::TX_ADVERT_INTERVAL_SECONDS,
[this]() { advertise_known_txs(); });
// ── canonical addr-discovery sweep (p2p.py:794-799) ──────────────────
// Also started before the dialing early-return: an inbound-only node needs to
// learn addresses. Self-limiting — getaddrs_sweep() is a no-op once the
// AddrStore reaches PREFERRED_ADDR_STORAGE (1000) or while we have no peers.
m_getaddrs_timer = std::make_unique<core::Timer>(m_context, true);
m_getaddrs_timer->start(20, [this]() { getaddrs_sweep(); });
// Advert-drain pump: handle_version (inline, link-free for the KAT
// targets) only QUEUES a peer's advertised best share; this timer is the
// node.cpp-side consumer that turns the queue into sharereq dispatches.
// 2s bounds the join latency; the check is O(1) when the queue is empty.
m_advert_timer = std::make_unique<core::Timer>(m_context, true);
m_advert_timer->start(2, [this]() {
drain_peer_best_adverts();
// IO thread: keep the display peer-info snapshot's uptimes fresh
// (peer add/remove refresh it on membership change; this ticks it so
// the dashboard card's uptime advances between events). Cheap — a few
// peers — and it is the reader-safe alternative to the HTTP thread
// ever touching m_peers directly.
publish_peer_info_snapshot();
});
if (m_target_outbound_peers == 0)
{
LOG_INFO << "[Pool] Outbound peer dialing disabled (target=0)";
return;
}
// btc/ltc node.cpp:1289-1327 port.
auto try_connect_peers = [this]()
{
const size_t outbound = m_outbound_addrs.size();
if (outbound >= m_target_outbound_peers || m_connections.size() >= m_max_peers)
return;
size_t needed = m_target_outbound_peers - outbound;
// Ask for a few extra in case some are already connected.
for (auto& ap : get_good_peers(needed + 4))
{
if (needed == 0)
break;
// Skip if already connected, already dialing, or banned.
if (m_connections.contains(ap.addr) || m_pending_outbound.contains(ap.addr)
|| is_banned(ap.addr))
continue;
LOG_INFO << "[Pool] Dialing outbound peer " << ap.addr.to_string();
m_pending_outbound.insert(ap.addr);
core::Client::connect(ap.addr);
--needed;
}
};
try_connect_peers(); // initial burst (--addnode/--connect seeds)
// Periodic maintenance — top up outbound peers every 30 seconds.
m_connect_timer = std::make_unique<core::Timer>(m_context, true);
m_connect_timer->start(30, try_connect_peers);
}
// ════════════════════════════════════════════════════════════════════════════
// clean_tracker — btc node.cpp:1878-2173 port (p2pool node.py:355-402:
// stale-head eating + tail dropping). Runs think + prune on the compute
// thread under the exclusive lock — chain modifications MUST NOT happen
// concurrently with think() or IO-thread reads. Scheduled by the run loop's
// periodic tick; without it the raw chain grows unbounded past
// 2*CHAIN_LENGTH+10.
// ════════════════════════════════════════════════════════════════════════════
void NodeImpl::clean_tracker()
{
// Prevent concurrent clean_tracker (timer re-entry safety).
if (m_clean_running.exchange(true))
return;
if (!m_context) {
m_clean_running.store(false);
return; // rig-free (KAT/standalone) node
}
// Take the think slot, which also blocks run_think() re-entry for the
// duration of the clean. #854: acquire_clean_slot() is a compare_exchange,
// closing the load()-then-store() check-then-act window this prologue used
// to carry as a TODO — the guard is now symmetric with the m_clean_running
// exchange() above. A failure records NO pending re-think: the think cycle
// that won the slot performs the same election clean's Step 1 would, and
// the periodic timer retries the prune shortly.
if (!dash::think::acquire_clean_slot(m_think_running)) {
m_clean_running.store(false);
return;
}
// Post the entire body to the compute thread: chain modifications happen
// under the exclusive lock, never concurrent with think() or IO reads.
boost::asio::post(m_think_pool, [this]() {
m_compute_thread_id.store(std::this_thread::get_id(), std::memory_order_relaxed);
bool clean_best_changed = false;
bool bootstrap = false;
try {
std::unique_lock lock(m_tracker_mutex); // exclusive
// #854: the tip AS THE CLEAN CYCLE FOUND IT. clean_best_changed is
// decided against this, not against the value Step 1 has already
// written — otherwise an election absorbed by Step 1 (the case a mint
// landing during a think IO-phase produces) makes Step 4's comparison
// trivially false and no work is pushed to the rigs. Read under the
// exclusive lock, on the compute thread, before either think() runs.
const uint256 best_at_entry = m_best_share_hash;
auto block_rel_height = m_block_rel_height_fn
? m_block_rel_height_fn
: std::function<int32_t(uint256)>([](uint256) -> int32_t { return 0; });
// Step 1: run think() inline (already holds the lock).
{
bootstrap = m_tracker.verified.size() == 0;
auto result = m_tracker.think(block_rel_height,
/*previous_block=*/uint256(),
/*bits=*/0, bootstrap);
m_last_top5_heads = std::move(result.top5_heads);
if (!result.best.IsNull()) {
m_best_share_hash = result.best;
apply_min_protocol_ratchet(); // v36 accept-floor ratchet (dgb node.cpp:1905 parallel)
}
flush_verified_to_leveldb();
}
const auto now_sec = static_cast<int64_t>(std::time(nullptr));