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