Skip to main content

Proof of work

Epic runs three proof-of-work algorithms on one chain at the same time, in a ratio enforced by consensus. Every block declares which algorithm produced it. All three are always valid, in defined proportions, rather than one replacing another at a fork.

The three algorithms

AlgorithmBottleneckMinimum difficulty
Cuckoo / CuckatooMemory-bound graph cycle search3
RandomXCPU4,000
ProgPowGPU200,000

Cuckoo parameters: proof size 42, minimum edge bits 19, second proof-of-work edge bits 31, base edge bits 24.

The algorithm implementations live in separate crates, EpicCash/randomx-rust and EpicCash/progpow-rust.

Difficulty is tracked separately per algorithm, so get_status returns tip.total_difficulty as an object with one u64 per algorithm, keyed cuckaroo, cuckatoo, randomx and progpow.

A block header names its algorithm in proof and carries the solution in solution, an externally tagged value whose shape depends on that algorithm:

header.proof and header.solution, field shapes
"proof": "Cuckoo" "solution": {"Cuckoo": [<u64>, ...]} 42 nonces
"proof": "RandomX" "solution": {"RandomX": "<decimal string>"} 32-byte hash, base 10
"proof": "ProgPow" "solution": {"ProgPow": [<byte>, ...]} 32-byte mix, one number per byte
"proof": "MD5" "solution": {"MD5": "<string>"} not scheduled on mainnet

Cuckatoo and Cuckaroo blocks both report Cuckoo in proof and are distinguished by edge bits. The types are in api/src/types.rs:574.

The block policy

Which algorithm the next block must use is decided by a deterministic policy. The implementation holds a counter per algorithm and selects whichever is furthest behind its allotted share of the current 100-block window, so the mix over a window is exact rather than an average.

Allotments per 100 blocks:

EraRandomXProgPowCuckatooCuckaroo
Eras 1 to 2603820
Era 3 onward484840

Cuckaroo is allotted zero on mainnet. It exists in the type system and is not scheduled.

The policy is compiled into the node and is not configurable through epic-server.toml on mainnet. Two keys narrow it on a test network:

epic-server.toml, floonet and usernet only
[server]
only_randomx = true # every block must be RandomX
no_progpow = false # or just drop ProgPow from the mix

only_randomx = true is what lets a single CPU miner advance a private chain: under the mixed policy a RandomX-only miner idles on every ProgPow height. Both keys change the block policy, so a chain mined under one setting is not valid under the other. Wipe chain_data when you change either.

The mainnet allotments are at core/src/core/block/feijoada.rs:249,

next_block_bottles()

advances the window and resets it at 100 blocks, choose_algo() makes the selection over PolicyConfig, and get_bottles_default() returns the zeroed counters each window starts from. The constants are in core/src/consensus.rs.

Difficulty

Each algorithm has its own difficulty track, retargeted independently by next_difficulty(). The adjustment window is 60 blocks (DIFFICULTY_ADJUST_WINDOW), one hour at the 60-second target, and the window walks back over blocks of the same algorithm only, so each track sees its own history.

All three tracks share the same clamp factor of 2 and damp factor of 3.

Consensus changes by height

Two consensus changes are keyed to mainnet block heights rather than to a version fork:

HeightChange
501,160The difficulty rule changes, and the RandomX and ProgPow minimums move from 5,000 and 100,000 to 4,000 and 200,000
880,000The graph weight expiry factor changes, from one year to one hundred years of blocks

An implementation that honours only declared fork versions diverges at both. The first declared hard fork height is 9,000,000, which at 60-second blocks is more than a decade away, so later header versions are reachable only on test networks.

Next

A miner connects over Stratum on port 3416, raw TCP with newline-delimited JSON, or over the node's foreign JSON-RPC surface with get_block_template, finalize_block_template and submit_block. EpicCash/epic-miner is the reference mining client.