Reference implementation for the blog-post series "Build a mining chip with BOOM": 1x small BOOM core runs Linux + stratum client; 8 SHA-256d mining engines do the actual hashing at 1 nonce/cycle/engine.
hw/sha256.scala 64-stage fully-pipelined SHA-256 compression core
hw/MiningAccel.scala mining engine FSM + TileLink MMIO wrapper + Chipyard config
hw/ControlCoreConfig.scala trimmed-BOOM fragment (no FPU, small caches)
hw/RocketConfig.scala Rocket-based configs: RocketMiningConfig (primary,
tapeout-oriented), RocketMiningDualConfig, and the
WithTinyControlRocket trim fragment
sw/miner_baremetal.c OS-less bring-up smoke test (HTIF exit code)
sw/miner_driver.c Linux userspace driver (mmap register layer)
sw/stratum_client.c COMPLETE stratum client (subscribe/authorize/notify/
submit, merkle fold, share target) -- verified
tests/golden.py golden reference model (pure-python SHA-256 w/ midstate)
tests/vectors.json generated golden vectors
tests/Sha256UnitTest.scala chiseltest unit test ("abc" known answer)
tests/MiningEngineTest.scala engine test against vectors.json
Block1 (bytes 0..63, no nonce)
midstate = compress(H0, Block1) <- once per template,
computed BY the inner pipe
Block2 (bytes 64..79 + padding, nonce in W[3])
inner = compress(midstate, Block2(nonce)) <- 64 rounds, streamed
outer = compress(H0, pad(inner)) <- 64 rounds, fully constant pad
hit = outer <= target <- a-word = MSB
Per-engine throughput: 1 nonce/cycle sustained (128-cycle latency, pipelined). 8 engines @ 1 GHz = 8 Ghash/s per chip.
- All MMIO words are BIG-ENDIAN interpretations of header bytes as SHA-256 sees them (software byte-swaps each 32-bit field when writing).
- The integer nonce is byte-reversed into W[3] inside block2Gen (Bitcoin header fields are little-endian on the wire). foundNonce is reported as the true header nonce (pre-reversal value), ready for stratum submit.
- Bitcoin compares the double-SHA digest as a LITTLE-ENDIAN 256-bit integer (Bitcoin Core arith_uint256): byte-reverse the whole 32-byte digest and read it as a big-endian number. Hardware: Cat(digest.reverse.map(bswap32)) <= target, with the target written by software in display order (MSW @0x60). The genesis-block KAT caught this TWICE (first BE words, then BE bytes within words) -- ALWAYS validate against a known historical block before tapeout.
0x00 W ctrl bit0=start bit1=load_template bit2=clear
0x04 R status bit0=busy bit1=done
0x08 RW irq_en -> PLIC source 0
0x0C RW nonce_start
0x10 RW block1_w[0..15] (0x10 + 4*i)
0x50 RW b2_w[0..2] (merkle tail | time | bits)
0x5C RW nonce_stride (per-engine split, default 0x02000000)
0x60 RW target[255:0] (8 words, word @0x60 = MSW)
0x80 R nonce_found
0x84 R digest_found[255:0]
0xA4 R found (sticky; clear with ctrl.bit2)
1. write block1_w, b2_w, target
2. ctrl = 0x2 (load_template; engines compute midstate, ~70 cycles)
3. poll status.busy == 0
4. write nonce_start; ctrl = 0x1 (start)
5. poll found / wait IRQ; read nonce_found, digest_found
6. ctrl = 0x4 to clear, go to 4 with next window or 1 for new template
Rocket is the recommended control core for this SoC (in-order, ~5x smaller than SmallBoom, trivial timing closure at any node, fast simulation); BOOM remains useful for bring-up and as a learning vehicle. Both configs share the identical accelerator + software (the control core only sees TileLink).
# from chipyard root; copy or symlink this repo into generators/mining
cd sims/verilator
make CONFIG=RocketMiningConfig # Rocket control core (tapeout)
make CONFIG=BoomMiningConfig # BOOM control core (bring-up/dev)
# smoke test (genesis template, target=all-ones, expect nonce 0):
make CONFIG=RocketMiningConfig run-binary BINARY=miner_baremetal.riscv
# bare-metal smoke test:
make CONFIG=BoomMiningConfig run-binary BINARY=miner-baremetal.riscv
# FireSim:
cd sims/firesim && make config-recipe CONFIG=BoomMiningConfig ...
# Scala unit tests (need chiseltest + spray-json on the classpath):
cd generators/mining && sbt "testOnly mining.Sha256PipeTest mining.MiningEngineTest"
# golden vectors:
python3 tests/golden.py
# stratum client logic test (no hardware needed; cross-check vs hashlib):
gcc -O1 -DSTRATUM_TEST sw/stratum_client.c -o /tmp/st
printf 'SUBSCRIBE_RESULT {"id":1,"result":[[["mining.set_difficulty","d"],["mining.notify","n"]],"c0ffee00",4]}\n' \
'NOTIFY {"id":null,"method":"mining.notify","params":["bf41","0000000000000000000000000000000000000000000000000000000000000000","aabbccddee","ffeeddccbbaa",["1111111111111111111111111111111111111111111111111111111111111111","2222222222222222222222222222222222222222222222222222222222222222"],"20000000","1d00ffff","66f2b8a0",false]}\n' \
'DIFF 1024\nABC\n' | /tmp/st
# expected: MERKLE 68cad05d..., ABC 4f8b42c2... (double-sha256 of "abc")
- No backpressure between pipes: consumer must accept 1 result/cycle. Add skid buffers if the outer pipe ever stalls (multi-bank target compare, throttling on thermal events).
- Nonce window split is static (stride). Production: per-engine dynamic work queues or rollover chaining.
- Target compare is exact-256-bit; real pools hand you a 256-bit target already expanded from 'bits', so this is fine -- just never compare against the encoded compact form.
- Power/clock gating of idle engines is left as an exercise (ASIC tapeout item: per-engine clock gates + nonce-range done interrupts).
- The RegWriteFn target update path clears-and-sets one word; make sure software writes all 8 words before load_template.
- SubsystemInjector / pbus.coupleTo API names shift between Chipyard releases -- check against your chipyard version's custom-device docs.
- Sha256PipeTest green ("abc" known answer)
- golden.py selftest green (matches hashlib incl. midstate path)
- MiningEngineTest green on vectors.json (midstate + 4 nonces)
- Verilator full-chip: BOOM writes template, engine finds a nonce for a historical block header with a known solution nonce
- FireSim: Linux boots, miner_driver mmap OK, stratum connect OK, share submits accepted by a test pool (regtest/testnet)
- FPGA (VCU118-class): 100-200 MHz, error-rate soak 24h
- 130nm shuttle (SkyWater/Efabless): PLL, IO, DFT, scan chain, power OK
- golden/RTL: nonce must be byte-reversed into W[3] (header fields are LE).
- golden/RTL: Bitcoin compares the digest as a LITTLE-ENDIAN 256-bit int (word order reversed).
- golden/RTL: ... AND byte order within each word is reversed too (genesis KAT caught both layers separately).
- stratum JSON parser: key/value separator ':' was never consumed.
- stratum JSON parser: object sibling chain overwrote key->value links.
- stratum hex2bin: read h[1] past the NUL terminator for even-length strings (UB; passed by luck on some layouts, failed on others).
- stratum sha256_fin: padding bytes went through sha256_upd, corrupting the bit counter before the length field was written.
- stratum sha256_fin: SHA-256's 64-bit length field is BIG-ENDIAN
- design-doc (not code): rounds 0..17 of Block 2 were claimed nonce-independent -- actually only rounds 0..2 are; the chaining add-back must use the original hinit, not the segment start. Caught by extending the golden model before writing any RTL. (C wrote it LE; empty message masked this because 0 == 0 in either order, and the "known-answer" blk test only passed because the manual vector happened to encode the length big-endian).
Moral: every one of these survived code review. Only golden-vector cross-checks (hashlib for crypto, python-vs-C for the client) killed them. Never tape out logic that has not matched an independent golden model.
make CONFIG=RocketMiningConfig # Rocket control core (tapeout)
make CONFIG=RocketMiningDualConfig # 2x Rocket: miner + monitor
make CONFIG=BoomMiningConfig # BOOM control core (bring-up/dev)
tests/golden.py now emits a state3 vector: inner rounds 0..2 use only
constant W[0..2], so they can run at template load. Corrections vs earlier
notes: (a) round 3 already consumes W[3]=nonce -- only 3 rounds are
precomputable, NOT 18 (the W[16]/W[17]-are-constant observation does not
help rounds 3..15); (b) the chaining add-back still uses the ORIGINAL hinit
(midstate) -- carry it to the final adder, do not add state3.
- A0 benchmarks defined (BENCHMARKS.md, named targets: BZM2 / SMART / Proto)
- A1 feasibility model: CSA+DVFS @28nm ~18-32 J/TH (docs/a1_model.json)
- A1 RTL:
useCSAparameter in hw/sha256.scala, same golden vectors - A2 16nm open-PDK shuttle scouting
- A3 engine_manager.c integration; RTL TODO: per-engine ENABLE bit (masking currently needs empty-window surgery)
- A4 sv2_client.c skeleton per docs/stratum_v2.md
- Kill criterion: B1 > 40 J/TH @28nm gate-level -> drop product Line C.