ASIC friendly computation method of ProgPoW
ProgPow has a design flaw:
64 bit
seedis too small,
This allows ASICs compute hash without memory access.
Thanks chfast for providing a readable implementation!
ProgPoW hash function is defined as:
result hash(const epoch_context& context, int block_number, const hash256& header_hash,
    uint64_t nonce) noexcept
{
    const uint64_t seed = keccak_progpow_64(header_hash, nonce);
    const hash256 mix_hash = hash_mix(context, block_number, seed, calculate_dataset_item_2048);
    const hash256 final_hash = keccak_progpow_256(header_hash, seed, mix_hash);
    return {final_hash, mix_hash};
}Suppose a block header block_header and a block number block_number are given.
Then, run 3 steps below:
- fix a seedto any 64 bit value and computemix_hash = hash_mix(block_number, seed).
- search extra_nonceso thatheader_hashmeets diffculty condition.
- search nonceso thatkeccak_progpow_64(header_hash, nonce) == seed.
In first step, a mix_hash is computed for a fixed seed and block_number.
Because mix_hash is designed to be a function of seed and block_number, we get a valid triple (seed, mix_hash, block_number).
Now, our goal is find a header_hash and a nonce satisfy two conditions:
- (A) keccak_progpow_64(header_hash, nonce) == seed
- (B) keccak_progpow_256(header_hash, seed, mix_hash) <= boundary
Remember we can generate any number of header hash by modifying extra nonce (use ExtraData in Ethereum).
Thus, condition (B) is easily acomplished by step 2.
Now, we have a fixed (header_hash, seed, mix_hash, block_number) but nonce is free yet.
Finally, step 3 scans nonce for condition (A).
Because seed has only 64 bit length, condition (A) provides only 64 bit security and step 3 can be executed by ASICs.
There are four functions keccak_1600, keccak_progpow_64, hash_mix and keccak_progpow_256.
Computation cost can be evaluated by counting these function calls needed before getting an answer.  This depends on network difficulty D.
| avg # of calls in normal | avg # of calls in ASIC | |
|---|---|---|
| keccak_1600 | 1 | D | 
| keccak_progpow_64 | D | 2^64 | 
| hash_mix | D | 1 | 
| keccak_progpow_256 | D | D | 
In normal hash computation, one keccak_1600 call is needed to compute header_hash from block_header and other functions are sequencially called for each nonce value.
In ASIC hash computation, one hash_mix call is needed in step 1, keccak_1600 and keccak_progpow_256 are called in step 2, and keccak_progpow_64 is called in step 3.
Since hash_mix is called only once in our ASIC computation, we can use host CPU to compute hash_mix.
Other functions are all keccak hash function, need no memory, and are easily computed on ASICs.
We need compare D and 2^64 in keccak_progpow_64 row.
Simply, larger D makes ASIC more profitable.
Estimating profitable threshold is hard, but I think current difficulty (> 2^50) is large enough.
Demo is in this repository.
$ git clone https://github.com/kik/progpow-exploit.git
$ cd progpow-exploit
$ mkdir build
$ cd build
$ cmake ..
$ make
$ ./test/ethash-test --gtest_filter=asic.search
In this demo, seed is truncated to 24 bit width to run on CPU.
See code.
Test code is simple.
search_asic is defined here.
I beleave disclosing a PoW vulnerability is more profitable than secret mining 😍
ETH: 0xcFc9751Bc71e412c19D877e6401c92d74d8e4344