-
Notifications
You must be signed in to change notification settings - Fork 15
Expand file tree
/
Copy pathtest_threading.cpp
More file actions
497 lines (428 loc) · 20.1 KB
/
Copy pathtest_threading.cpp
File metadata and controls
497 lines (428 loc) · 20.1 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
/**
* Threading architecture regression tests.
*
* Covers the fixes introduced for 48h stress-test crashes/freezes:
*
* 1. ScryptControlledByCheckPowFlag
* share_init_verify(share, false) skips scrypt and is measurably faster
* than the same number of raw scrypt calls. Regression guard: if someone
* removes the check_pow guard, each "fast" call also runs scrypt, so the
* fast loop approaches ~2x the scrypt baseline — the margin assertion
* catches that. Skipped under sanitizer builds (wall-clock unreliable).
*
* 2. VerifyShareUsesPresetHashToSkipScrypt
* After Phase-1 stores a share's SHA256d hash in m_hash, the expression
* share_init_verify(share, share.m_hash.IsNull()) evaluates check_pow=false.
* This is the exact guard added to verify_share() to prevent the 40-95 s
* think() freeze.
*
* 3. ThreadPoolAtomicCounterHandoffToIoc
* Direct unit test of the processing_shares Phase-1 → Phase-2 pattern:
* - N tasks posted to thread_pool
* - atomic counter decremented by each task
* - when counter reaches 0, last task posts Phase-2 callback to io_context
* Validates ordering, single-firing, and correct counter value.
*
* 4. IocResponsiveDuringParallelVerification
* The core property we protect: while heavy CPU work (scrypt) runs on the
* thread pool, the io_context must remain responsive to timers and I/O.
* Fires a short timer on ioc while pool is busy — timer must fire before
* the pool finishes, proving no ioc stall.
*
* 5. ParallelShareInitVerifyDeterministic
* Runs share_init_verify on the same share from 4 threads simultaneously.
* All must produce the identical SHA256d hash, proving thread safety of
* the scrypt + SHA256d code path.
*
* 6. ConcurrentVectorElementModification
* Documents and validates the C++ standard guarantee that concurrent writes
* to distinct elements of std::vector<T> are safe — the exact pattern used
* in processing_shares() Phase 1.
*
* 7. Phase2SkipsNullHashShares
* Simulates Phase-1 producing a mix of verified (hash set) and failed
* (hash null) shares. Phase-2 filtering must emit only verified shares.
*/
#include <gtest/gtest.h>
#include <impl/ltc/share.hpp>
#include <impl/ltc/share_check.hpp>
#include <impl/ltc/params.hpp>
#include <sharechain/sharechain.hpp>
#include <core/pack.hpp>
#include <btclibs/crypto/scrypt.h>
#include <btclibs/crypto/sha256.h>
#include <boost/asio.hpp>
#include <atomic>
#include <chrono>
#include <cstring>
#include <future>
#include <memory>
#include <set>
#include <thread>
namespace io = boost::asio;
// ─── Shared fixture: a real V36 testnet share loaded from raw hex ────────────
static const char* SHARE_HEX =
"23fd9601fe00000020654f11363698fc9a54e43f126f294bd1a33b650148e8b6b"
"b532fc08500cb6966e8103066140b041db0022a77e3af9c1de80a16583bed2a61"
"79b63ed410b890b113cfd0fcd68bafa4096779b90503fd823100731a92d3226d6"
"839617a4b44785235374766374a575a756e6e43324a7a37325351747746544b68"
"dec14025000000000000fe2302a41fb37f52f6747afbbeae61462feaa40b8b365"
"5f8fb7af60843111101ec5f958e93b9a76bb46536bf807b1caef9635f432d982b"
"d907eb5050130b6ec00aeabc2bb9ca34c5f1ba0bd332fc3d217d9853754fe4279"
"7e32cf9ddddcab6f66ab8056f1b64efa2157281c406fc6a5d9de6db5e2adf63c8"
"6646a4edc91c51f86d74c707c0221e8828011ef310306675b3210073990593df0"
"d00000000000000000000000100000000000000c357550d5a390b342f665a3d85"
"3c039a626b803bb37976c20ba0b5ee5a56fceedc0220e67c088987582af73218c"
"99820276bbf0004c5c18f7dd691f9c4326bfd9930d5567a6d109fec00f4eca887"
"c42e80ddaa57df9bda8db8b277110a50a9a268b6";
static ltc::ShareType load_test_share()
{
PackStream ps;
ps.from_hex(SHARE_HEX);
chain::RawShare rshare;
ps >> rshare;
return ltc::load_share(rshare, NetService{"0.0.0.0", 0});
}
// LTC testnet CoinParams for the share-verify path (the loaded share is a
// real V36 *testnet* share). const + namespace-scope so the threaded test
// below reads it concurrently without capturing it per-lambda.
static const core::CoinParams g_test_params = ltc::make_coin_params(/*testnet=*/true);
// ─── Test 1: scrypt is controlled by check_pow flag ──────────────────────────
// Wall-clock timing is unreliable under sanitizer instrumentation: ASan/UBSan
// inflate the allocation-heavy verify path far more than the tight scrypt inner
// loop, distorting the device-relative comparison below. The ASan CI leg pairs
// -fsanitize=address with undefined, so detecting ASan covers that whole leg.
#if defined(__SANITIZE_ADDRESS__)
# define C2POOL_SANITIZER_TIMING_UNRELIABLE 1
#elif defined(__has_feature)
# if __has_feature(address_sanitizer) || __has_feature(thread_sanitizer) || __has_feature(memory_sanitizer)
# define C2POOL_SANITIZER_TIMING_UNRELIABLE 1
# endif
#endif
TEST(VerifyShareThreading, ScryptControlledByCheckPowFlag)
{
#ifdef C2POOL_SANITIZER_TIMING_UNRELIABLE
GTEST_SKIP() << "wall-clock scrypt timing is unreliable under sanitizer "
"instrumentation; this guard is covered on the non-sanitized legs";
#else
auto share = load_test_share();
// More reps than strictly necessary so per-call scheduling jitter averages
// out — the assertion is device-relative, not an absolute bound.
constexpr int SCRYPT_REPS = 40;
// --- Baseline: measure SCRYPT_REPS raw scrypt calls for reliable timing ---
char dummy_in[80] = {};
char dummy_out[32] = {};
auto t_scrypt_start = std::chrono::steady_clock::now();
for (int i = 0; i < SCRYPT_REPS; i++)
scrypt_1024_1_1_256(dummy_in, dummy_out);
auto t_scrypt_us = std::chrono::duration_cast<std::chrono::microseconds>(
std::chrono::steady_clock::now() - t_scrypt_start).count();
// scrypt-1024-1-1-256 is memory-hard; even on fast hardware takes > 50µs each
EXPECT_GT(t_scrypt_us, 50)
<< SCRYPT_REPS << " scrypt calls took " << t_scrypt_us << "µs total — unexpectedly fast";
// --- Fast path: same number of share_init_verify(check_pow=false) calls ---
// The fast path does the (non-scrypt) verify work W per call; the scrypt
// baseline does S per call. Empirically W ≈ S, so the healthy fast loop
// runs at ~1.0x the scrypt baseline. If the check_pow guard is removed,
// each fast call ALSO runs scrypt (W + S per call) and the fast loop jumps
// to ~2.0x the baseline. Assert the fast loop stays well under that: a 1.5x
// ceiling sits midway between the healthy ~1.0x and the regressed ~2.0x,
// absorbing the scheduling jitter that the old zero-margin EXPECT_LT could
// not (it flipped on a 2.3% timing wobble under instrumentation).
auto t_fast_start = std::chrono::steady_clock::now();
for (int i = 0; i < SCRYPT_REPS; i++)
{
share.ACTION({ ltc::share_init_verify(*obj, g_test_params, false); });
}
auto t_fast_us = std::chrono::duration_cast<std::chrono::microseconds>(
std::chrono::steady_clock::now() - t_fast_start).count();
// t_fast_us < 1.5 * t_scrypt_us, evaluated in integers to avoid float in the
// predicate (both operands are int64 microsecond counts).
EXPECT_LT(t_fast_us * 2, t_scrypt_us * 3)
<< SCRYPT_REPS << " share_init_verify(false) calls took " << t_fast_us
<< "µs vs " << t_scrypt_us << "µs for raw scrypt (ratio "
<< (t_scrypt_us ? (double)t_fast_us / t_scrypt_us : 0.0) << "x). "
"A ratio near 2x means check_pow=false is no longer skipping scrypt.";
#endif
}
// ─── Test 2: verify_share uses pre-set m_hash to skip scrypt ─────────────────
TEST(VerifyShareThreading, VerifyShareUsesPresetHashToSkipScrypt)
{
auto share = load_test_share();
// Before Phase 1: m_hash is zero (not yet computed)
uint256 initial_hash;
share.ACTION({ initial_hash = obj->m_hash; });
EXPECT_TRUE(initial_hash.IsNull())
<< "freshly loaded share should have null m_hash before Phase 1";
// Phase 1 pattern: compute and store hash (check_pow=true — full validation)
uint256 phase1_hash;
bool phase1_ok = true;
try {
share.ACTION({
obj->m_hash = ltc::share_init_verify(*obj, g_test_params, true);
phase1_hash = obj->m_hash;
});
} catch (const std::exception& e) {
// PoW may legitimately fail for a testnet share if the target is
// stricter than the hex data meets. Fall back to check_pow=false.
phase1_ok = false;
share.ACTION({
obj->m_hash = ltc::share_init_verify(*obj, g_test_params, false);
phase1_hash = obj->m_hash;
});
}
(void)phase1_ok;
EXPECT_FALSE(phase1_hash.IsNull()) << "Phase 1 must produce a non-null hash";
// After Phase 1: m_hash is set → IsNull() == false
bool is_null_after_phase1 = true;
share.ACTION({ is_null_after_phase1 = obj->m_hash.IsNull(); });
EXPECT_FALSE(is_null_after_phase1)
<< "m_hash must not be null after Phase 1 stores it";
// Simulate verify_share()'s guard: share_init_verify(share, share.m_hash.IsNull())
// With hash pre-set, IsNull()=false → check_pow=false → no scrypt.
// Result must equal the stored hash (SHA256d is deterministic).
uint256 verify_hash;
share.ACTION({
verify_hash = ltc::share_init_verify(*obj, g_test_params, obj->m_hash.IsNull());
});
EXPECT_EQ(verify_hash, phase1_hash)
<< "verify_share re-computation must return the same hash as Phase 1. "
"If this fails the IsNull() guard was bypassed.";
// Confirm timing: with hash pre-set, N calls should be faster than N scrypt calls.
// (Regression: if the guard is removed, this triggers N scrypt calls each time)
constexpr int N = 5;
char scrypt_in[80] = {}; char scrypt_out[32] = {};
auto t0 = std::chrono::steady_clock::now();
for (int i = 0; i < N; i++)
scrypt_1024_1_1_256(scrypt_in, scrypt_out);
auto n_scrypt_us = std::chrono::duration_cast<std::chrono::microseconds>(
std::chrono::steady_clock::now() - t0).count();
auto t1 = std::chrono::steady_clock::now();
for (int i = 0; i < N; i++)
{
share.ACTION({
ltc::share_init_verify(*obj, g_test_params, obj->m_hash.IsNull()); // check_pow=false
});
}
auto fast_us = std::chrono::duration_cast<std::chrono::microseconds>(
std::chrono::steady_clock::now() - t1).count();
// Generous margin: verify should be at most 10x scrypt time.
// On shared CI runners, timing jitter can make verify appear slower
// than scrypt for small N. The real-world speedup is ~400x.
EXPECT_LT(fast_us, n_scrypt_us * 10)
<< N << " verify-with-preset-hash calls took " << fast_us
<< "µs; " << N << " scrypt calls took " << n_scrypt_us << "µs.";
}
// ─── Test 3: processing_shares threading pattern ─────────────────────────────
//
// Validates the exact async pattern used in NodeImpl::processing_shares():
// • N tasks posted to a thread_pool
// • Each task decrements an atomic<int> counter
// • The task that decrements to 0 posts the Phase-2 callback to io_context
// • Phase-2 fires exactly once on the io_context thread
//
// This catches bugs in the atomic counter or post() ordering that would cause
// Phase 2 to never fire (freeze) or fire multiple times (corruption).
TEST(VerifyShareThreading, ThreadPoolAtomicCounterHandoffToIoc)
{
constexpr int N = 24; // divisible by 4 (thread pool size)
io::io_context ioc;
boost::asio::thread_pool pool(4);
// Work guard keeps ioc.run_for() alive until Phase 2 fires and resets it.
auto work = std::make_shared<io::executor_work_guard<io::io_context::executor_type>>(
io::make_work_guard(ioc));
auto remaining = std::make_shared<std::atomic<int>>(N);
std::atomic<int> phase2_fire_count{0};
for (int i = 0; i < N; i++)
{
boost::asio::post(pool,
[remaining, &ioc, &phase2_fire_count, work]()
{
// Simulate per-share CPU work (scrypt) with a short sleep
std::this_thread::sleep_for(std::chrono::milliseconds(2));
if (--(*remaining) == 0)
{
boost::asio::post(ioc,
[&phase2_fire_count, work]()
{
phase2_fire_count.fetch_add(1);
work->reset(); // release work guard → ioc.run_for() returns
});
}
});
}
// Run ioc until Phase-2 fires (work guard released) or 10s elapses
ioc.run_for(std::chrono::seconds(10));
pool.join(); // ensure all thread-pool tasks have finished
EXPECT_EQ(phase2_fire_count.load(), 1)
<< "Phase-2 must fire exactly once; fired " << phase2_fire_count.load()
<< " times. 0=never fired (atomic/post broken); >1=race in counter";
EXPECT_EQ(remaining->load(), 0)
<< "All " << N << " Phase-1 tasks must complete before Phase-2 fires";
}
// ─── Test 4: io_context stays responsive while thread pool does scrypt ────────
//
// The whole point of the threading architecture: heavy CPU work (scrypt) runs
// on thread_pool while io_context continues to service timers, P2P, Stratum.
// This test posts real scrypt work to a thread pool and simultaneously schedules
// a short timer on ioc. The timer must fire promptly while scrypt is running.
TEST(VerifyShareThreading, IocResponsiveDuringParallelVerification)
{
io::io_context ioc;
boost::asio::thread_pool pool(4);
constexpr int SCRYPT_TASKS = 16; // enough to keep pool busy for ~80ms
auto remaining = std::make_shared<std::atomic<int>>(SCRYPT_TASKS);
std::atomic<bool> pool_done{false};
std::atomic<bool> timer_fired{false};
auto work = std::make_shared<io::executor_work_guard<io::io_context::executor_type>>(
io::make_work_guard(ioc));
// Post scrypt work to thread pool (NOT ioc)
for (int i = 0; i < SCRYPT_TASKS; i++)
{
boost::asio::post(pool,
[remaining, &pool_done, &ioc, work]()
{
char in[80] = {}; char out[32] = {};
scrypt_1024_1_1_256(in, out); // ~20ms each
if (--(*remaining) == 0)
{
pool_done.store(true);
boost::asio::post(ioc, [work]() { work->reset(); });
}
});
}
// Schedule a timer on ioc that fires after 5ms — well before pool finishes
boost::asio::steady_timer timer(ioc, std::chrono::milliseconds(5));
timer.async_wait([&timer_fired](const boost::system::error_code& ec)
{
if (!ec) timer_fired.store(true);
});
ioc.run_for(std::chrono::seconds(30));
pool.join();
EXPECT_TRUE(timer_fired.load())
<< "io_context timer must fire while thread pool runs scrypt. "
"If false, scrypt is blocking the io_context thread.";
EXPECT_TRUE(pool_done.load())
<< "All scrypt tasks must complete.";
}
// ─── Test 5: parallel share_init_verify produces deterministic results ────────
//
// Runs share_init_verify on independent copies of the same share from 4 threads
// simultaneously. All must produce the same SHA256d hash, proving that the
// scrypt + SHA256d code path uses no shared mutable state.
TEST(VerifyShareThreading, ParallelShareInitVerifyDeterministic)
{
constexpr int THREADS = 4;
std::vector<ltc::ShareType> shares(THREADS);
for (int i = 0; i < THREADS; i++)
shares[i] = load_test_share();
std::vector<uint256> results(THREADS);
std::vector<std::thread> threads;
for (int i = 0; i < THREADS; i++)
{
threads.emplace_back([&shares, &results, i]()
{
shares[i].ACTION({
// Use check_pow=false to avoid PoW target mismatch on testnet data
results[i] = ltc::share_init_verify(*obj, g_test_params, false);
});
});
}
for (auto& t : threads) t.join();
// All results must be the same non-null hash
EXPECT_FALSE(results[0].IsNull()) << "share_init_verify must produce a non-null hash";
for (int i = 1; i < THREADS; i++)
{
EXPECT_EQ(results[i], results[0])
<< "Thread " << i << " produced different hash than thread 0. "
"share_init_verify has shared mutable state — NOT thread-safe!";
}
}
// ─── Test 6: concurrent writes to distinct vector elements are safe ──────────
//
// processing_shares() Phase 1 modifies data->m_items[i] from thread i.
// C++ guarantees this is safe when indices don't overlap (ISO 17.6.5.9).
// This test exercises the exact pattern to ensure no TSAN/ASAN failures.
TEST(VerifyShareThreading, ConcurrentVectorElementModification)
{
constexpr int N = 32;
std::vector<uint256> results(N);
boost::asio::thread_pool pool(4);
auto remaining = std::make_shared<std::atomic<int>>(N);
std::promise<void> done;
auto future = done.get_future();
for (int i = 0; i < N; i++)
{
boost::asio::post(pool,
[i, &results, remaining, &done]()
{
// Each thread writes only to results[i] — no overlap
char input[80] = {};
input[0] = static_cast<char>(i & 0xFF);
input[1] = static_cast<char>((i >> 8) & 0xFF);
uint256 hash;
CSHA256().Write(reinterpret_cast<const unsigned char*>(input), 80)
.Finalize(hash.begin());
results[i] = hash;
if (--(*remaining) == 0)
done.set_value();
});
}
ASSERT_EQ(future.wait_for(std::chrono::seconds(10)), std::future_status::ready)
<< "Concurrent vector element writes must complete within 10s";
pool.join();
// Verify each result is unique and non-null (different inputs → different hashes)
std::set<uint256> unique_hashes(results.begin(), results.end());
EXPECT_EQ(unique_hashes.size(), N)
<< "Expected " << N << " unique hashes from " << N << " unique inputs. "
"Duplicates suggest a data race on the vector.";
}
// ─── Test 7: Phase-2 filtering skips shares with null hash ───────────────────
//
// In processing_shares(), Phase 1 may fail to verify some shares (exception
// thrown, hash stays null). Phase 2 must skip those and only insert verified
// shares into the chain. This test simulates that filtering.
TEST(VerifyShareThreading, Phase2SkipsNullHashShares)
{
constexpr int TOTAL = 10;
constexpr int FAIL_EVERY = 3; // shares 0, 3, 6, 9 will "fail"
// Simulate Phase-1 output: a vector of shares, some with hash set, some null
struct FakeShare {
uint256 hash;
int index;
};
std::vector<FakeShare> phase1_output(TOTAL);
for (int i = 0; i < TOTAL; i++)
{
phase1_output[i].index = i;
if (i % FAIL_EVERY != 0)
{
// "Verified" share — set a non-null hash
char buf[32] = {};
buf[0] = static_cast<char>(i);
std::memcpy(phase1_output[i].hash.begin(), buf, 32);
}
// else: hash stays null (default-constructed uint256 is zero)
}
// Phase-2 filtering: exact pattern from processing_shares_phase2()
std::vector<FakeShare> valid_shares;
for (auto& share : phase1_output)
{
if (share.hash.IsNull())
continue;
valid_shares.push_back(share);
}
// Count expected: indices 1,2,4,5,7,8 pass; 0,3,6,9 fail
int expected_valid = 0;
for (int i = 0; i < TOTAL; i++)
if (i % FAIL_EVERY != 0) expected_valid++;
EXPECT_EQ(static_cast<int>(valid_shares.size()), expected_valid)
<< "Phase 2 must skip null-hash shares. Got " << valid_shares.size()
<< " but expected " << expected_valid;
// Ensure no null-hash share leaked through
for (const auto& share : valid_shares)
{
EXPECT_FALSE(share.hash.IsNull())
<< "Phase 2 emitted a share with null hash (index=" << share.index << ")";
}
}