Documentation
MachinaHash (MH-1) — the PoW algorithm for PowMachina
Status: the calibration is closed and the algorithm is two. The CUDA kernels exist (
machinahash-cuda) and are bit-exact against all 1,000 vectors on sm_86 and sm_120 alike.Gate 2 (the B-core on a GPU) met — 12.0× against an EPYC 9474F, lane G pulls 44 % of the card’s ceiling. Gate 1 (the L-core on a CPU) not met — against a Ryzen 7950X3D the ratio came out at 0.47×, that is, a 96 GB card is faster on lane C than a desktop. Details in §7.5.
The decision (§7.6):
MachinaHash-C= RandomX v2 with the PowMachina salt (CPU) ·MachinaHash-G= the B-core without the L segment (GPU) · no lane H (§7.7). The lane rotation rule, the per-lane retargets, the chainwork normalization, MESS and finality do not change.Naming: in the UI the algorithms are called
MachinaHash-CandMachinaHash-G, with no additions — no “RandomX v2” and no “CPU/GPU” in brackets on screen. What is under the hood belongs here, ininternal/randomx/csrc/VENDOR-RANDOMX.mdand inNOTICE. MachinaHash-C is not stock RandomX: it changes the Argon2 salt, so the hashes do not match upstream (just as with RandomWOW, RandomXL or RandomARQ). RandomX’s license (BSD-3) does not reserve the name; it asks for a copyright notice with binaries too and forbids promoting our product with tevador’s or Monero’s name — henceNOTICEand the sentence saying that none of them endorses us.After the change (§7.7): lane G does 1.12 MH/s on an RTX PRO 6000 = 65 % of the card’s ceiling, gate 2 = 17.3×, gate 1 flipped from 0.47× to ~18× (with large pages).
Sections 2.1 and 7.1–7.3 describe the L-core and a design with three lanes. That design was measured and rejected; the sections are kept as the record of what was measured and why it was not enough — not as a valid design.
1. What the algorithm has to achieve
| Goal | How |
|---|---|
| CPUs get a foothold | their own lane with its own difficulty + a latency-hard phase where a CPU wins |
| GPUs get a foothold | their own lane + a bandwidth-hard phase where a GPU wins |
| ASICs/FPGAs make no sense | a chip would have to cover two opposing hardware domains at once; otherwise it earns at most 1/3 of the emission |
| The shares are guaranteed | not by faith in a “fair” algorithm, but by consensus (per-lane difficulty + the rotation rule) |
Why not “one fair algorithm”
Memory-hard PoW is always either latency-bound (the critical path = the serial latency of random accesses → CPUs win: a big cache, low latency, strong single-thread) or bandwidth-bound (many independent accesses → GPUs win: GDDR6X/HBM has 10–15× the throughput of DDR5). It is not a matter of tuning, it is a matter of what is on the critical path.
Anyone trying to hit parity with a single algorithm (Xelis: ~440 KB of scratchpad in v2, ~544 KB in v3, ChaCha8 + Blake3 + AES rounds) has to dig it up again with every new hardware generation — Xelis changed its structure three times from v1 to v3, the last time precisely in order to suppress GPU parallelism. That is proof that parity is not a state but never-ending maintenance.
MachinaHash turns that around: it does not attempt parity in the algorithm. It has two clearly separated domains and guarantees each of them a share of the emission in consensus.
2. The two cores
2.1 The L-core (the latency domain — the CPU’s domain)
S = ChaCha8(key = BLAKE3(header || nonce || lane)) → fills sp_bytes of scratchpad
acc = BLAKE3_XOF(S[0..64]) → a 4× u64 accumulator
for i in 0 .. ITER_L-1: # serially dependent, the count is set by the lane
idx = acc[0] mod (1 MiB / 32) # the index depends on the previous iteration
v = S[idx*32 .. idx*32+32] # a 32 B read
acc = MIX64(acc, v) # add/xor/rot + a 64×64→128 widening mul
S[idx*32 ..] = acc # written back (the scratchpad changes)
L_out = BLAKE3(S)
Properties:
- A 64 KiB – 1 MiB scratchpad, the segment picks the size. A full 1 MiB fits in the L2 of a modern x86 core (Zen 4/5: 1 MiB/core, Intel P-core: 2 MiB). The size is a segment parameter because filling the scratchpad is parallelizable work — a short L segment must pay a short fill, otherwise the fill dominates the latency mix (in the first measurement it was 62 % of lane G’s time).
- A pointer chase —
idxdepends on the data just read → no ILP, no prefetch, no unrolling into SIMD. The critical path =ITER_L × (L2 latency + the ALU mix). - Writing back — it rules out TMTO (a time-memory trade-off) and recomputing the scratchpad from scratch.
- A 128-bit widening mul — expensive on an FPGA, an ordinary instruction on x86/ARM.
- A GPU suffers twice here: 1 MiB × thousands of threads does not fit in L2 (4–96 MiB) → it goes to VRAM with a latency of ~350–500 cycles, and parallelism does not help, because the critical path is serial.
2.2 The B-core (the bandwidth domain — the GPU’s domain)
DAG = the epoch's dataset (starting at 1 GiB, +16 MiB/epoch), generated from a 16 MiB cache
mix = BLAKE3(L_out)[0..256] # a 256 B mix state
for r in 0 .. ROUNDS_B-1:
for k in 0 .. 63: # 64 INDEPENDENT accesses
idx = FNV1a(mix, r, k) mod (|DAG| / 256)
mix = RANDMATH(mix, DAG[idx*256 .. +256]) # mul/rot/xor/clz/popcount, seed = the epoch
mix = BLAKE3(mix) # a barrier between rounds
B_out = BLAKE3(mix)
Properties:
- Independent 256 B accesses, the count per round is picked by the segment (64 in lane C, 256 in G and H) → purely bandwidth-bound; a GPU coalesces and parallelizes, a CPU is limited by its DDR channels.
- A 1–8 GiB DAG — beyond any on-chip SRAM; an ASIC would need GDDR/HBM = a GPU at a worse price. It grows +4 MiB/epoch with a cap of 8 GiB (reached after ≈ 34 years): the growth keeps out an ASIC with fixed memory, the slow pace keeps the algorithm from retiring cards quickly — Ethash retired 4 GB GPUs. With an epoch of 6.9 days that is 2.0 GiB after 5 years and 3.1 GiB after 10, so a 6 GB card gets a decade of mining. Above ~4 GiB (≈ after 14.5 years) 4–6 GB cards, 8 GB laptops and SBCs drop out — that gets handled with a tweak when the time comes.
- The RANDMATH sequence is seeded by the epoch (the ProgPoW/KawPow principle) — the order of operations changes every epoch, so a hardwired datapath has nothing to optimize.
- Light verification: a DAG item is recomputed from the 16 MiB cache (the Ethash model). But
it is expensive: the cost grows linearly with the number of items read, and in F2 light
verification of lane G came out at 101 ms/block with 8,192 items. Hence: lane G is capped at
4,096 items (1 MiB) per hash and the full DAG is held by a miner and by a node that is
catching up on history (≈1 GiB of RAM, verification ~1 ms). A node at the tip verifies light
(
-dag=auto): a block every 2 min in ~63 ms with 28 MiB instead of 1,052 MiB (measured, mainnet epoch 0). An initial sync entirely on the light path (-dag=light) takes ~2 h per year of history. Both paths call the same item function (Cache.datasetItem) and give a bit-identical result —TestLightAndFullDagAgree,TestLightVerifierMatchesFull.
3. The lanes — where the guarantee comes from
A block declares lane ∈ {C, G} in its header. The lane determines which algorithm is
computed — not the ratio of rounds of one algorithm.
| Lane | Algorithm | A miner’s memory | Optimal hardware | Share of blocks |
|---|---|---|---|---|
| C | MachinaHash-C — RandomX v2, the PowMachina salt | a 2.1 GiB dataset (fast) | CPU | 1/2 |
| G | MachinaHash-G — the B-core over the epoch DAG, 16 × 256 accesses | a 1 GiB DAG (growing) | GPU | 1/2 |
The PoW preimage is the same for both: header(80) ‖ nonce LE(8) ‖ lane(1), that is 89 bytes
with the nonce at offset 80. Lane C hands it to RandomX directly — the same pattern rx/scash and
rx/pcoin use over the Bitcoin header. A miner therefore needs to know a single thing: where the
nonce is.
The guaranteed share of the emission does not come from the algorithm but from consensus. Each lane has its own LWMA, so its difficulty settles at half the blocks regardless of how much hashrate it has. The work is normalized across the lanes (§5) so that an attacker cannot “buy” chainwork in the cheaper one. That is why a core can be swapped without touching any of this — which is exactly what happened in the move to RandomX.
The lane is part of the preimage, so a nonce found in one lane is useless in the other.
There is no hybrid CPU+GPU lane. A lane H was designed and measured; it had a single purpose — for a balanced rig to beat the same hardware split into C+G. With RandomX that is unachievable in principle: as the CPU half RandomX is so heavy that the GPU half comes for free next to it, and the tandem came out 11× worse than C and G separately. The details are in §7.7.
4. The lane rotation rule (a defence against 51 % on the weakest lane)
The classic weakness of multi-algo coins: an attacker picks the lane with the lowest hashrate and reorganizes the chain on their own. Hence:
A valid block must not have the same lane as both of its two direct predecessors. (At most 2 blocks of the same lane in a row.)
The genesis does not count towards the run. Its lane is a constant written into the binary, not the result of mining — if it counted, which lane may mine the network’s first blocks would be decided by a number we made up — a CPU miner would mine one block and then wait for a GPU.
The consequence: a longer reorg is not possible with one domain — an attacker needs both lanes, that is both CPU and GPU capacity.
With two lanes the rule is harder than with three: after two blocks of the same lane a single option is left, not two. And it matters more than it did: lane C is RandomX, so rented hashrate can be pointed at it (Monero has orders of magnitude more, NiceHash sells it). An attacker with rented CPUs makes at most two blocks in a row and then has to deliver lane G — and that they cannot rent.
The liveness fallback: if the gap between the block and its parent exceeds 900 s, the rule is not applied to that block (otherwise one lane dying out completely would stop the chain).
The gap is measured against the parent block’s time, not against median time past. MTP is the median of eleven blocks, so it trails the tip by roughly six of them (~12 minutes at the 120 s target the network had then, ~3 at today’s 30 s) — measured against MTP, a perfectly healthy chain satisfies the relief almost always and the rule is switched off nearly all the time. That is not a theoretical concern: a test network running with the relief measured against MTP produced 31 lane C blocks in a row.
5. Chainwork across the lanes
A naive 2^256/target does not work — the lanes’ difficulties are not on the same scale and an
attacker would pick the one where “work” is cheapest to buy. We use normalization by a geometric
mean (the DigiByte MultiShield model):
W_block(lane) = (2^256 / target_lane) * k_lane
k_lane = geomean(avg_work_C, avg_work_G) / avg_work_lane
where avg_work_* is the average over the last 1,440 blocks of that lane, k_lane is recomputed
once per epoch (5,000 blocks) and is deterministically derivable from the chain (no external
input). The effect: one block from each lane weighs the same.
With two lanes the geometric mean is an integer square root, that is big.Int.Sqrt —
deterministic and with no code of our own. The cube root, which the project had to write itself,
went away along with lane H.
6. The difficulty retarget
The rules are in the specification §3.6; in short:
- Per lane, against the chain’s clock: an exponential retarget in the style of aserti3-2d with a half-life of 1 h, recomputed at every block. A silent lane gets easier at the speed of real time, because the other lane keeps the clock moving.
- The target time: 60 s per lane (two lanes, a 30 s network block).
- One gap moves a lane by at most 16× either way, never below the network’s floor.
- The timestamp: not before the parent’s (the same second is allowed), at most 30 s into the future of the validating node’s clock.
7. Calibration — MANDATORY before the parameters are fixed
mh-bench measures it; the key metric is not the hashrate but how the time breaks down across
the segments. The conditions:
- lane C: a CPU/GPU ratio of ≥ 8× in the CPU’s favour (per socket vs per card),
- lane G: a GPU/CPU ratio of ≥ 8× in the GPU’s favour,
lane H on a balanced rig beat the same hardware split into C+G by at least 20 %— gone along with lane H (§7.7): with RandomX as the CPU half the GPU half comes for free, so the tandem came out 11× worse than C and G separately. It was not close, it was a matter of principle.- the time of one hash was in a range stratum can live with (the target: 0.5–2 ms for lane C).
The hardware we have for it: an EPYC 9474F (48C/96T, Zen 4, 12-channel DDR5) and an RTX 4080 (716 GB/s). At least one ordinary desktop (6–8 cores) and one older GPU (an RTX 3060 / RX 6600) are to be added, otherwise we set the parameters by the extremes.
Without 1–2 being met there is no reason to separate the lanes and the whole model loses its point. That is exactly what happened — gate 1 failed and lane C was swapped for RandomX (§7.5–7.6). The lane structure survived it unchanged, which was the whole point of describing it separately from the cores (SPEC §8).
7.1 The F2 results — three lanes and the L-core
The reference measurement: a Ryzen 9 7950X3D (Zen 4, 16C/32T, AVX2, 1 MiB L2/core, 3D V-Cache), a 1 GiB DAG = the mainnet size, on the RTX PRO 6000 host.
| lane | H/s (1 thr.) | H/s (16 thr.) | ms/hash | latency | bandwidth | fill |
|---|---|---|---|---|---|---|
| C | 570 | 8,427 | 1.75 | 80 % | 12 % | 7 % |
| G | 1,622 | 19,635 | 0.62 | 1 % | 97 % | 1 % |
| H | 1,026 | 14,831 | 0.97 | 52 % | 33 % | 14 % |
The gate (the CPU part): 3/3 met — lane C is latency-bound (80 % ≥ 75 %), lane G is bandwidth-bound (97 % ≥ 80 %), lane C is 1.75 ms (the window is 0.5–2 ms). The sum of the segments matches the real hash (±6 %).
Lane C’s margin is thin. Repeated runs on the same machine give a latency of 75–80 % (a median of ~78 %) against a threshold of 75 %, and a hashrate of 558–584 H/s. The gate passes, but one run touched the boundary. If the lower end of that range is confirmed elsewhere too, the fix is to reduce lane C’s B segment from 6 rounds to 5 (the light penalty drops from ~3.7× to ~3.3×).
The L-core, ns per mix iteration by scratchpad size: 64 KiB → 4.35 · 256 KiB → 4.79 · 512 KiB → 5.44 · 1 MiB → 6.87. The B-core (cold accesses): 1.71 GB/s per thread, 20.7 GB/s on 16 threads; one DAG item costs ~150 ns — that is DRAM latency, not throughput.
Light verification (without the full DAG): lane C 6.63 ms · lane H 7.35 ms · lane G 13.40 ms.
A measurement trap. A benchmark that measures the segments with a fixed seed has the B-core reading the same DAG items over and over, and they sit in cache after the first round. The result is optimistic everywhere there is a lot of DAG work: lane G showed 90 % bandwidth (really 97 %), lane H 73 % latency / 8 % bandwidth (really 52 % / 33 %), the B-core ceiling 120.8 GB/s (really 20.7 GB/s). With lane C’s 64 items the error did not show. It was uncovered by the discrepancy between the sum of the segments and the real hash; the benchmark therefore rotates the seed and warns about a discrepancy > 10 % itself. The numbers here are from the rotating-seed benchmark.
The first round (a QEMU VM, 8 vCPUs without AVX2) gave C 2.65 ms / 82 % latency, G 0.85 ms / 84 % bandwidth, a B-core ceiling of 36.9 GB/s. It served for the profile, not for absolute numbers — and it is exactly where the bugs described below were found.
What the measurement implied and what changed:
- The original parameters gave lane G a profile of 62 % fill / 4 % DAG — a GPU lane that does not work with memory. The fix: the scratchpad size is now a segment parameter (lane G has 64 KiB) and the B segment has 256 accesses per round instead of 64.
- Light verification of lane G came out at 101 ms/block → lane G was capped at 1 MiB of DAG per hash and a full node holds the full DAG (see §2.2).
- Settled by measurement: lane C ran at 2.65 ms on the VM (above the window). The parameter was deliberately not changed and on a real CPU with AVX2 it came out at 1.52 ms — so calibrating by a QEMU VM really would have been a mistake.
- Lane H is not fill-bound on real hardware: 38 % on the VM → 18 % on Zen 4 (ChaCha8 with AVX2). It remains open whether moving the fill onto the GPU pays off with a pipeline — F3 decides that.
7.2 What the Zen 4 measurement opened up for F3
Big L2/L3 caches shrink the penalty for a 1 MiB scratchpad. On the VM the mix latency rose from 64 KiB to 1 MiB by 83 % (5.93 → 10.87 ns), on the 7950X3D by 58 % (4.35 → 6.87 ns) — 1 MiB fits in a core’s L2 and, on top of that, in the 3D V-Cache. That means GPUs with a big L2 (Ada 96 MB, Blackwell 128 MB) may be better at the L-core than the design assumes. F3 has to measure that directly; if it is confirmed, the response is to grow lane C’s scratchpad beyond the capacity of a GPU’s L2 per thread, not to add iterations.
The B-core ratio looks safe. With cold accesses a CPU manages 20.7 GB/s on 16 threads, because it is limited by DRAM latency (~150 ns per item), not by throughput. A GPU parallelizes thousands of accesses at once over 936 GB/s (an RTX 3090) up to ~1.8 TB/s (an RTX PRO 6000). The original worry that the ≥ 8× gate might fail came from the distorted figure of 120.8 GB/s. Only the GPU kernel can confirm it.
7.3 Lane C without the DAG — the hole and its limits
The hole: lane C read only 64 DAG items per hash, so it could be mined without the DAG — with just the 16 MiB cache, from which the items are recomputed. The light path was only ~1.5× slower. Lane C could therefore be mined on devices with nowhere to store a DAG at all: routers, IoT, a browser. Since lane C has a guaranteed third of the emission, that is exactly the kind of hashrate we do not want.
The fix: lane C reads 1,536 items (6 rounds × 256). The measurement for real (a rotating seed, Zen 4):
| rounds | items | hash time | light penalty | latency |
|---|---|---|---|---|
| 1 (originally) | 256 | 1.58 ms | 1.5× | 89 % |
| 4 | 1,024 | 1.70 ms | 2.9× | 83 % |
| 6 | 1,536 | 1.77 ms | 3.7× | 79.5 % |
| 8 | 2,048 | 1.85 ms | 4.4× | 76 % |
| 12 | 3,072 | 2.01 ms | 5.8× | 70 % ✗ |
The limit of this fix: while keeping lane C’s latency profile it cannot get above ~4×. One DAG item costs ~150 ns from RAM and recomputing it from the cache ~3.3 µs — so at most ~22× per item, and every extra item pushes lane C from latency towards bandwidth. 4× combined with the weak CPUs of such devices is enough to drop out routers, IoT and the browser. It does not stop a botnet on ordinary PCs — those simply load the 1 GiB DAG.
A stronger penalty is possible, but outside lane C: raising the cost of recomputing an item
(DAG_PARENTS 256 → 1024) would make the light path ~4× more expensive with no effect on the full
DAG. It would, however, touch every lane and every vector, slow down building the DAG (~1 s → ~4 s
per 1 GiB) and light verification for SPV clients (lane G ~13 → ~54 ms). Not done — an open option.
What the algorithm will never solve: a botnet on ordinary PCs with enough RAM, and cloud cryptojacking (stolen credits). Lane C has a guaranteed third of the emission — if botnets take it over, they get it. The decision to leave lane C its guaranteed share was taken knowingly, with that risk.
7.4 What is left to measure
| Measurement | Where | Status |
|---|---|---|
| a CPU with AVX2, 16 cores (Zen 4) | the 7950X3D | done — §7.1 |
| an EPYC 9474F, 12-channel DDR5 | a 48-core Zen 4 host | done — §7.5 |
| an RTX 3090 (936 GB/s) | a dedicated host | done — §7.5 |
| an RTX PRO 6000 Blackwell (1,792 GB/s) | a dedicated host | done — §7.5 |
| an older GPU (a 3060 / RX 6600) | to be added | open — but the gate already decided without it (§7.6) |
7.5 The F3 results — the gate decided
The CUDA kernels are in machinahash-cuda/, bit-exact against all 1,000 test vectors on both
cards (sm_86 and sm_120). The mainnet profile, a 1 GiB DAG.
| lane C | lane G | lane H | |
|---|---|---|---|
| EPYC 9474F (1 socket, 96 thr.) | 19,502 | 62,477 | 38,062 |
| Ryzen 9 7950X3D (16C/32T) | 8,427 | 19,635 | 14,831 |
| RTX PRO 6000 (1,792 GB/s) | 18,092 | 749,053 | 30,886 |
| RTX 3090 (936 GB/s) | 6,958 | 260,544 | 9,370 |
Gate 2 (the B-core ≥ 8× in the GPU’s favour): MET. 6000/EPYC = 12.0× · 6000/Ryzen = 38.2× · 3090/Ryzen = 13.3× · 3090/EPYC = 4.2×. On the 6000 lane G pulls 785 GB/s, that is 44 % of the card’s theoretical ceiling; on the 3090 273 GB/s (29 %).
Gate 1 (the L-core ≥ 8× in the CPU’s favour): NOT MET, and not narrowly. EPYC/6000 = 1.08× · Ryzen/6000 = 0.47× (the card is 2.1× faster) · Ryzen/3090 = 1.21×.
Why the L-core did not hold up against a GPU
The L-core’s only weapon is a 1 MiB scratchpad with a serial dependency. A GPU beats that with parallelism:
CPU: 96 cores / 4 ns (L2) = 24 accesses/ns
GPU: (96 GB / 1 MiB) / 400 ns = 240 accesses/ns
It cannot be saved by the scratchpad’s size. For a CPU to win 8× against a 96 GB card, the scratchpad would have to be ≈ 80 MB — and that fits in no cache, so the CPU would be DRAM-bound too and the advantage would vanish. Against a 24 GB card it comes out at 20 MB, the same problem.
Three asymmetries remain at the level of operations, and only one of them is big:
| lever | effect | in the L-core |
|---|---|---|
| data-dependent branching (a program per hash) | warp divergence, up to 32× | no |
| AES-NI | a CPU does 1 round/cycle, a GPU ~20× slower in software | no |
| floats with exact rounding | a GPU emulates them | no |
The measurement shows why AES alone is not enough: on lane C a GPU runs at 0.6 % of its ALU capacity (it is bound by memory latency), so it would absorb added arithmetic entirely. Branching is the only thing that breaks the SIMT model the card stands on.
Why chaining more CPU algorithms does not help
The CPU/GPU ratio is T_gpu / T_cpu and chaining adds the times on both sides, so the result
is a weighted average of the components — it never exceeds the best one:
| composition (the same CPU time for each part) | CPU/GPU |
|---|---|
| RandomX-like only (branching + AES) | 6.0× |
| RandomX-like + our L-core | 3.5× |
| RandomX-like + Argon2-like | 3.8× |
| all three | 2.9× |
Every weaker component dilutes the ratio. The right move is to give the best construction 100 % of the lane.
7.6 The decision: two named algorithms
| content | status | |
|---|---|---|
| MachinaHash-G | the B-core as it is — the DAG, RANDMATH, the FNV mix | stays, the gate is met |
| MachinaHash-C | RandomX with PowMachina’s own configuration | replaces the L-core |
| lane H | a tandem of MachinaHash-C + MachinaHash-G | by the same model |
Why RandomX and not our own VM: “a program per hash with branching” is a virtual machine. Writing our own means a cryptographic design from scratch, our own audit and years of trust — and it would end up very similar, only unvetted.
What of it is still ours:
- our own parameters (the scratchpad, the program length, the instruction frequencies). There
is precedent — xmrig supports
rx/wow,rx/arq,rx/sfxand other variants. - our own key schedule — a RandomX key changes periodically, which fits exactly onto the rotation every 130,000 blocks from §8.
- the lane architecture with a guaranteed third of the emission for each domain — that is the part nobody else has, and a change of core does not touch it.
What does NOT change: the lane structure, the per-lane LWMA, the lane rotation rule, the
chainwork normalization, MESS, finality, the whole of powmachinad. That is exactly why the lane
architecture and the cores are described separately in this specification (SPEC §8).
7.7 The results with two algorithms
The change was made and measured. Lane C = MachinaHash-C (RandomX v2), lane G = MachinaHash-G without the L segment, no lane H.
Lane G is 50 % faster without the L segment
Removing the L segment threw the ChaCha8 fill and the 64 KiB per-thread scratchpad out of the GPU kernel. The scratchpad was 11 % of the memory traffic — and, above all, what capped the number of concurrent hashes on a 24 GB card before the hashrate stopped rising.
| with the L segment | without it | ||
|---|---|---|---|
| RTX PRO 6000 | 749,053 H/s · 44 % of the ceiling | 1,115,844 H/s · 1,170 GB/s · 65 % of the ceiling | 1.49× |
| RTX 3090 | 260,544 H/s · 29 % of the ceiling | 390,867 H/s · 410 GB/s · 44 % of the ceiling | 1.50× |
A thread now needs only 65 words of shared memory, so VRAM holds the DAG alone.
The CPU (an EPYC 9474F, 1 socket, 96 threads, a 1 GiB DAG)
| H/s | ms/hash | |
|---|---|---|
| lane C, W^X on | 12,006 | 1.96 |
| lane C, W^X off | 25,112 | — |
| lane G | 64,515 (67.7 GB/s) | 1.06 |
Light verification (a node without the full datasets): lane C 24.9 ms, lane G 98.0 ms.
The gate
| result | ||
|---|---|---|
| Gate 2 — lane G, GPU/CPU ≥ 8× | 6000/EPYC = 17.3× | ✅ |
| 3090/EPYC = 6.1× | against the hardest CPU reference | |
| Gate 1 — lane C, CPU/GPU ≥ 8× | EPYC/RTX 4090 = 7.4× without large pages | ⚠ |
| the estimate with large pages ≈ 18.5× | ✅ once configured | |
| Gate 4 — lane C within the 0.5–2 ms window | 1.96 ms | ✅ |
For comparison: the original L-core had gate 1 at 0.47× — the card was faster than the desktop. The change therefore flipped the ratio by roughly 16-fold.
Two things an operator has to know
RANDOMX_FLAG_SECURE (W^X) costs 2.17× — 12,006 vs 25,112 H/s on 96 threads. On a single
thread almost nothing, so it is not the cost of mprotect but a TLB shootdown across the cores.
The API handles it with a role: Verifier (foreign data, W^X always) and Miner { secure } (your
own template). A node pays for one hash per block, so the cost does not concern it.
Large pages have to be configured by hand. RandomX uses MAP_HUGETLB exclusively, that is,
reserved vm.nr_hugepages; transparent huge pages in madvise mode are not enough for it (it does
not call madvise). Without them the whole of RandomX runs on 4 KiB pages and loses ~2.5×. It is
the same requirement xmrig has — but it belongs in the documentation for miners.
8. Rotation
What rotates and what does not
| Interval | Who has to do something | |
|---|---|---|
| RANDMATH in the B-core + DAG growth (lane G) | every epoch, 5,000 blocks ≈ 6.9 days | nobody, it is derived from the height |
| The RandomX key (lane C) | the same epoch | nobody, it is derived from the height |
An epoch’s key is BLAKE3(DOMAIN_RX_KEY ‖ epoch_LE). It is derived from the height alone, not
from the hash of an older block as in Monero. The dataset is public and deterministic anyway, so
deriving it from the chain gains nothing — and it removes the question of what to do about a reorg
across an epoch boundary. A miner can produce the key arbitrarily far in advance.
No scheduled rotation of the CPU algorithm
The L-core design changed its mix variant every 130,000 blocks and required a new binary. That had a single purpose: abandoned mining malware has nobody to update it, so it dies out. Monero ended 72 %, 89 % and 96 % of campaigns that way in 2018–19 (IMC 2019).
With RandomX that stops making sense from two directions:
- The mechanism would not work. Rotation only works when a miner has to obtain new code. With RandomX that would mean a new algorithm ID in xmrig every six months — nobody maintains that and miners would rightly see it as harassment.
- The replacement is better. RandomX asks for 2.1 GiB per miner in fast mode. That is a memory barrier the L-core with its 1 MiB scratchpad did not have at all. A botnet cannot inconspicuously ask for 2 GiB on somebody else’s computer, and light mode is orders of magnitude slower.
Monero, incidentally, rotated its PoW exactly as long as it did not have RandomX. Rotation was a substitute for the resistance it lacked, not an addition to it.
Changing the PoW by a hard fork remains an option — it just is not planned in advance. The
signalling bits in the header’s version have existed for that since genesis (SPEC §2.4).
9. The reference implementation
Status: it exists for both lanes.
| where | verification | |
|---|---|---|
| MachinaHash-C | internal/randomx/ (+ csrc/randomx/) |
RandomX v2 pinned to 615de64, the only change is the salt (internal/randomx/csrc/VENDOR-RANDOMX.md in the repository); randomx_conformance_test.go verifies our build against the official RandomX vectors in light and fast mode alike |
| MachinaHash-G | internal/mh/bcore.go, dag.go |
1,000 test vectors |
| The GPU (lane G) | machinahash-cuda |
500 lane G vectors bit-exact on sm_86 and sm_120 |
| The node | cmd/powmachinad |
it calls internal/mh directly — no second implementation |
- One implementation of each algorithm. MachinaHash-C is the vendored canonical RandomX — one of our own must not be written, not even in Go. MachinaHash-G is Go and both the node and the miner call it directly. → A consensus disagreement between two implementations cannot arise, which is the most common way a new chain dies on a fork.
- The GPU kernel does not get the RANDMATH sequence by computing it, but as a constant from the host. KISS99 in two implementations would be exactly the one-constant difference that splits a network — and somebody would only notice it at an epoch change.
- No floats in our code. (RandomX does use floats, but with firmly defined rounding — that is part of its specification and of its audits.)
10. Known risks
| Risk | Impact | Mitigation |
|---|---|---|
| Rented RandomX hashrate on lane C | a 51 % attack — Monero has orders of magnitude more hashrate and NiceHash sells it | the lane rotation rule (§4): an attacker with rented CPUs makes at most two blocks in a row, then has to deliver lane G, which they cannot rent. On top of that MESS and the finality quorum (security). It is the price of not having invented our own VM. |
| Botnets in lane C | a guaranteed 1/2 of the emission for stolen hardware | RandomX asks for 2.1 GiB per miner in fast mode — a botnet cannot inconspicuously ask for that on somebody else’s machine; light mode is orders of magnitude slower |
| A bug in RandomX’s JIT | the node executes generated machine code over data from foreign blocks | RANDOMX_FLAG_SECURE (W^X) for the Verifier role, the vendored commit pinned after the JIT bug fix from v2.0.1 (internal/randomx/csrc/VENDOR-RANDOMX.md in the repository) |
| GPUs handle lane G better than we expect | — | that is the intent; gate 2 is 17.3× (§7.7) |
| 51 % on one lane | a reorg | the rotation rule, §4 |
| No pool supports MachinaHash | nobody mines it | lane C is RandomX, so a variant in xmrig is enough (precedent: rx/scash, rx/pcoin); lane G needs its own stratum — it is in the roadmap, not an afterthought (the lesson from KRX) |
| Calibration by 3 machines | the parameters are hit for an EPYC + a 6000 + a 3090, not for ordinary hardware | widen the test hardware (§7.4) |
11. Sources / references
- XELIS Hash (v2/v3) — https://docs.xelis.io/features/mining/xelis-hash
- zawy12, Multi-POW to attract all POWs — https://github.com/zawy12/difficulty-algorithms/issues/52
- Evaluating Memory-Hard Proof-of-Work Algorithms on Three Processors (VLDB) — http://www.vldb.org/pvldb/vol13/p898-feng.pdf
- Itsuku: a Memory-Hardened Proof-of-Work Scheme — https://eprint.iacr.org/2017/1168.pdf