Blog

From 78 to 137 GH/s by changing one number

SummaryThe miner behind this site was averaging 78 GH/s on deep strikes. Its work interval was set to 22 ms, but the loop that sends work only comes around about every 11 ms and an interval rounds up to whole passes, so new work was really going out every 32 ms. Setting the interval to 10 ms sends work on every pass, one task per 11.4 ms. A 30-minute test then ran at 150 GH/s for the first five minutes and averaged about 138 GH/s overall. A second change, stopping at the first winning nonce, took an 11-zero strike from a fixed 15 minutes to an expected two.

Both changes went in on 9 November 2026. The numbers here come from the benchmark logs of that session and from the two commits. Where the logs do not explain something, I say so.

How I measure hash rate

The chip does not report a hash rate, and I would not trust one if it did. I count results instead.

The chip is set to report every nonce whose hash has at least 32 leading zero bits (difficulty 1, the eight hex zeros of Bitcoin's original target). On average one hash in 232 qualifies. The host re-hashes every reported nonce and keeps only the ones that verify. So:

hash rate = unique valid difficulty-1 solutions × 2^32 / seconds

"Unique" matters. The baseline run logged 2,344 reports, all valid, but only 1,093 distinct nonces. Counting raw reports would have more than doubled the answer.

The baseline: 78 GH/s

For anything deeper than eight zeros, one header is not enough. A single header's 232 nonces hold one difficulty-1 solution on average and far fewer at higher difficulty, so the driver rolls the timestamp: each task sent to the chip carries a new nTime, which makes it a fresh search space.

The daemon ran those strikes with --work-interval 0.022. The reasoning at the time was that the chip sweeps a full nonce range in about 21 ms at 300 MHz, so 22 ms would hand it new work just as it finished.

A 60-second run at that setting sent 1,879 tasks and found 1,093 unique solutions. That is 78.2 GH/s. It is also 31.3 tasks per second, one every 32 ms, which is not what a 22 ms interval should give.

The loop only ticks about every 11 ms

The mining loop is simple. Each pass reads from USB, parses any frames, checks whether it is time to send work, and then sleeps:

            if time.time() - last_work_time >= args.work_interval:
                ...
                    send_work(dev, midstate, merkle_residual, job_id=job_id, quiet=True)
                work_count += 1
                last_work_time = time.time()
            ...
            time.sleep(0.01)

Abridged from the mining loop in test_direct_usb.py.

With a 10 ms sleep plus the read, one pass takes about 11 ms. The interval check can only fire on a pass, so the real interval is the setting rounded up to a whole number of passes. Two passes come to roughly 21.5 ms, which is just short of 22, so at the old setting work went out on every third pass. That is the 32 ms in the log. The setting had been chosen to match the chip's 21 ms sweep and missed the second tick by half a millisecond.

I ran a sweep of interval settings, 30 seconds each, to see the shape:

Interval setTasks sentTasks / sUnique validGH/s
100 ms2799.312618.0
60 ms46215.422932.8
40 ms68822.939756.8
22 ms93331.152174.6
10 ms2,63087.71,226175.5
6 ms2,62887.61,051150.5
3 ms2,63087.71,032147.7
1 ms2,62987.61,059151.6

Two things stand out. Hash rate follows tasks per second almost in step. And from 10 ms down, the number of tasks sent does not move: 2,630, 2,628, 2,630, 2,629. The loop cannot send more often than it comes around, so 87.7 tasks per second (one per 11.4 ms) is the ceiling, and asking for 6, 3 or 1 ms buys nothing.

The 175.5 at 10 ms is a 30-second sample and reads high. A pair of 60-second runs gave a cleaner comparison: 10 ms sent 5,265 tasks and measured 157.8 GH/s, while 12 ms fell back to every other pass (2,711 tasks) and measured 96.9. That cliff between 10 and 12 is the tick.

The change

-        cmd += ["--ntime-roll", "--work-interval", "0.022"]
+        cmd += ["--ntime-roll", "--work-interval", "0.010"]

From commit 6003cf7, daemon/strike_daemon.py.

The soak: 150, then about 137

Short runs flatter the chip, so I ran 10 ms for 30 minutes at 300 MHz. Totals from the log: 157,806 tasks sent, 57,802 nonce reports, 57,800 of them valid. That is 137.9 GH/s over the half hour.

Two reports out of 57,802 did not verify against the timestamp their job ID mapped to. There was no USB error and no dropped device in the run.

The rate was not flat. Splitting the run into five-minute blocks:

MinutesValid solutionsGH/s
0 to 510,502150.4
5 to 109,700138.9
10 to 159,561136.9
15 to 209,584137.2
20 to 259,385134.4
25 to 309,068129.8

The log has no wall-clock stamps, so the blocks are cut by rolled timestamp, which advances once per task at a steady 87.7 per second.

So the honest figure is: 150 GH/s for the first five minutes, 137 to 139 through the middle, and about 130 in the last five. I use 137 GH/s as the sustained number. The logs do not record temperature, so I cannot say from them why the rate declines.

There is one more thing in the data that I have not explained. If every task were a complete sweep of 232 nonces, each task would yield one unique solution on average. The logs show 0.45 to 0.58 per task at the slow intervals and 0.37 to 0.47 at 10 ms. The yield per task barely fell when the interval was cut by nearly two thirds, which is why total throughput went up. Why it is near a half to begin with is an open question.

The second change: stop when you have won

Until that evening, the miner ran for a fixed time budget no matter what. An 11-zero strike had a 900-second budget, and it used all of it: 903 seconds, even when the winning nonce turned up in the first minute.

Commit c5c27b1 adds a --stop-on-valid flag. The loop exits on the first nonce that meets the target, and the normal cleanup still runs.

                    if is_valid:
                        valid_count += 1
                        if args.stop_on_valid:
                            break

            if args.stop_on_valid and valid_count:
                print("  stop-on-valid: winning nonce found, ending run")
                break

Tested on the chip, the genesis header exited in 2.9 seconds, and eight 10-zero runs exited 3 to 15 seconds after start. About 2.7 seconds of each is chip startup.

The budget is now a cap. At 137 GH/s an 11-zero block needs 1611 hashes on average, which is 128 seconds. So an 11-zero strike goes from a fixed 15 minutes to about 2 minutes on average, and some will be much quicker or slower than that. The arithmetic is in a separate post.

Why there is no shortcut to more zeros

Both changes make the machine waste less time. Neither makes the problem easier, and nothing can.

SHA-256 gives no partial credit and no direction. A header whose hash starts with nine zeros is no closer to ten than any other header. Flip one bit of the input and every bit of the output is, as far as anyone can tell, a fresh coin toss. There is no way to steer toward zeros, only to try another input.

That makes each hash an independent draw with a fixed chance of success: 1 in 16n for n leading hex zeros. Each extra zero multiplies the expected work by 16. The only levers are the ones in this post: do more draws per second, and stop drawing when you have what you need.

It also means past effort does not accumulate. If an 11-zero search has run for five minutes without a hit, the expected time remaining is still about two minutes.

You can compare your own browser's rate against the chip with the browser vs ASIC tool.

The tiers this affects are on the home page: 8, 10, 11 and 12 leading zeros. Each is a standalone block mined on this chip, not a block on Bitcoin's chain.