Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions platformio.ini
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
17 changes: 11 additions & 6 deletions src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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)");
Expand Down
98 changes: 73 additions & 25 deletions src/mining/miner.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -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);
}
Expand Down
8 changes: 8 additions & 0 deletions src/mining/miner.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down
34 changes: 32 additions & 2 deletions src/mining/sha256_ll.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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(&reg[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
}

Expand Down
Loading