• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

dangernoodle-io / TaipanMiner / 25245812656

02 May 2026 06:28AM UTC coverage: 92.338% (-0.3%) from 92.597%
25245812656

push

github

web-flow
feat(mining): SHA H-write-during-compute canary (TA-320a) (#308)

Companion to the TEXT-overlap canary. If safe, the next-pass H=midstate
reload can move into the previous pass's busy-wait window, enabling
cross-nonce pipelining in the hot loop.

Result surfaced via /api/info as "sha_hwrite": "safe"|"unsafe".

707 of 852 branches covered (82.98%)

Branch coverage included in aggregate %.

0 of 5 new or added lines in 1 file covered. (0.0%)

1607 of 1654 relevant lines covered (97.16%)

249583.16 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

73.88
/components/mining/src/mining.c
1
#include "mining.h"
2
#include "diag.h"
3
#ifdef ASIC_CHIP
4
#include "board.h"
5
#endif
6
#include "sha256.h"
7
#include "stratum.h"
8
#include "work.h"
9
#include "bb_log.h"
10
#include "bb_byte_order.h"
11
#include <string.h>
12
#include <stdio.h>
13
#include <inttypes.h>
14
#include <math.h>
15

16
void mining_stats_update_ema(hashrate_ema_t *ema, double sample, int64_t now_us)
37✔
17
{
18
    if (ema->value == 0.0) {
37✔
19
        ema->value = sample;
3✔
20
    } else {
21
        ema->value = 0.2 * sample + 0.8 * ema->value;
34✔
22
    }
23
    ema->last_us = now_us;
37✔
24
}
37✔
25

26
// SHA self-test flag (process-static, exposed for host tests)
27
static bool s_sha_self_test_failed = false;
28

29
bool mining_sha_self_test_failed(void) { return s_sha_self_test_failed; }
2✔
30

31
void mining_set_sha_self_test_failed(void) {
1✔
32
    s_sha_self_test_failed = true;
1✔
33
}
1✔
34

35
// TA-339: cached HW SHA peripheral microbench result (set once at boot).
36
static bool   s_sha_microbench_valid = false;
37
static double s_sha_us_per_op        = 0.0;
38
static double s_sha_khs_ceiling      = 0.0;
39

40
void mining_set_sha_microbench(double us_per_op, double khs_ceiling) {
×
41
    s_sha_us_per_op = us_per_op;
×
42
    s_sha_khs_ceiling = khs_ceiling;
×
43
    s_sha_microbench_valid = true;
×
44
}
×
45

46
bool mining_get_sha_microbench(double *us_per_op, double *khs_ceiling) {
×
47
    if (!s_sha_microbench_valid) return false;
×
48
    if (us_per_op) *us_per_op = s_sha_us_per_op;
×
49
    if (khs_ceiling) *khs_ceiling = s_sha_khs_ceiling;
×
50
    return true;
×
51
}
52

53
// TA-320a: SHA TEXT-overlap canary state.
54
static sha_overlap_state_t s_sha_overlap_state = SHA_OVERLAP_UNKNOWN;
55

56
void mining_set_sha_overlap_safe(bool safe) {
×
57
    s_sha_overlap_state = safe ? SHA_OVERLAP_SAFE : SHA_OVERLAP_UNSAFE;
×
58
}
×
59

60
sha_overlap_state_t mining_get_sha_overlap_state(void) {
×
61
    return s_sha_overlap_state;
×
62
}
63

64
// TA-320a: SHA H-write-during-compute canary state.
65
static sha_overlap_state_t s_sha_hwrite_state = SHA_OVERLAP_UNKNOWN;
66

NEW
67
void mining_set_sha_hwrite_safe(bool safe) {
×
NEW
68
    s_sha_hwrite_state = safe ? SHA_OVERLAP_SAFE : SHA_OVERLAP_UNSAFE;
×
NEW
69
}
×
70

NEW
71
sha_overlap_state_t mining_get_sha_hwrite_state(void) {
×
NEW
72
    return s_sha_hwrite_state;
×
73
}
74

75
#ifdef ESP_PLATFORM
76
#include "esp_log.h"
77
#include "esp_timer.h"
78
#include "esp_task_wdt.h"
79
#if CONFIG_IDF_TARGET_ESP32S3 || CONFIG_IDF_TARGET_ESP32S2 || CONFIG_IDF_TARGET_ESP32C3
80
#include "sha256_hw_ahb.h"
81
#elif CONFIG_IDF_TARGET_ESP32
82
#include "sha256_hw_dport.h"
83
#endif
84
#include "bb_nv.h"
85
#include "bb_system.h"
86
#include "driver/temperature_sensor.h"
87

88
static const char *TAG = "mining";
89

90
QueueHandle_t work_queue = NULL;
91
QueueHandle_t result_queue = NULL;
92

93
mining_stats_t mining_stats = {0};
94

95
static temperature_sensor_handle_t s_temp_handle = NULL;
96

97
// TA-341: run SHA self-tests synchronously in app_main, before any task
98
// starts. Previously these ran inside mining_task, which meant other tasks
99
// (stratum, http, ui) started before the failure flag was set — making the
100
// gate at main.c ineffective. Informational probes (canaries, microbench)
101
// stay in sha256_hw_init under the mining task.
102
void mining_run_self_tests(void)
103
{
104
    if (sha256_sw_self_test() != BB_OK) {
105
        bb_log_e(TAG, "SHA SW self-test FAILED — mining will not start");
106
        mining_set_sha_self_test_failed();
107
        return;
108
    }
109
#if CONFIG_IDF_TARGET_ESP32S3 || CONFIG_IDF_TARGET_ESP32S2 || CONFIG_IDF_TARGET_ESP32C3
110
    if (sha256_hw_ahb_self_test() != BB_OK) {
111
        bb_log_e(TAG, "SHA AHB self-test FAILED — mining will not start");
112
        mining_set_sha_self_test_failed();
113
    }
114
#elif CONFIG_IDF_TARGET_ESP32
115
    if (sha256_hw_dport_self_test() != BB_OK) {
116
        bb_log_e(TAG, "SHA DPORT self-test FAILED — mining will not start");
117
        mining_set_sha_self_test_failed();
118
    }
119
#endif
120
}
121

122
void mining_stats_load_lifetime(void)
123
{
124
    uint32_t lo = 0, hi = 0;
125

126
    bb_nv_get_u32("taipanminer", "lt_shares", &mining_stats.lifetime.total_shares, 0);
127
    bb_nv_get_u32("taipanminer", "lt_hashes_lo", &lo, 0);
128
    bb_nv_get_u32("taipanminer", "lt_hashes_hi", &hi, 0);
129

130
    mining_stats.lifetime.total_hashes = ((uint64_t)hi << 32) | lo;
131

132
    bb_log_i(TAG, "loaded lifetime stats: shares=%" PRIu32 " hashes=%" PRIu64,
133
             mining_stats.lifetime.total_shares, (uint64_t)mining_stats.lifetime.total_hashes);
134
}
135

136
void mining_stats_save_lifetime(const mining_lifetime_t *snapshot)
137
{
138
    bb_nv_set_u32("taipanminer", "lt_shares", snapshot->total_shares);
139
    bb_nv_set_u32("taipanminer", "lt_hashes_lo", (uint32_t)(snapshot->total_hashes & 0xFFFFFFFF));
140
    bb_nv_set_u32("taipanminer", "lt_hashes_hi", (uint32_t)(snapshot->total_hashes >> 32));
141
}
142

143
temperature_sensor_handle_t mining_stats_temp_handle(void)
144
{
145
    return s_temp_handle;
146
}
147

148
void mining_stats_init(void)
149
{
150
    mining_stats.mutex = xSemaphoreCreateMutex();
151
    mining_stats.session.start_us = esp_timer_get_time();
152
#ifdef ASIC_CHIP
153
    mining_stats.vcore_mv = -1;
154
    mining_stats.icore_ma = -1;
155
    mining_stats.pcore_mw = -1;
156
    mining_stats.fan_rpm = -1;
157
    mining_stats.fan_duty_pct = -1;
158
    mining_stats.board_temp_c = -1.0f;
159
    mining_stats.vin_mv = -1;
160
    mining_stats.vr_temp_c = -1.0f;
161
#ifdef ASIC_BM1370
162
    mining_stats.asic_freq_configured_mhz = (float)BM1370_DEFAULT_FREQ_MHZ;
163
#else
164
    mining_stats.asic_freq_configured_mhz = (float)BM1368_DEFAULT_FREQ_MHZ;
165
#endif
166
    mining_stats.asic_freq_effective_mhz = -1.0f;
167
#endif
168

169
    mining_stats_load_lifetime();
170

171
#if CONFIG_IDF_TARGET_ESP32S3
172
    temperature_sensor_config_t cfg = TEMPERATURE_SENSOR_CONFIG_DEFAULT(-10, 80);
173
    BB_ERROR_CHECK(temperature_sensor_install(&cfg, &s_temp_handle));
174
    BB_ERROR_CHECK(temperature_sensor_enable(s_temp_handle));
175
#endif
176
}
177
#endif
178

179
// Build the 64-byte block2 from header tail + SHA padding
180
void build_block2(uint8_t block2[64], const uint8_t header[80])
8✔
181
{
182
    memset(block2, 0, 64);
8✔
183
    memcpy(block2, header + 64, 16);
8✔
184
    block2[16] = 0x80;
8✔
185
    block2[62] = 0x02;
8✔
186
    block2[63] = 0x80;
8✔
187
}
8✔
188

189
// Pack target bytes [28-31] into a single word for early reject comparison
190
uint32_t pack_target_word0(const uint8_t target[32])
7✔
191
{
192
    return ((uint32_t)target[28] << 24) |
7✔
193
           ((uint32_t)target[29] << 16) |
7✔
194
           ((uint32_t)target[30] <<  8) |
14✔
195
            (uint32_t)target[31];
7✔
196
}
197

198
// Fill a mining_result_t from work + nonce + version info
199
// Stratum mining.submit version field: pool expects the rolled bits only
200
// (XOR delta from base version), not the full rolled version.  The pool
201
// reconstructs the block version by applying the mask to its base template.
202
void package_result(mining_result_t *result,
9✔
203
                    const mining_work_t *work,
204
                    uint32_t nonce,
205
                    uint32_t ver_bits)
206
{
207
    strncpy(result->job_id, work->job_id, sizeof(result->job_id) - 1);
9✔
208
    result->job_id[sizeof(result->job_id) - 1] = '\0';
9✔
209
    strncpy(result->extranonce2_hex, work->extranonce2_hex, sizeof(result->extranonce2_hex) - 1);
9✔
210
    result->extranonce2_hex[sizeof(result->extranonce2_hex) - 1] = '\0';
9✔
211
    sprintf(result->ntime_hex, "%08" PRIx32, work->ntime);
9✔
212
    sprintf(result->nonce_hex, "%08" PRIx32, nonce);
9✔
213
    if (ver_bits != 0) {
9✔
214
        sprintf(result->version_hex, "%08" PRIx32, ver_bits);
3✔
215
    } else {
216
        result->version_hex[0] = '\0';
6✔
217
    }
218
}
9✔
219

220
// --- Software SHA hash backend (native build + host tests) ---
221

222
void sw_prepare_job(hash_backend_t *b,
3✔
223
                    const mining_work_t *work,
224
                    const uint8_t block2[64])
225
{
226
    sw_backend_ctx_t *ctx = (sw_backend_ctx_t *)b->ctx;
3✔
227
    memcpy(ctx->midstate, sha256_H0, sizeof(sha256_H0));
3✔
228
    sha256_transform(ctx->midstate, work->header);
3✔
229
    ctx->target_word0 = pack_target_word0(work->target);
3✔
230
    memcpy(ctx->block2, block2, 64);
3✔
231
}
3✔
232

233
hash_result_t sw_hash_nonce(hash_backend_t *b,
3✔
234
                            uint32_t nonce,
235
                            uint8_t hash_out[32])
236
{
237
    sw_backend_ctx_t *ctx = (sw_backend_ctx_t *)b->ctx;
3✔
238

239
    // Set nonce in block2 (bytes 12-15, little-endian)
240
    ctx->block2[12] = (uint8_t)(nonce & 0xff);
3✔
241
    ctx->block2[13] = (uint8_t)((nonce >> 8) & 0xff);
3✔
242
    ctx->block2[14] = (uint8_t)((nonce >> 16) & 0xff);
3✔
243
    ctx->block2[15] = (uint8_t)((nonce >> 24) & 0xff);
3✔
244

245
    // First SHA-256: clone midstate + transform block2
246
    uint32_t state[8];
247
    memcpy(state, ctx->midstate, 32);
3✔
248
    sha256_transform(state, ctx->block2);
3✔
249

250
    // Write first hash into block3_words
251
    for (int i = 0; i < 8; i++) {
27✔
252
        ctx->block3_words[i] = state[i];
24✔
253
    }
254

255
    // Second SHA-256: H0 + transform block3_words
256
    memcpy(state, sha256_H0, 32);
3✔
257
    sha256_transform_words(state, ctx->block3_words);
3✔
258

259
    // Early reject: check MSB word
260
    if (state[7] <= ctx->target_word0) {
3!
261
        bb_store_be32(hash_out,      state[0]);
3✔
262
        bb_store_be32(hash_out + 4,  state[1]);
3✔
263
        bb_store_be32(hash_out + 8,  state[2]);
3✔
264
        bb_store_be32(hash_out + 12, state[3]);
3✔
265
        bb_store_be32(hash_out + 16, state[4]);
3✔
266
        bb_store_be32(hash_out + 20, state[5]);
3✔
267
        bb_store_be32(hash_out + 24, state[6]);
3✔
268
        bb_store_be32(hash_out + 28, state[7]);
3✔
269
        return HASH_CHECK;
3✔
270
    }
271
    return HASH_MISS;
×
272
}
273

274
void sw_backend_setup(hash_backend_t *b, sw_backend_ctx_t *ctx)
3✔
275
{
276
    memset(ctx->block3_words, 0, sizeof(ctx->block3_words));
3✔
277
    ctx->block3_words[8]  = 0x80000000U;
3✔
278
    ctx->block3_words[15] = 0x00000100U;
3✔
279

280
    b->init = NULL;
3✔
281
    b->prepare_job = sw_prepare_job;
3✔
282
    b->hash_nonce = sw_hash_nonce;
3✔
283
    b->ctx = ctx;
3✔
284
}
3✔
285

286
#ifdef ESP_PLATFORM
287

288
#if CONFIG_IDF_TARGET_ESP32S3 || CONFIG_IDF_TARGET_ESP32S2 || CONFIG_IDF_TARGET_ESP32C3
289
// --- Hardware SHA hash backend (AHB-bus: ESP32-S3/S2/C3) ---
290

291
typedef struct {
292
    uint32_t midstate_hw[8];
293
    uint32_t *block2_words;
294
} hw_backend_ctx_t;
295

296
static void hw_backend_init(hash_backend_t *b)
297
{
298
    (void)b;
299
    sha256_hw_init();
300
}
301

302
static void hw_prepare_job(hash_backend_t *b,
303
                           const mining_work_t *work,
304
                           const uint8_t block2[64])
305
{
306
    hw_backend_ctx_t *ctx = (hw_backend_ctx_t *)b->ctx;
307
    sha256_hw_midstate(work->header, ctx->midstate_hw);
308
    ctx->block2_words = (uint32_t *)block2;
309
}
310

311
static hash_result_t hw_hash_nonce(hash_backend_t *b,
312
                                   uint32_t nonce,
313
                                   uint8_t hash_out[32])
314
{
315
    hw_backend_ctx_t *ctx = (hw_backend_ctx_t *)b->ctx;
316
    uint32_t digest_hw[8];
317
    uint32_t h7_raw = sha256_hw_mine_nonce(ctx->midstate_hw, ctx->block2_words,
318
                                           nonce, digest_hw);
319

320
    if ((h7_raw >> 16) == 0) {
321
        for (int i = 0; i < 8; i++) {
322
            uint32_t w = __builtin_bswap32(digest_hw[i]);
323
            bb_store_be32(hash_out + i * 4, w);
324
        }
325
        return HASH_CHECK;
326
    }
327
    return HASH_MISS;
328
}
329

330
static void hw_backend_setup(hash_backend_t *b, hw_backend_ctx_t *ctx)
331
{
332
    b->init = hw_backend_init;
333
    b->prepare_job = hw_prepare_job;
334
    b->hash_nonce = hw_hash_nonce;
335
    b->ctx = ctx;
336
}
337

338
#elif CONFIG_IDF_TARGET_ESP32
339
// --- Hardware SHA hash backend (DPORT-bus: classic ESP32) ---
340
// TA-271 step B: NerdMiner-verbatim hot loop via sha256_hw_dport_per_nonce.
341
// prepare_job stores the header pointer; per_nonce re-hashes block1 every call
342
// because classic ESP32 has no writable H registers (no midstate injection).
343

344
typedef struct {
345
    const uint8_t *header;
346
} hw_backend_ctx_t;
347

348
static void hw_backend_init(hash_backend_t *b)
349
{
350
    (void)b;
351
    sha256_hw_dport_init();
352
}
353

354
static void hw_prepare_job(hash_backend_t *b,
355
                           const mining_work_t *work,
356
                           const uint8_t block2[64])
357
{
358
    hw_backend_ctx_t *ctx = (hw_backend_ctx_t *)b->ctx;
359
    (void)block2;
360
    ctx->header = work->header;
361
}
362

363
static hash_result_t hw_dport_hash_nonce(hash_backend_t *b,
364
                                          uint32_t nonce,
365
                                          uint8_t hash_out[32])
366
{
367
    hw_backend_ctx_t *ctx = (hw_backend_ctx_t *)b->ctx;
368
    if (sha256_hw_dport_per_nonce(ctx->header, nonce, hash_out)) {
369
        return HASH_CHECK;
370
    }
371
    return HASH_MISS;
372
}
373

374
static void hw_backend_setup(hash_backend_t *b, hw_backend_ctx_t *ctx)
375
{
376
    b->init = hw_backend_init;
377
    b->prepare_job = hw_prepare_job;
378
    b->hash_nonce = hw_dport_hash_nonce;
379
    b->ctx = ctx;
380
}
381

382
#endif // CONFIG_IDF_TARGET_ESP32S3 || ... / CONFIG_IDF_TARGET_ESP32
383

384
#endif // ESP_PLATFORM
385

386
bool mine_nonce_range(hash_backend_t *backend,
7✔
387
                      mining_work_t *work,
388
                      const mine_params_t *params,
389
                      mining_result_t *result_out,
390
                      bool *found_out)
391
{
392
    uint8_t block2[64];
393
    build_block2(block2, work->header);
7✔
394
    backend->prepare_job(backend, work, block2);
7✔
395

396
#ifdef ESP_PLATFORM
397
    int64_t start_us = esp_timer_get_time();
398
    uint32_t hashes = 0;
399
#if CONFIG_IDF_TARGET_ESP32S3 || CONFIG_IDF_TARGET_ESP32S2 || CONFIG_IDF_TARGET_ESP32C3
400
    hw_backend_ctx_t *hw_ctx = (hw_backend_ctx_t *)backend->ctx;
401
#endif
402
#endif
403

404
    for (uint32_t nonce = params->nonce_start; ; nonce++) {
25✔
405
        uint8_t hash[32];
406
#if defined(ESP_PLATFORM) && (CONFIG_IDF_TARGET_ESP32S3 || CONFIG_IDF_TARGET_ESP32S2 || CONFIG_IDF_TARGET_ESP32C3)
407
        uint32_t digest_hw[8];
408
        uint32_t h7_raw = sha256_hw_mine_nonce(hw_ctx->midstate_hw,
409
                                                hw_ctx->block2_words,
410
                                                nonce, digest_hw);
411
        hash_result_t hr;
412
        if ((h7_raw >> 16) == 0) {
413
            for (int i = 0; i < 8; i++) {
414
                uint32_t w = __builtin_bswap32(digest_hw[i]);
415
                bb_store_be32(hash + i * 4, w);
416
            }
417
            hr = HASH_CHECK;
418
        } else {
419
            hr = HASH_MISS;
420
        }
421
#else
422
        hash_result_t hr = backend->hash_nonce(backend, nonce, hash);
25✔
423
#endif
424

425
        if (hr == HASH_CHECK && meets_target(hash, work->target)) {
25!
426
            mining_result_t result;
427
            package_result(&result, work, nonce, params->ver_bits);
5✔
428

429
#ifdef ESP_PLATFORM
430
            double share_diff = hash_to_difficulty(hash);
431

432
            // Sanity check: target/difficulty must be valid and share must meet pool diff
433
            if (work->difficulty < 0.001 || !is_target_valid(work->target) ||
434
                share_diff < work->difficulty * 0.5) {
435
                bb_log_e(TAG, "share sanity fail: share_diff=%.4f pool_diff=%.4f, skipping",
436
                         share_diff, work->difficulty);
437
                continue;
438
            }
439

440
            bb_log_i(TAG, "share found! (nonce=%08" PRIx32 ")", nonce);
441

442
            if (xSemaphoreTake(mining_stats.mutex, pdMS_TO_TICKS(2)) == pdTRUE) {
443
                if (share_diff > mining_stats.session.best_diff) {
444
                    mining_stats.session.best_diff = share_diff;
445
                }
446
                xSemaphoreGive(mining_stats.mutex);
447
            }
448

449
            if (!stratum_is_connected()) {
450
                bb_log_d(TAG, "stratum disconnected, discarding share");
451
            } else if (xQueueSend(result_queue, &result, 0) != pdTRUE) {
452
                bb_log_w(TAG, "result queue full, share dropped");
453
            }
454
#endif
455
            if (result_out) {
5!
456
                *result_out = result;
5✔
457
            }
458
            if (found_out) {
5!
459
                *found_out = true;
5✔
460
                return false;  // in tests, stop after first hit
5✔
461
            }
462
        }
463

464
#ifdef ESP_PLATFORM
465
        hashes++;
466

467
        // Periodic yield + job refresh
468
        if (((nonce + 1) & params->yield_mask) == 0) {
469
            // Tier 1: lightweight new-work check (every 256K nonces)
470
            sha256_hw_release();
471
            int64_t tier1_start_us = esp_timer_get_time();  // Diag: dwell timer starts here
472
            mining_work_t new_work;
473
            if (xQueuePeek(work_queue, &new_work, 0) == pdTRUE &&
474
                new_work.work_seq != work->work_seq) {
475
                // Diag: time the swap path (acquire + prepare_job)
476
                int64_t swap_start_us = esp_timer_get_time();
477
                memcpy(work, &new_work, sizeof(*work));
478
                {
479
                    int64_t job_elapsed_us = swap_start_us - start_us;
480
                    double job_hashrate = (double)hashes / ((double)job_elapsed_us / 1000000.0);
481
                    bb_log_i(DIAG, "prev job %s: %" PRIu32 " nonces in %.2fs (%.1f kH/s)",
482
                             work->job_id, hashes, (double)job_elapsed_us / 1000000.0,
483
                             job_hashrate / 1000.0);
484
                }
485
                bb_log_i(TAG, "new job (%s)", work->job_id);
486
                build_block2(block2, work->header);
487
                sha256_hw_acquire();
488
                backend->prepare_job(backend, work, block2);
489
                start_us = esp_timer_get_time();
490
                int64_t swap_dur_us = start_us - swap_start_us;
491
                if (swap_dur_us > 50000) {
492
                    bb_log_w(DIAG, "job_swap: %lldms (acquire + prepare_job)", swap_dur_us / 1000);
493
                }
494
                hashes = 0;
495
                nonce = params->nonce_start - 1;
496
                continue;
497
            }
498

499
            sha256_hw_acquire();
500

501
            // TA-199: pause check on Tier 1 cadence (256K nonces ≈ 1.1s at tdongle SW-mining
502
            // speed) so the mining_pause() ACK timeout (5s) has headroom. Previously this lived
503
            // inside the Tier 2 block (every 1M nonces ≈ 4.5s), which was right at the edge and
504
            // failed under scheduling jitter — letting OTA run with mining still at full load.
505
            sha256_hw_release();
506
            if (mining_pause_check()) {
507
                sha256_hw_acquire();
508
                backend->prepare_job(backend, work, block2);
509
                start_us = esp_timer_get_time();
510
                hashes = 0;
511
                nonce = params->nonce_start - 1;
512
                continue;
513
            }
514
            sha256_hw_acquire();
515

516
            // Diag: measure time spent inside the Tier-1 dance (release → peek → acquire →
517
            // release → pause_check → acquire). Captures real internal stalls; immune to job-swap
518
            // and pause-resume paths that continue before reaching this point.
519
            {
520
                int64_t tier1_dwell_us = esp_timer_get_time() - tier1_start_us;
521
                if (tier1_dwell_us > 50000) {  // 50ms threshold
522
                    bb_log_w(DIAG, "tier1_dwell: %lldms (acq/rel + peek + pause)", tier1_dwell_us / 1000);
523
                }
524
            }
525

526
            // Tier 2: full yield (every 1M nonces)
527
            if (((nonce + 1) & params->log_mask) == 0) {
528
                int64_t elapsed_us = esp_timer_get_time() - start_us;
529
                if (elapsed_us > 0) {
530
                    double hashrate = (double)hashes / ((double)elapsed_us / 1000000.0);
531
                    uint32_t shares = 0;
532
                    if (xSemaphoreTake(mining_stats.mutex, 0) == pdTRUE) {
533
                        mining_stats.hw_hashrate = hashrate;
534
                        mining_stats_update_ema(&mining_stats.hw_ema, hashrate, esp_timer_get_time());
535
                        mining_stats.session.hashes += hashes;
536
                        shares = mining_stats.hw_shares;
537
                        xSemaphoreGive(mining_stats.mutex);
538
                    }
539
                    bb_log_i(TAG, "hw: %.1f kH/s | shares: %" PRIu32, hashrate / 1000.0, shares);
540
                }
541

542
                {
543
                    float temp = 0;
544
                    if (s_temp_handle && temperature_sensor_get_celsius(s_temp_handle, &temp) == ESP_OK) {
545
                        if (xSemaphoreTake(mining_stats.mutex, 0) == pdTRUE) {
546
                            mining_stats.temp_c = temp;
547
                            xSemaphoreGive(mining_stats.mutex);
548
                        }
549
                    }
550
                }
551

552
                // Release SHA lock so mbedTLS can use HW SHA during TLS/OTA
553
                sha256_hw_release();
554
                esp_task_wdt_reset();
555
                vTaskDelay(pdMS_TO_TICKS(5));
556
                sha256_hw_acquire();
557
            }
558
        }
559
#endif
560

561
        if (nonce == params->nonce_end) break;
20✔
562
    }
563
    return false;
2✔
564
}
565

566
#ifdef ESP_PLATFORM
567
void mining_task(void *arg)
568
{
569
    (void)arg;
570

571
    bb_log_i(TAG, "mining task started");
572

573
    // SHA self-tests now run in app_main before any task starts (TA-341).
574
    // The gate at main.c is what skips creating this task on failure;
575
    // double-check here in case someone calls mining_task directly.
576
    if (mining_sha_self_test_failed()) {
577
        bb_log_e(TAG, "SHA self-test previously failed — mining task exiting");
578
        return;
579
    }
580

581
    // Set up hash backend
582
    hw_backend_ctx_t hw_ctx;
583
    hash_backend_t backend;
584
    hw_backend_setup(&backend, &hw_ctx);
585

586
    if (backend.init) {
587
        backend.init(&backend);
588
    }
589

590
    // Subscribe mining task to TWDT — IDLE1 monitoring is disabled because
591
    // this task is CPU-bound on core 1 by design. Feed at each yield point.
592
    esp_task_wdt_add(NULL);
593

594
    sha256_hw_acquire();
595

596
    for (;;) {
597
        mining_work_t work;
598
        sha256_hw_release();
599
        BaseType_t got = xQueuePeek(work_queue, &work, pdMS_TO_TICKS(5000));
600
        sha256_hw_acquire();
601
        if (got != pdTRUE) {
602
            esp_task_wdt_reset();
603
            continue;
604
        }
605

606
        if (!is_target_valid(work.target)) {
607
            bb_log_w(TAG, "invalid target from queue, waiting for fresh work");
608
            vTaskDelay(pdMS_TO_TICKS(100));
609
            continue;
610
        }
611

612
        bb_log_i(TAG, "new job (%s)", work.job_id);
613

614
        uint32_t base_version = work.version;
615
        uint32_t ver_bits = 0;
616

617
        for (;;) {  // version rolling outer loop
618
            if (work.version_mask != 0 && ver_bits != 0) {
619
                uint32_t rolled = (base_version & ~work.version_mask) | (ver_bits & work.version_mask);
620
                work.header[0] = rolled & 0xFF;
621
                work.header[1] = (rolled >> 8) & 0xFF;
622
                work.header[2] = (rolled >> 16) & 0xFF;
623
                work.header[3] = (rolled >> 24) & 0xFF;
624
            }
625

626
            mine_params_t params = {
627
                .nonce_start = 0,
628
                .nonce_end = 0xFFFFFFFFU,
629
                .yield_mask = 0x3FFFF,
630
                .log_mask = 0xFFFFF,
631
                .ver_bits = ver_bits,
632
                .base_version = base_version,
633
                .version_mask = work.version_mask,
634
            };
635

636
            mine_nonce_range(&backend, &work, &params, NULL, NULL);
637

638
            if (work.version_mask == 0) break;
639
            ver_bits = next_version_roll(ver_bits, work.version_mask);
640
            if (ver_bits == 0) break;
641
            bb_log_i(TAG, "rolling version: mask=%08" PRIx32 " bits=%08" PRIx32, work.version_mask, ver_bits);
642
        }
643

644
        bb_log_w(TAG, "exhausted nonce range for job %s", work.job_id);
645
    }
646
}
647

648
#ifndef ASIC_CHIP
649
const miner_config_t g_miner_config = {
650
    .init = NULL,
651
    .task_fn = mining_task,
652
    .name = "mining_hw",
653
    .stack_size = 8192,
654
    .priority = 20,
655
    .core = 1,
656
    .extranonce2_roll = false,
657
    .roll_interval_ms = 0,
658
};
659
#endif
660
#endif // ESP_PLATFORM
661

STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc