Algorithm.md
August 2, 2018 · View on GitHub
小记
在一个 epoch 中, dataset 和 cache 都是确定的, pow 其实就是利用 dataset 中的数据、区块 hash 以及 guess 的 nonce 值来计算 digest 和 result。
在计算 result 过程中,会利用 dataset 中的数据以及fnv算法来将 result 值变得不可预测和不可破解,你这样乖乖一步一步的改变 nonce 值来寻找满足要求的 result 值。
result = crypto.Keccak256(append(seed, digest...)
pow 大致过程可能就是这样吧~
不得不服以太坊真是个牛逼的项目~
具体算法细节~暂时不看了,太费时了~
hashimotoFull
hashimotoFull函数的入参为:巨大的辅助数组 dataset、区块头 hash(不包括nonce)、nonce,该函数主要定义了查找表 lookup ,并调用 hashimoto 函数来进行工作量证明计算。
// hashimotoFull aggregates data from the full dataset (using the full in-memory
// dataset) in order to produce our final value for a particular header hash and
// nonce.
func hashimotoFull(dataset []uint32, hash []byte, nonce uint64) ([]byte, []byte) {
lookup := func(index uint32) []uint32 { // 定义了一份 lookup 函数
offset := index * hashWords // hashWords == 16
return dataset[offset : offset+hashWords] // 返回一个 64 bytes 数据集
}
return hashimoto(hash, nonce, uint64(len(dataset))*4, lookup)
}
hashimoto
hashimoto函数的返回值是不可预知的,即运行函数前是无法确定给定参数的输出值(result[]),所以只能一个一个nonce去尝试,毕竟如果result[]生成过程存在被破译的途径,那么必然有方法可以更快地找到符合条件的数组,通过更快的挖掘出区块,在整个以太坊系统中逐渐占据主导。所以Ethash共识算法应用了非常复杂的一系列运算,包含了多次、多种不同的哈希函数运算。
具体算法目前还没有完全理解~后续再分析~
又回到这里,继续阅读吧~
hashimoto 中入参 hash、nonce、size 都很好确定,关键在 lookup, 就从 [lookup] 开搞吧~
// hashimoto 聚合来自完整数据集的数据,以便为特定的 hash 和 nonce 生成最终值(pow值)
// 关于参数 [size](https://github.com/xianfeng92/go-ethereum/blob/45bd4feddeadfbde5d1e560797155aacb0abbadf/consensus/ethash/algorithm.go#L405)
// 其返回值为两个长度均为32的byte数组 - digest[]和result[]
func hashimoto(hash []byte, nonce uint64, size uint64, lookup func(index uint32) []uint32) ([]byte, []byte) {
// Calculate the number of theoretical rows (we use one buffer nonetheless)
rows := uint32(size / mixBytes) // mixBytes 128
// Combine header+nonce into a 64 byte seed
seed := make([]byte, 40) // 定义一个长度为40的字节数组
copy(seed, hash) // 将hash的前40个字节拷贝给seed
binary.LittleEndian.PutUint64(seed[32:], nonce)
seed = crypto.Keccak512(seed) // 此时 seed 为 [64]byte
seedHead := binary.LittleEndian.Uint32(seed) // 将 seed 从 []byte 转换为 []uint32 类型
// Start the mix with replicated seed
mix := make([]uint32, mixBytes/4) // mix 为长度为 32 []uint32
for i := 0; i < len(mix); i++ {
mix[i] = binary.LittleEndian.Uint32(seed[i%16*4:])
}
// Mix in random dataset nodes
temp := make([]uint32, len(mix))
// 循环的从 dataset 中取值混入到 mix 中
for i := 0; i < loopAccesses; i++ { // loopAccesses 64
parent := fnv(uint32(i)^seedHead, mix[i%len(mix)]) % rows
for j := uint32(0); j < mixBytes/hashBytes; j++ { // hashBytes 64 mixBytes 128
copy(temp[j*hashWords:], lookup(2*parent+j)) // hashWords 16
}
fnvHash(mix, temp)
}
// Compress mix
for i := 0; i < len(mix); i += 4 {
mix[i/4] = fnv(fnv(fnv(mix[i], mix[i+1]), mix[i+2]), mix[i+3])
}
mix = mix[:len(mix)/4]
digest := make([]byte, common.HashLength)
for i, val := range mix {
binary.LittleEndian.PutUint32(digest[i*4:], val)
}
return digest, crypto.Keccak256(append(seed, digest...))
}
hashimotoLight
hashimotoLight 中并不需要整个 dataset 数据集,只需要一个 cache 就可以了
重点关注这里的 lookup 函数,这里的 rawData 其实就是 dataset 中所使用到的数据集合,所以说在 hashimoto(hash, nonce, size, lookup) 就可以轻易验证 nonce 值是否满足 pow 的要求
// hashimotoLight aggregates data from the full dataset (using only a small
// in-memory cache) in order to produce our final value for a particular header
// hash and nonce.
func hashimotoLight(size uint64, cache []uint32, hash []byte, nonce uint64) ([]byte, []byte) {
keccak512 := makeHasher(sha3.NewKeccak512())
lookup := func(index uint32) []uint32 { // 自己去生成指定 index 的 data,其实也就是指定 index 的 dataset 中的数据集合
rawData := generateDatasetItem(cache, index, keccak512)
data := make([]uint32, len(rawData)/4)
for i := 0; i < len(data); i++ {
data[i] = binary.LittleEndian.Uint32(rawData[i*4:])
}
return data
}
return hashimoto(hash, nonce, size, lookup)
}
generateDatasetItem
// generateDatasetItem 组合来自256个伪随机选择的缓存节点的数据,以及用于计算单个数据集节点的哈希值。
func generateDatasetItem(cache []uint32, index uint32, keccak512 hasher) []byte {
// Calculate the number of theoretical rows (we use one buffer nonetheless)
rows := uint32(len(cache) / hashWords)
// Initialize the mix
mix := make([]byte, hashBytes)
binary.LittleEndian.PutUint32(mix, cache[(index%rows)*hashWords]^index)
for i := 1; i < hashWords; i++ {
binary.LittleEndian.PutUint32(mix[i*4:], cache[(index%rows)*hashWords+uint32(i)])
}
keccak512(mix, mix)
// Convert the mix to uint32s to avoid constant bit shifting
intMix := make([]uint32, hashWords)
for i := 0; i < len(intMix); i++ {
intMix[i] = binary.LittleEndian.Uint32(mix[i*4:])
}
// fnv it with a lot of random cache nodes based on index
for i := uint32(0); i < datasetParents; i++ {
parent := fnv(index^i, intMix[i%16]) % rows
fnvHash(intMix, cache[parent*hashWords:])
}
// Flatten the uint32 mix into a binary one and return
for i, val := range intMix {
binary.LittleEndian.PutUint32(mix[i*4:], val)
}
keccak512(mix, mix)
return mix
}
Lookup
lookup 主要是根据其参数 index 来从 dataset 中取数据,那么 dataset 的数据如何产生的呢?
来看看 dataset
lookup := func(index uint32) []uint32 { // 定义了一个 lookup 函数
offset := index * hashWords // hashWords == 16
return dataset[offset : offset+hashWords] // 返回一个 64 bytes 数据集,直接从 dataset 切片获取
}
generateCache
// generateCache 为输入 seed 创建给定大小的验证缓存
// 缓存生成过程包括首先按顺序填充32 MB memory,然后执行 then performing two passes of Sergio Demian Lerner's RandMemoHash
// algorithm from Strict Memory Hard Hashing Functions (2014) 输出是a一组524288 64-byte 值
// 此方法将结果以机器字节顺序放入dest
func generateCache(dest []uint32, epoch uint64, seed []byte) {
// Print some debug logs to allow analysis on low end devices
logger := log.New("epoch", epoch)
start := time.Now()
defer func() {
elapsed := time.Since(start)
logFn := logger.Debug
if elapsed > 3*time.Second {
logFn = logger.Info
}
logFn("Generated ethash verification cache", "elapsed", common.PrettyDuration(elapsed))
}()
// Convert our destination slice to a byte buffer
header := *(*reflect.SliceHeader)(unsafe.Pointer(&dest))
header.Len *= 4
header.Cap *= 4
cache := *(*[]byte)(unsafe.Pointer(&header))
// Calculate the number of theoretical rows (we'll store in one buffer nonetheless)
size := uint64(len(cache))
rows := int(size) / hashBytes
// Start a monitoring goroutine to report progress on low end devices
var progress uint32
done := make(chan struct{})
defer close(done)
go func() {
for {
select {
case <-done:
return
case <-time.After(3 * time.Second):
logger.Info("Generating ethash verification cache", "percentage", atomic.LoadUint32(&progress)*100/uint32(rows)/4, "elapsed", common.PrettyDuration(time.Since(start)))
}
}
}()
// Create a hasher to reuse between invocations
keccak512 := makeHasher(sha3.NewKeccak512())
// Sequentially produce the initial dataset
keccak512(cache, seed)
for offset := uint64(hashBytes); offset < size; offset += hashBytes {
keccak512(cache[offset:], cache[offset-hashBytes:offset])
atomic.AddUint32(&progress, 1)
}
// Use a low-round version of randmemohash
temp := make([]byte, hashBytes)
for i := 0; i < cacheRounds; i++ {
for j := 0; j < rows; j++ {
var (
srcOff = ((j - 1 + rows) % rows) * hashBytes
dstOff = j * hashBytes
xorOff = (binary.LittleEndian.Uint32(cache[dstOff:]) % uint32(rows)) * hashBytes
)
bitutil.XORBytes(temp, cache[srcOff:srcOff+hashBytes], cache[xorOff:xorOff+hashBytes])
keccak512(cache[dstOff:], temp)
atomic.AddUint32(&progress, 1)
}
}
// Swap the byte order on big endian systems and return
if !isLittleEndian() {
swap(cache)
}
}
generateDataset
重点关注 copy(dataset[index*hashBytes:], item) 每次使用新生成的 item 填充 64 个dataset中的元素
而 item := generateDatasetItem(cache, index, keccak512),故当 cache 和 index 确定时,就可以从 dataset 中取出确定的元素
这里的 cache 为 generateDataset的入参
// generateDataset生成用于 mining 的整个ethash数据集。
// 此方法将结果以机器字节顺序放入dest。
func generateDataset(dest []uint32, epoch uint64, cache []uint32) {
// Print some debug logs to allow analysis on low end devices
logger := log.New("epoch", epoch)
start := time.Now()
defer func() {
elapsed := time.Since(start)
logFn := logger.Debug
if elapsed > 3*time.Second {
logFn = logger.Info
}
logFn("Generated ethash verification cache", "elapsed", common.PrettyDuration(elapsed))
}()
// 弄清楚是否需要为机器交换字节
swapped := !isLittleEndian()
// Convert our destination slice to a byte buffer
// 将目标切片转换为字节缓冲区
header := *(*reflect.SliceHeader)(unsafe.Pointer(&dest))
header.Len *= 4
header.Cap *= 4
dataset := *(*[]byte)(unsafe.Pointer(&header))
// Generate the dataset on many goroutines since it takes a while
// 利用多个 goroutine 去生成数据集
threads := runtime.NumCPU()
size := uint64(len(dataset))
var pend sync.WaitGroup
pend.Add(threads)
var progress uint32
for i := 0; i < threads; i++ {
go func(id int) {
defer pend.Done()
// Create a hasher to reuse between invocations
keccak512 := makeHasher(sha3.NewKeccak512())
// Calculate the data segment this thread should generate
batch := uint32((size + hashBytes*uint64(threads) - 1) / (hashBytes * uint64(threads)))
first := uint32(id) * batch
limit := first + batch
if limit > uint32(size/hashBytes) {
limit = uint32(size / hashBytes)
}
// Calculate the dataset segment
percent := uint32(size / hashBytes / 100)
for index := first; index < limit; index++ {
item := generateDatasetItem(cache, index, keccak512)
if swapped {
swap(item)
}
copy(dataset[index*hashBytes:], item) //每次使用新生成的 item 填充 64 个dataset中的元素
if status := atomic.AddUint32(&progress, 1); status%percent == 0 {
logger.Info("Generating DAG in progress", "percentage", uint64(status*100)/(size/hashBytes), "elapsed", common.PrettyDuration(time.Since(start)))
}
}
}(i)
}
// Wait for all the generators to finish and return
pend.Wait()
}