diff --git a/platformio.ini b/platformio.ini index 3a4ed18..4f47ab3 100644 --- a/platformio.ini +++ b/platformio.ini @@ -577,8 +577,9 @@ build_unflags = -Os build_flags = -D AUTO_VERSION=\"v2.9.5\" -D ESP32_C3_SUPERMINI=1 - ; Software SHA (RISC-V has no Xtensa assembly) - -D USE_SOFTWARE_SHA=1 + ; Hardware SHA-256 via direct register access (full double-hash, no midstate + ; restore -- midstate restore is unsupported on ESP32-C3). See issue #34. + -D USE_HARDWARE_SHA=1 -D USE_DISPLAY=0 -D BUTTON_PIN=9 ; Optimization for single-core @@ -611,8 +612,9 @@ build_unflags = -Os build_flags = -D AUTO_VERSION=\"v2.9.5\" -D ESP32_C3_OLED=1 - ; Software SHA (RISC-V has no Xtensa assembly) - -D USE_SOFTWARE_SHA=1 + ; Hardware SHA-256 via direct register access (full double-hash, no midstate + ; restore -- midstate restore is unsupported on ESP32-C3). See issue #34. + -D USE_HARDWARE_SHA=1 -D USE_DISPLAY=0 -D USE_OLED_DISPLAY=1 ; OLED configuration (128x64 SSD1306 I2C) diff --git a/src/main.cpp b/src/main.cpp index bac037c..f74f6cd 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -562,18 +562,23 @@ void setupTasks() { Serial.println("[INIT] All tasks created (dual-core mining)"); #else - // Single-core (C3, S2): Run only one miner task, not pinned - // Must yield frequently to let WiFi/Stratum work + // Single-core (C3, S2): one miner task, not pinned. Prefer the HW + // full double-hash path (sha256_ll_double_hash_full), but gate it on + // a boot self-test against the software reference -- untested silicon + // must not fail silently with a fast counter and zero shares (#34). + // Falls back to the software miner (pre-#39 behavior) on mismatch. + bool hwShaOk = miner_c3s2_hw_sha_selftest(); xTaskCreate( - miner_task_core0, + hwShaOk ? miner_task_core1 : miner_task_core0, "Miner", - MINER_0_STACK, + hwShaOk ? MINER_1_STACK : MINER_0_STACK, NULL, MINER_0_PRIORITY, - &miner0Task + hwShaOk ? &miner1Task : &miner0Task ); - Serial.println("[INIT] All tasks created (single-core mining)"); + Serial.printf("[INIT] All tasks created (single-core %s mining)\n", + hwShaOk ? "HW-SHA" : "software"); #endif } else { Serial.println("[INIT] Monitor task created (mining disabled - no wallet)"); diff --git a/src/mining/miner.cpp b/src/mining/miner.cpp index 7b216b0..b08474d 100644 --- a/src/mining/miner.cpp +++ b/src/mining/miner.cpp @@ -893,23 +893,67 @@ void miner_task_core1(void *param) { #else // Fallback for ESP32-C3/S2: Use sequential HAL-based mining with Midstate Optimization +// One-shot boot check for the raw-register HW path below: run a fixed header +// + nonce through sha256_ll_double_hash_full and byte-compare against the +// software reference (the pool-proven path). On untested silicon a wrong +// register sequence would otherwise fail silently -- fast hashrate counter, +// zero shares, no logs (#34) -- so main.cpp runs this before spawning the +// miner and falls back to the software task on mismatch. +// Vector: all-zero 80-byte header, nonce 0x0000C4E6. Its true double-SHA256 +// starts with 0x000025EE, so both 16-bit early-reject gates pass and the +// full digests are actually written out for comparison. +bool miner_c3s2_hw_sha_selftest(void) { + block_header_t hb = {}; + hb.nonce = 0x0000C4E6; + + uint32_t header_swapped[20]; + uint32_t *header_words = (uint32_t *)&hb; + for (int i = 0; i < 20; i++) { + header_swapped[i] = __builtin_bswap32(header_words[i]); + } + + sha256_hash_t sw_midstate, swHash, hwHash; + miner_sha256_midstate(&sw_midstate, &hb); + bool swOk = miner_sha256_header(&sw_midstate, &swHash, &hb); + + sha256_ll_acquire(); + bool hwOk = sha256_ll_double_hash_full((const uint8_t *)header_swapped, hb.nonce, hwHash.bytes); + sha256_ll_release(); + + bool pass = swOk && hwOk && (memcmp(swHash.bytes, hwHash.bytes, sizeof(sha256_hash_t)) == 0); + if (pass) { + Serial.println("[SHA-SELFTEST] HW full double-hash PASS - using hardware SHA miner"); + } else { + Serial.println("[SHA-SELFTEST] HW full double-hash FAIL - falling back to software miner"); + Serial.printf("[SHA-SELFTEST] swOk=%d hwOk=%d\n", swOk, hwOk); + Serial.printf("[SHA-SELFTEST] SW hash[28-31]=%02x%02x%02x%02x\n", + swHash.bytes[28], swHash.bytes[29], swHash.bytes[30], swHash.bytes[31]); + Serial.printf("[SHA-SELFTEST] HW hash[28-31]=%02x%02x%02x%02x (000025ee on PASS)\n", + hwHash.bytes[28], hwHash.bytes[29], hwHash.bytes[30], hwHash.bytes[31]); + } + return pass; +} + void miner_task_core1(void *param) { - block_header_t hb; - sha256_hash_t ctx; + block_header_t hb; // unswapped header (nonce source + software verify) + sha256_hash_t hwHash; // hardware double-SHA result + sha256_hash_t swHash; // software re-hash for verification + sha256_hash_t sw_midstate; // software midstate for verification char jobId[MAX_JOB_ID_LEN]; uint32_t minerId = 1; - Serial.printf("[MINER1] Started on core %d (Hardware SHA Midstate, priority %d)\n", + Serial.printf("[MINER1] Started on core %d (HW SHA full double-hash, priority %d)\n", xPortGetCoreID(), uxTaskPriorityGet(NULL)); // Wait for first job while (!s_miningActive) { vTaskDelay(100 / portTICK_PERIOD_MS); } - Serial.println("[MINER1] Got first job, starting mining loop"); + Serial.println("[MINER1] Got first job, starting HW SHA mining loop"); while (true) { if (!s_miningActive) { + s_core1Mining = false; vTaskDelay(100 / portTICK_PERIOD_MS); continue; } @@ -922,50 +966,54 @@ void miner_task_core1(void *param) { strncpy(jobId, s_currentJobId, MAX_JOB_ID_LEN); xSemaphoreGive(s_jobMutex); - // Create swapped header for hardware SHA + // Byte-swapped header (big-endian words) for the hardware SHA engine uint32_t header_swapped[20]; uint32_t *header_words = (uint32_t *)&hb; for (int i = 0; i < 20; i++) { header_swapped[i] = __builtin_bswap32(header_words[i]); } + const uint8_t *header_bytes = (const uint8_t *)header_swapped; - // Set starting nonce for this core - hb.nonce = s_startNonce[minerId]; + // Software midstate on the UNSWAPPED header, used to re-verify candidates + miner_sha256_midstate(&sw_midstate, &hb); - // Prepare midstate variables - uint32_t midstate[8]; - uint8_t *header_bytes = (uint8_t *)header_swapped; + hb.nonce = s_startNonce[minerId]; - // Acquire hardware SHA lock for this mining burst sha256_ll_acquire(); - // Compute midstate once for the block - sha256_ll_midstate(midstate, header_bytes); - + uint32_t yieldCounter = 0; while (s_miningActive) { - // Optimized midstate mining - // Uses pre-computed midstate and only hashes the tail (last 16 bytes + padding) - // header_bytes[64] is the start of the 2nd chunk (tail) - if (sha256_ll_double_hash(midstate, &header_bytes[64], hb.nonce, ctx.bytes)) { - hashCheck(jobId, &ctx, hb.timestamp, hb.nonce); + // Full hardware double-SHA256. Re-hashes block 1 every nonce (no midstate + // restore -- seeded SHA_H state must be big-endian here, see #34 and + // espressif/esp-idf#12440 -- and re-hashing block 1 avoids it entirely). + if (sha256_ll_double_hash_full(header_bytes, hb.nonce, hwHash.bytes)) { + // The raw-register HW path is not yet hardware-verified on these chips, + // so re-hash in software (the proven BitsyMiner path) before submitting. + // This gate guarantees a wrong HW hash can never become a bad share. + bool swVerified = miner_sha256_header(&sw_midstate, &swHash, &hb); + #if defined(DEBUG_SHARE_VALIDATION) + Serial.printf("[HW-DBG] candidate nonce=%08lx SW verify=%s hash[28-31]=%02x%02x%02x%02x\n", + (unsigned long)hb.nonce, swVerified ? "PASS" : "FAIL", + swHash.bytes[28], swHash.bytes[29], swHash.bytes[30], swHash.bytes[31]); + #endif + if (swVerified) { + hashCheck(jobId, &swHash, hb.timestamp, hb.nonce); + } } hb.nonce++; s_stats.hashes++; + s_core1Hashes++; - // Yield periodically to prevent WDT (every ~1M nonces) - if ((hb.nonce & 0xFFFFF) == 0) { + // Single shared core: yield often so WiFi/Stratum/Monitor stay responsive. + if ((++yieldCounter & 0x7FF) == 0) { sha256_ll_release(); vTaskDelay(1); sha256_ll_acquire(); - // Recompute midstate after yield just in case hardware state was lost (unlikely but safe) - sha256_ll_midstate(midstate, header_bytes); } } - // Release hardware SHA lock sha256_ll_release(); - s_core1Mining = false; vTaskDelay(20 / portTICK_PERIOD_MS); } diff --git a/src/mining/miner.h b/src/mining/miner.h index 84c885d..bf61021 100644 --- a/src/mining/miner.h +++ b/src/mining/miner.h @@ -62,6 +62,14 @@ void miner_task_core0(void *param); */ void miner_task_core1(void *param); +/** + * One-shot boot self-test for single-core (C3/S2) builds: verifies the + * raw-register HW double-hash against the software reference so unproven + * silicon falls back to the software miner instead of failing silently. + * Only defined for single-core targets (issue #34). + */ +bool miner_c3s2_hw_sha_selftest(void); + /** * Set pool difficulty for share validation */ diff --git a/src/mining/sha256_ll.cpp b/src/mining/sha256_ll.cpp index 2ad59dc..455e5f0 100644 --- a/src/mining/sha256_ll.cpp +++ b/src/mining/sha256_ll.cpp @@ -370,8 +370,38 @@ bool IRAM_ATTR sha256_ll_double_hash_full(const uint8_t *header, uint32_t nonce, return ll_read_digest_if(hash_out); #else - // For other ESP32 variants, use the existing implementation - return false; + // ESP32-S2/S3/C3: full double SHA-256 with NO external midstate injection. + // These chips' SHA_H digest-state registers expect big-endian state words + // (espressif/esp-idf#12440), so the cached-midstate path in + // sha256_ll_double_hash() -- which seeds them little-endian -- silently + // computes the wrong hash (the root cause of the zero-shares bug, issues + // #34/#28/#10/#5). This path re-hashes block 1 on every call (no midstate + // caching) using only the legitimate START -> CONTINUE flow, which makes no + // assumption about seeded-state byte order and is correct on every variant. + uint32_t *reg = (uint32_t *)(SHA_TEXT_BASE); + uint32_t *hdr_words = (uint32_t *)header; + + // First SHA, block 1: first 64 header bytes (fresh START) + for (int i = 0; i < 16; i++) { + REG_WRITE(®[i], hdr_words[i]); + } + REG_WRITE(SHA_MODE_REG, SHA2_256); + REG_WRITE(SHA_START_REG, 1); + sha256_ll_wait_idle(); + + // First SHA, block 2: last 16 header bytes + nonce + padding (CONTINUE) + ll_fill_second_block(header + 64, nonce); + REG_WRITE(SHA_MODE_REG, SHA2_256); + REG_WRITE(SHA_CONTINUE_REG, 1); + sha256_ll_wait_idle(); + + // Second SHA: hash of the 32-byte first digest (fresh START) + ll_fill_inter_block(); // copy SHA_H -> SHA_TEXT[0..7] and append padding + REG_WRITE(SHA_MODE_REG, SHA2_256); + REG_WRITE(SHA_START_REG, 1); + sha256_ll_wait_idle(); + + return ll_read_digest_if(hash_out); #endif }