# Obelisk SC1 → Bitcoin BLAKE2b fork: mining investigation Working document. Status: initial feasibility established (ASIC is compatible; obstacle is the firmware's stratum layer). Updated 2026-09-04. ## Objective Get the Obelisk SC1 (Gen 2 / "Slim") to mine the Bitcoin Knots BLAKE2b hard-fork chain (POW switched SHA-256d → BLAKE2b at block 961,640, 2026-08-30). The chain uses a 164-byte "version 2" block header for consensus; miners attach through a DATUM Gateway that speaks a "Siacoin dialect" of Stratum v1. Miner under test: SC1 Slim @ 192.168.1.133, fw v1.6.0, cgminer 4.10.0, SSH root/obelisk, web admin/admin. ~470 GH/s on Blake2b-Sia. ## Three candidate paths (in priority order) 1. Custom firmware — patch the SC1's cgminer/sia driver to speak the fork's dialect and compute the correct target. 2. Custom DATUM Gateway — make the gateway emit work the stock v1.6.0 firmware can consume. 3. Stratum proxy — a middleman translating fork↔firmware dialect. ## Resources - Hardware (PCB/schematics; ASIC design is proprietary and NOT included): https://gitlab.com/NebulousLabs/obelisk-hardware - Early firmware (OB1 control card, SC1a/DCR1a = Gen 1; buildroot + cgminer fork): https://gitlab.com/NebulousLabs/obelisk-ob1 - DATUM Gateway BLAKE2b variant: https://github.com/innerhat-dev/datum_gateway (checked out at commit 2fea7e51286d3821c19dc1c240b8caa92bd92532) - Local clones: /home/objecta/workspace/obelisk-blake2b/{obelisk-hardware, obelisk-ob1, datum_gateway} - Miner logs copied to: /home/objecta/workspace/sc1-logs/ Verified-working hardware on this fork: Goldshell SC line (stock firmware), Antminer A3, Innosilicon S11, ePIC SC200. ## Architecture (from ob1 source) - Control card (Linux): cgminer (driver-obelisk.c; util.c parse_notify), apiserver (Go "Crow/0.1"), lighttpd, sshd, ntpd. This is the SSH target. - Hashboard MCU: obelisk/ dir — CSS_SC1_hal.c, Usermain.c, Ob1Hashboard.c, siahash/ (siastratum.c, siaverify.c). Drives the ASICs over SPI. - ASIC: hashes an 80-byte Sia header with Blake2b. Fixed 80-byte input. Key firmware function — siastratum.c siaCalculateStratumHeader(): arbTx = 0x00 || coinb1 || extranonce1 || extranonce2 || coinb2 checksum = Blake2b(arbTx) # "coinbase hash" per merkle branch: checksum = Blake2b(0x01 || branch || checksum) header = prevhash(32) || nonce(8) || ntime(8) || checksum(32) # 80 bytes Verification in siaverify.c confirms: blake2b(checksum, 32, header, 80, ...). ## Core finding: the ASIC is compatible The fork does NOT hash a 164-byte header on the ASIC. In datum_pow.c: - datum_blake2b_pow_hash_le() → datum_blake2b_256(hash, work, 80) # 80 bytes - datum_blake2b_build_work_header(): prevhash(32)||nonce(8)||ntime(8)||root(32) = 80 bytes, identical layout to the Sia header. - datum_blake2b_work_root(): root = Blake2b(0x00 || coinb1(39) || extranonce(12)) - datum_blake2b_sia_coinb1(): coinb1 = 3 zero bytes || 32-byte commitment || 4 zero - datum_blake2b_sia_prevhash(): tagged-SHA256(prevhash) with first 6 bytes zeroed - The 164-byte v2 header is only datum_blake2b_serialize_block_header() — block serialization for consensus, reconstructed by the gateway, never hashed by the ASIC. So the SC1 ASIC can hash this fork. The block is purely in the firmware's stratum → header layer. ## The exact protocol gap (3 differences) 1. Merkle/root computation - Firmware: Blake2b(0x00||coinb1||en1||en2||coinb2) + merkle-branch rounds. - Fork: Blake2b(0x00||coinb1||extranonce) — direct, no coinb2, no branches. - These are identical when coinb2 is empty and branch count is 0. Compatible in principle; the firmware just rejects empty fields. 2. Empty-field parsing (the observed "memcpy 0 bytes" in util.c parse_notify:2144) - Fork sends empty coinb2, empty merkle list, empty version. - SC1 cgminer's parse_notify() memcpys 0 bytes and errors. 3. Target/difficulty interpretation (the spurious 6,731 "Found block" hits) - Fork sends Bitcoin compact nbits ("193c2d40", network diff ~71M). - Sia firmware derives its target from a Sia-style difficulty, not nbits. ## Feasibility - Path 1 (firmware): most promising. Changes are localized to the stratum parser + target conversion; ASIC driver untouched. Leverage: we have root SSH to the control card, so a patched cgminer binary may be swappable in place without a full image reflash. - Path 2 (gateway): partially viable but bounded. The gateway can already send the correct coinb1 and pre-transformed prevhash, but cannot compensate for the empty-field rejection or the nbits mismatch: Blake2b is preimage-resistant, so no merkle-branch value exists that makes the firmware's tree computation equal the fork's direct-hash root. - Path 3 (proxy): same ceiling as path 2. ## Refined root cause (session 2 — build/flash path) The stratum→ASIC stack is a single user-space cgminer binary on the control card (Linux), NOT hashboard MCU firmware. Makefile builds `cgminer-sia` with `MODEL SC1` + `ALGO BLAKE2B`, cross-compiled via `build_cgminer_arm.sh` using the buildroot toolchain at `controlCardImage/host`. All of driver-obelisk.c, obelisk/CSS_SC1_hal.c, obelisk/Usermain.c, obelisk/Ob1Hashboard.c and obelisk/siahash/* are compiled into that one binary (Makefile.am 143-190). Concrete failure causes, now pinned to code: 1. Share submission is refused — cgminer.c:6737 `if (work->nonce2_len > 4)` logs "Not attempting to submit shares" and drops the work. The fork's subscribe reports extranonce2_size = 8; the Obelisk cgminer only accepts ≤ 4 bytes. This is the direct reason for zero submissions. 2. Target scale — CHECKED, and it is actually COMPATIBLE (no patch needed). siaverify.c derives the pool target as baseDifficulty 0xffff000000000000 / difficulty at target[4..11] with 0xFF tail, which equals the fork's get_target_from_diff() diff1/diff to within ~2^-62 (verified numerically: firmware/fork target ratio = 1.0 across D=1..100000). The fork's "stratum difficulty" and the firmware's are the same scale. So the 6,731 "Found block" hits are NOT a difficulty-scale bug — they are a symptom of the malformed work (empty coinb2/merkle/version corrupting the parsed header), to be re-examined once the protocol fixes land. 3. Extranonce layout — fork builds the merkle leaf as Blake2b(0x00 || coinb1(39) || extranonce(12)) where extranonce = sid_inv(4) || extranonce2(8) (datum_stratum.c ~1138). Firmware builds 0x00 || coinb1 || en1(8) || en2(8) || coinb2. Different byte layout. 4. Empty-field parse (memcpy 0 bytes) on empty coinb2/merkle/version — non-fatal but shows the notify shape differs. ## Build/flash path — findings - Control card = Microchip SAMA5D2 SOM (ARMv7, Linux) in Gen 1 (ob1). Buildroot 2018.02 toolchain. Firmware is flashed via SD card (sdCard.img) or the mtdblock partitions (controlCardRootFS.img / controlCardZImage.img). - Gen 2 (Slim) is NOT this exact hardware: hardware README says Gen 2 control board uses an STM32 and 32 ASICs (Gen 1 = Microchip SOM, 15 ASICs). The running Gen 2 firmware is "ob2" (/usr/local/ob2/), whose source is NOT public. - cgminer version matches (4.10.0), so the stratum/header code (util.c, siastratum.c, siaverify.c) is very likely shared with ob1; only the ASIC/hal layer differs for Gen 2. This is the key assumption to validate. ### ob2 binary validation (done) Extracted /usr/local/ob2/bin/cgminer-sia (264 KB, ELF 32-bit ARMv7 EABI5, stripped, BuildID 478a6119..., self-identifies "Obelisk OB2" / "obelisk-miner/ 2.0.0"). Live box: armv7l, Linux 4.9.118, "Generic AM33XX" (TI AM335x) — i.e. Gen 2 control card is AM335x, NOT the ob1 SAMA5D27. Strings present in the ob2 binary that match the ob1 cgminer.c/util.c stratum layer verbatim: "Not attempting to submit shares", "Asked to memcpy", "SIA board found", "parse_notify", "Found block for pool", the "mining.subscribe"/"cgminer/4.10.0" JSON, "Failed to get valid n2size". This confirms the stratum layer (where all four patch points live) is shared with ob1; the obelisk hardware layer (driver-obelisk.c / CSS_SC1_hal.c) is the only Gen1-vs-Gen2 difference and is NOT where we patch. Two implementation routes for path 1: - A) Binary-patch the ob2 cgminer-sia in place (preserves the correct Gen 2 hardware layer; the nonce2_len>4 check is a single CMP immediate findable via the error-string xref). No reflash, swap via root SSH. - B) Rebuild from ob1 source (patch the shared stratum code, cross-compile with build_cgminer_arm.sh using an arm-linux toolchain) — but this yields the Gen 1 hardware layer (SAMA5D27 / 15-ASIC), so it likely won't drive the 32-ASIC Gen 2 board. Route A is preferred unless we can reconstruct the ob2 hal. ## Path 1 patch list (localized, all in cgminer) 1. cgminer.c: raise the nonce2_len ceiling from 4 to 8 (and the submit framing). 2. Fix the extranonce byte layout to match the fork's sid(4)||en2(8). 3. Replace the Sia "stratum difficulty" target math with the fork's nbits/diff target conversion. 4. Verify siaCalculateStratumHeader already yields the fork root when coinb2 and merkle branches are empty (it does, in principle). ## Restore & deployment (filesystem findings) - Root `/` = ext4 READ-ONLY (mmcblk0p2, 1.5G). `/var` = ext4 rw (mmcblk0p3). `/media/boot` = vfat ro. So the original binaries are immutable on the ro rootfs until deliberately remounted — restore is inherently safe. - Launch: /etc/init.d/cgminer-sia → process-monitor watchdog → /usr/local/ob2/bin/cgminer-sia --default-config /var/etc/ob2/cgminer.conf --api-listen --api-allow W:127.0.0.1 --log 4 --protocol-dump (config on writable /var). No integrity checking on the binary. - Pristine backups (byte-identical, SHA256-verified): - Local: pristine/{cgminer-sia.orig, cgminer-dcr.orig, apiserver.orig, SHA256SUMS, config/*} - On-device: /var/backup/obelisk-original/ (same + config + SHA256SUMS) - Original hashes: cgminer-sia 95aa670d…, cgminer-dcr aa906110…, apiserver 21d56bd6… - Deploy: `mount -o remount,rw /` → replace /usr/local/ob2/bin/cgminer-sia → `mount -o remount,ro /` → `/etc/init.d/cgminer-sia restart` (or reboot). - Restore: reverse — copy cgminer-sia.orig back, remount ro, restart. Reflash via SD card is the last-resort full restore. - Rollout to the other 3 miners: same binary + same remount-replace-restart procedure, scripted (per-miner: backup original + hash → scp patched → remount-replace → verify hash → restart). ## Patch details (binary patch of ob2 cgminer-sia) Binary is 32-bit ARM (armv7, ARM state, not Thumb). Disassembler: extract binutils-arm-linux-gnueabi + `LD_LIBRARY_PATH=/tmp/armbinutils/usr/lib/x86_64-linux-gnu`. Patch #1 — nonce2_len gate (the "Not attempting to submit shares" blocker): - VMA 0x1c6b8 `e59031c0 ldr r3,[r0,#448]` (work->nonce2_len @ struct offset 0x1c0) - VMA 0x1c6bc `e3530004 cmp r3,#4` ← patch this immediate - VMA 0x1c6c0 `8a00017c bhi 1ccb8` (→ "Not attempting to submit shares") - File offset = 0xc6bc (VMA − 0x10000). Patch byte 0x04 → 0x08: `e3530008` (cmp r3,#8). Single-byte change, allows nonce2_len 8. Remaining (need more fork-side analysis before patching): - #2 target: NOT NEEDED. Verified the firmware Sia-difficulty target == fork diff1/diff target (ratio 1.0). No firmware target patch required. - #3 extranonce: NOT NEEDED. Official gateway = CONVOYMining/datum_gateway (per bitcoin-blake2b.org/mining; requires commit 56c31f4 "header-v2 bit in BLAKE2b H1 commitment" or later — HEAD b9ea7dc includes it). CONVOY uses the Sia-native layout via datum_pow.c: coinb1 = 39 bytes (000000 || h2 || 0000, datum_blake2b_coinb1), extranonce1 = 4 bytes (sid_inv, subscribe "%8.8x"), extranonce2 = 8 bytes, coinb2 empty; leaf = Blake2b(0x00 || coinb1(39) || sid_inv(4) || en2(8)) = 52 bytes (datum_blake2b_work_root). The firmware's siaCalculateStratumHeader (0x00||coinb1||en1||en2||coinb2) equals this byte-for-byte. Note: iohzrd/datum_gateway splits the same 52 bytes differently (35-byte coinb1 + 8-byte en1) — also compatible, but CONVOY is the official repo to use. So the ONLY firmware change needed is #1 nonce2_len >4→>8. - #4 header construction: compatible in principle (80-byte prevhash||nonce|| ntime||root) once #3 is correct; empty coinb2/merkle parse errors are non-fatal. All four are needed together before shares will be accepted — patch #1 alone would just submit invalid work (target + extranonce still wrong). ## Open questions / next steps - [ ] Where does stratum parsing + header construction actually run on the Gen 2 build — control-card cgminer vs hashboard MCU? (determines what we flash) - [ ] Does the ob1 (Gen 1) source match the ob2 (Gen 2) binary closely enough to patch and cross-compile? (ob2 source is not public) - [ ] Determine the control-card architecture / toolchain for cross-compiling. - [ ] Inspect the running /usr/local/ob2/bin/ layout on the live miner. - [ ] A3 / S11 / SC200 similarity check — do any share the Obelisk ASIC or board, and is there a known-good firmware patch to crib from? - [ ] Nail the extranonce layout: fork uses 12-byte extranonce (sid(4)||en2(8)), Sia uses en1(8)||en2(8) = 16 bytes (see datum_stratum.c ~1138). ## Session state (2026-09-04 evening) — RESUME HERE Gateway is now **CONVOYMining/datum_gateway** (commit 56c31f4+, HEAD b9ea7dc), not the earlier innerhat-dev checkout. Built with libsodium deps in /home/objecta/datum-deps; binary at convoy-datum/build/datum_gateway. Config: datum_gateway_config.json (mode 600). Protocol #pow:0.4.1. ### Firmware patches applied (deployed to /tmp/cgminer-sia.patched → miner) - Patch #1: nonce2_len gate `cmp r3,#4`→`cmp r3,#8` (file offset 0xc6bc). - Patch #2: ExtraNonce2Size 4→8 (file offset 0x32610, VMA 0x42610). ### Gateway source changes (convoy-datum/src/datum_stratum.c, uncommitted) - extranonce2 zero-pad: high 4 bytes zeroed (`for(i=4;i<8;i++) extranonce_bin[i+4]=0`) because ob2 firmware has only a 32-bit nonce2 (submits 4 real + 4 stack-garbage). - Debug stderr logging of prevhash_bin, extranonce_bin, commitment, root, nonce8, ntime8, work(80), share_hash — the "DBG ..." lines in the gateway log. - line 1606 notify coinb2 reverted to empty `"\"\",[],"` (HEAD). A temporary "00" experiment + an accidental extra `\"` were both reverted; notify verified valid JSON (9 params, coinb2 empty, version single-quoted). ### Current result Miner connects, hashes in bursts, submits shares — all rejected `H-not-zero` (error 23). The `memcpy 0 bytes` in parse_notify (empty coinb2) is non-fatal. ### Key finding: midstate is byte-identical Live midstate located in miner heap at 0x82d68 (work struct at 0x82ce8, midstate @ +128). prevhash/ntime match the notify, nonce=0, root= 9a7f7e7fe0e2872fca25fa61429f0d52f5851e25995bac3a800da8338a789aba — byte-identical to the gateway's reconstruction. So header layout (prevhash32||nonce8||ntime8|| root32) is correct on both sides. ### Byte-order analysis (all ruled out) - nonce LE (stm r3,{r0,r1} = [low32,high32]), matches CONVOY hex2bin order. - ob2 Blake2b = standard IV (0x6a09e667… @0x421e0), param block 0x01010020, standard rotations/sigma; finalization is memcpy(state→out,32) no reversal. - H-not-zero check: SIA checks checksum[0..3]==0 (low word h[0]); CONVOY's datum_blake2b_pow_hash_le reverses digest then upk_u32le(share_hash,28) also tests h[0]. Same word on both sides. → Header identical + Blake2b standard + same h[0] check, yet hash differs. PARADOX. ### Hypothesis B (root/en1 byte order) — RULED OUT 2026-09-05 Leaf construction verified against the gateway's own share processing (3/3 shares MATCH): leaf = 0x00 || coinb1(39) || en1(4) || nonce2(4) || 00000000(4), where coinb1 = "000000"+commitment(32)+"00000000" and en1 is the raw hex bytes (NO reversal). Miner's en1 read from memory = "b1ccf00d" (nonce1 ptr @0x65a38), nonce2 = uint32 counter. Root computation is correct — en1 byte order is consistent between miner and gateway. NOT the cause. ### Hypothesis A (NEON Blake2b divergence) — CONFIRMED 2026-09-05 Gateway DBG output (submit handler) proves: gateway reconstructs the 80-byte work header (prevhash||nonce8||ntime8||root) byte-identical to the miner's, computes standard libsodium blake2b, and rejects with H-not-zero because upk_u32le(share_hash,28) != 0 (share_hash = reversed digest → this is the LOW 4 bytes digest[0..3] = h[0] low word non-zero). Verified by recomputing blake2b(work) in Python: raw digest share1 f43dd094... / share2 88fe017e... → h[0] non-zero, and DBG share_hash == reversed(raw) exactly. The miner nevertheless accepts these nonces (its NEON blake2b sees h[0]==0). Crucially NO 4-byte window of the standard digest is zero, so the divergence is a genuine computational difference (not a byte-order permutation). The ob2 NEON Blake2b (and/or the ASIC hardware Blake2b) produces a different digest than standard Blake2b. ### Next step (pin the exact divergence) 1. Capture the miner's actual 32-byte blake2b output at the verify call 0x42c10 (ptrace dump) and diff against standard blake2b(work) — determines whether the hardware or the NEON software path is the divergent one. 2. Or disassemble NEON compression fn 0x39b68 / finalization 0x42088 and compare to blake2b-ref.c line-by-line. ### Recommended next step (deterministic, do B first) Read miner's nonce2 + en1 from the work struct at 0x82ce8 via /proc//mem, compute blake2b(0x00||coinb1||en1||nonce2||00000000) with en1 both raw and reversed, compare to root 9a7f7e7f… . Match → hypothesis A; mismatch → en1/arbTx byte-order pinned. ### ptrace header-dump blocker ptrace_dump.py (Python ctypes, runs on miner's Python 3.5) uses PTRACE_POKETEXT to arm a software breakpoint at 0x42c10 (verified: orig instr = ebfffc9a = bl 0x41e80, the Blake2b call). But: (1) PTRACE_PEEKDATA returns EFAULT (errno 14) even at the mapped entry point 0x150fc (mapping 0x10000–0x4f000) — never armed via PEEKDATA; (2) miner hashes only in bursts and keeps restarting (PIDs 18595→32300→32729), so the breakpoint window is missed. Once the miner hashes continuously (non-empty coinb2 or parse_notify fix), re-run ptrace_dump.py with a longer timeout to capture the 80-byte header + Blake2b checksum at 0x42c10 and diff against DBG work. ### Tooling notes - Disassemble: LD_LIBRARY_PATH=/tmp/armbinutils/usr/lib/x86_64-linux-gnu /tmp/armbinutils/usr/bin/arm-linux-gnueabi-objdump -d --start-address=… --stop-address=… - Miner also runs cgminer-dcr (Decred) + process-monitor — target only cgminer-sia. - Helper scripts in the workdir: ptrace_dump.py, diag_attach.py, run_ptrace.py, run_main.py, run_one.py, run_diag.py, capture_notify.py, mem_search4.py. ## Session state (2026-09-05) — divergence pinned to software NEON Blake2b ### SIGBUS crash = debugging artifact (not a miner bug) dmesg shows `Unhandled prefetch abort: debug event (0x002) at 0x00042c40` — this is MY software breakpoint (`0xe1200070` at `0x42c10`, the `bl 0x41e80` Blake2b call) being executed by the UNTRACED main thread. The verify path runs on the MAIN thread (`cgminer-sia`, high CPU: utime≈75k stime≈206k), NOT on `cg@0/Miner` (idle, utime=2). When I traced `cg@0/Miner` and armed the BKPT in shared .text, the main thread hit it untraced → kernel "unhandled debug event" → SIGBUS → process death. `0x42c40` is the pipeline-prefetch offset (~48B ahead of 0x42c10), not a second breakpoint. So the miner's repeated restarts are an artifact of tracing the wrong thread. Fix: trace the MAIN thread. ### IV RULED OUT (was a strong lead, resolved to a typo) `CSS_SC1Defines.h` defines SC1 hardware state registers with a MODIFIED IV: `SC1_V12_VAL = 0x510E527FADE68281` (std IV[4]=…ade682d1) and `SC1_V14_VAL = 0xE07C265404BE4294` (std IV[6]=…fb41bd6b). V14 even appears in RFC 7693's appendix (known IV-listing errata). BUT: byte-search of /tmp/cgminer-sia.patched finds the STANDARD IV (IV4 d182e6ad7f520e51 @0x29fe8/0x3eab8, IV6 6bbd41fbabd9831f @0x29ff8/0x3eac8) and ZERO occurrences of the modified V12/V14. → ob2 loads the STANDARD IV into hardware+software; the modified values are Gen1 data-sheet typos never used. IV is not the divergence. ### Option 1 (non-empty coinb2) is a NO-OP Empty coinb2 is harmless: gateway leaf = 0x00||coinb1(39)||extranonce(12) = 52B (datum_pow.c datum_blake2b_work_root), miner arbTx identical, root matches. The "memcpy 0 bytes" is a non-fatal warning; work is accepted. Appending a coinb2 byte would only add a spurious coinbase suffix and change the root for nothing. ### cgminer API state (empirical) `Accepted=0, Rejected=23, Discarded=68,219,167, MHS av=0.00, Work Diff=16383.75, Found Blocks=23`. The ASIC finds chip-valid nonces; the software verify discards ~68M and accepts only 23, all of which the gateway rejects H-not-zero. ### Conclusion Divergence is in the ob2 SOFTWARE NEON Blake2b verify fn (0x41e80, compression 0x39b68). It computes a different digest than libsodium for the same 80-byte header, despite standard IV/param/rotations/sigma. R2 rotation (24) confirmed correct in disassembly; exact bug (message load / add / xor / finalization / round count) still to be found via continued disassembly or qemu-arm test of 0x41e80 against known vectors. ### RULED OUT 2026-09-05 (later same day): ob2 NEON Blake2b (0x41e80) is STANDARD Direct ptrace call-injection test of 0x41e80 (signature `blake2b(out,in,inlen)`, outlen fixed 32). Overcame an alignment trap (NEON `vst1.8 {d16-d17},[r3 :64]` at 0x41f04 needs 8-byte-aligned sp; the stopped-thread sp was 4-byte aligned → fixed by `& ~7`). Result: 4/4 test vectors MATCH hashlib.blake2b(digest_size=32): empty 0e5751c0…, "abc" bddd813c…, 80×00 143aa0da…, 80×00..4f 066de100… — including the 80-byte header-size case. → the SOFTWARE Blake2b is NOT the divergence. ob1 reference confirm: siaValidNonce (driver-obelisk.c:155) → siaHeaderMeetsChipTargetAndPoolDifficulty (siaverify.c:66) does `blake2b(checksum,32,header,80)` then `checksum[0..3]==0 (h[0]==0) AND checksum[4..11]<=poolTarget` (baseDifficulty 0xffff000000000000 / difficulty, big-endian into poolTarget[4..11]) — standard + correct, returns 2=submit only when h[0]==0. The disassembled ob2 verify at 0x42c10 (bl 0x41e80) + byte-by-byte check 0x42c28–0x42c40 matches siaHeaderMeetsProvidedTarget. Since the software path is provably standard, the remaining divergence is in the HARDWARE ASIC (producing chip-valid nonces that standard blake2b rejects) and/or a bug in the ob2 verify's target/comparison (e.g. comparing against the wrong/looser chip target instead of the pool target). NEXT: test an actual ASIC chip-valid nonce against standard blake2b to settle hardware-vs-software. ### HARDWARE IS STANDARD (2026-09-05) — divergence is in the verify logic, not the ASIC Breakpoint at 0x42c10 (bl 0x41e80 in the verify) captured 4 live ASIC chip-valid nonces on the main thread (tid=pid, e.g. 7034). Each 80-byte header, hashed with standard blake2b, yields h[0]==0 (digest[0..3]==0) AND meets the full pool target (digest[4..11] tiny). Nonces: 0x1f919c1a772bc4fe, 0x1f9a4156386c347e, 0x1fb9ccad81dcd7d2, 0x1fb330a3305ebcc3 — 4/4. → the ASIC correctly finds valid diff1 nonces; hardware is NOT divergent. Combined with 0x41e80 being standard, the H-not-zero rejection is NOT a blake2b/hardware divergence — it is a bug in the verify's target/comparison (e.g. comparing against the wrong/looser target, r8) or in the submission path (submitting a different nonce than the chip-valid one). NEXT: disassemble the full verify fn to identify the r8 (target) source, and/or capture the nonce at submit_nonce to compare against the chip-valid nonce. ### Verify target confirmed = POOL target (2026-09-05, same day) Read the verify's target pointer at the 0x42c10 breakpoint: r8=0x61e18, value 00000000 0004000c003000c0 ffffffff... — the CORRECT pool-target layout ([0..3]=0 h[0] gate, [4..11]=adjustedDifficulty (~16383), [12..31]=0xff). r8 is computed as `r8 = sl + (*[work+344])*32` where sl=0x61e18 (global target array). So the ob2 verify (0x41e80 standard + pool target + byte-by-byte compare 0x42c28-0x42c40) is FULLY CORRECT — it should accept h[0]==0 nonces and reject h[0]!=0. Yet the discrepancy is now sharp: the 43 submitted shares hash to h[0]!=0 (nonce 0x00057cf4aaa809d1 → digest a894c756…), while captured chip-valid nonces hash to h[0]==0 (0x1fc59e7277d26886 → 00000000…). So the miner submits a nonce/header DIFFERENT from the chip-valid one it verified. The bug is in the SUBMISSION path (miner submits a mangled/different nonce) or the gateway's reconstruction (byte-order in the submitted nonce). NEXT: breakpoint at submit_nonce to capture the exact nonce the miner submits, and diff it against the chip-valid nonce. ### ROOT CAUSE FOUND (2026-09-05): merkle root uses 4-byte extranonce2 in miner, 8-byte in gateway Correlated the SAME share (nonce f08cea88db433b00, block 967837, 08:44:35) between the miner midstate (ptrace at work+128) and the gateway DBG log. prevhash + ntime match; the ROOT differs: miner midstate root = f1bce05e711c692a9b07d87dcac5de9a867163e836cfd096c5e8a5ff4eea9ec6 gateway root = a1070528d8fca1d215792f3eef54db24512551a38c7c58542c2ca471a14e68c1 Verified: blake2b(0x00||coinb1(39)||en1(4)||en2) with en2 = d0fb0100 (4 bytes) == miner root; with en2 = d0fb010000000000 (8 bytes zero-padded) == gateway root. So the ob2 miner builds its leaf with a 4-byte extranonce2 (48-byte leaf) while CONVOY uses 8 bytes (52-byte leaf). The "ExtraNonce2Size 4->8" patch fixed the stratum SUBMIT size but NOT the merkle-root leaf construction. The different root makes the header hash h[0]!=0 -> the gateway's H-not-zero rejection. FIX: patch the ob2 merkle-root/leaf builder to zero-pad the extranonce2 to 8 bytes (52-byte leaf), matching CONVOY. NEXT: locate the ob2 leaf/root builder and patch it (the blake2b at 0x41e80 is reused; the leaf is 0x00||coinb1||en1||en2). ### RESOLVED (2026-09-05): shares now ACCEPTED Two fixes deployed, both making en2 = [nonce2 (32-bit)][0 (32-bit)] (64-bit zero-extended): 1. Miner leaf/root builder (ob2 binary, file offsets 0x32694 + 0x32698): - 0x32694: `bl memcpy` -> `str r8,[r0]` (zero the en2's high 4 bytes in the leaf) - 0x32698: `add r2,r8,r7` -> `add r2,r7,#4` (hash 52 bytes instead of 48) The miner's work-root leaf now zero-pads the 32-bit nonce2 to 64-bit (52-byte leaf). 2. Gateway zero-extension (datum_stratum.c, client_mining_submit): read only the low 4 bytes of the 8-byte extranonce2, memset the high 4 to 0. Second bug found en route: the miner's SUBMISSION reads 8 bytes from a 4-byte uint32_t nonce2 (stratum_sthread `__bin2hex(nonce2hex, &extranonce2, nonce2_len=8)`), so the high 4 bytes are stack garbage (the adjacent work-pointer spill slot at [fp-1120]). The pristine gateway read that garbage into the leaf -> root mismatch -> H-not-zero. The gateway zero-extension (fix #2) handles it gateway-side. A true miner-side submission patch was impractical: the ob2 binary is tightly packed (.text has only 17 bytes of fragmented free space in two 8/9-byte runs, no code cave), and the fix needs ~7 instructions. Deploy: /tmp/cgminer-sia.patched (md5 a59d7e04cb01e03cc52617320fd11614) installed at /usr/local/ob2/bin/cgminer-sia (on-device backup /var/backup/cgminer-sia.preleaf; local pre-leaf backup /tmp/cgminer-sia.patched.preleaf). Gateway rebuilt from convoy-datum (only local change = datum_stratum.c zero-extension). VERIFIED: miner log now shows "Accepted 00000028 Diff 18.4E/16384 BLOCK! Obelisk OB2 0" (previously "Rejected ... H-not-zero").